이번 항해99 6주차는 처음으로 리액트를 사용하여 팀 미니 프로젝트를 만들어보는것이었다!
드디어 내가 사랑하는 리액트를 이용해서 백엔드 개발자와 첫 팀플을 하다보니 매우 즐겁게 한것 같았다.
프로젝트 기간 : 2021-07-09 ~ 2012-07-15
프로젝트명 : 아프지멍 (애견동물병원 예약플랫폼)
팀 구성 : 리액트 개발자 3명, 스프링부트 개발자 2명
사용 기술 : 리액트, 파이어베이스, 스프링부트
사용 IDE : VS Code
프로젝트 설명 : 진료받고자 하는 증상별 카테고리 선정후 전문성있는 병원들에 대한 예약시스템 구축
기능 : 회원가입 및 로그인, 프로필 이미지 업로드, 병원별 예약 및 리뷰 평점 시스템, 마이페이지 내 나의 예약내역 조회, 진료받고자 하는 증상병 카테고리 필터링
프로젝트 진행 순서 : 프로젝트 주제 선정 -> 와이어프레임 -> 필요한 기능 선정 -> 필요한 api 별 정리(url 및 파라미터명) -> 프론트엔드 및 백엔드 개발자들 역할 분담 -> 기능 구현 -> 기능 테스트
와이어프레임 url - https://ovenapp.io/view/gMfUj8EvR4DBv1FG6g5BwpVkd6ur0LrA#sNWSj
notion url - https://www.notion.so/26-ac379cabede94755ae8303bba6f9361c
1. 회원가입후 로그인할때 서버에서 랜덤한 토큰값을 받아와 해당 토큰을 axios header와 웹서비스 쿠키에 저장하여 서버와 통신하는 jwt 방식 사용
const loginDB = (userName, password) => {
return function (dispatch, getState, { history }) {
let login_info = {
userName,
password,
};
instance
.post("/user", login_info)
.then((response) => {
console.log(response);
const accessToken = response.data;
// API 요청하는 콜마다 해더에 accessTocken 담아 보내도록 설정
instance.defaults.headers.common["Authorization"] = `${accessToken}`;
//받은 token 쿠키에 저장
setCookie("token", accessToken, 1, "/");
// const token = getCookie("token");
dispatch(setUser(login_info));
history.push("/pages/mainpage");
})
.catch((error) => console.log("로그인 중 에러가 발생했어요!", error));
};
};
1. 데이터베이스에 저장된 병원 리스트 출력
const getHospitalsDB = () => {
return function (dispatch, getState, { history }) {
instance.get("/hospitals").then((result) => {
dispatch(getHospitals(result.data));
});
};
};
const getHospitals = createAction(GET_HOSPITALS, (hospitals) => ({
hospitals,
}));
export default handleActions(
{
[GET_HOSPITALS]: (state, action) => {
return produce(state, (draft) => {
draft.hospital_list = action.payload.hospitals;
});
},
},
initialState
);
1. 해당 병원의 id값을 조회한 후 알맞은 병원의 상세 정보를 출력
const { id } = useParams();
useEffect(() => {
dispatch(userActions.loginCheckDB());
dispatch(getHospitalDB(id));
if (location.state !== undefined) {
setTabIndex(location.state.tabIndex);
}
}, []);
export const getHospitalDB = (id) => {
return function (dispatch, getState, { history }) {
const token = getCookie("token");
instance.defaults.headers.common["Authorization"] = `${token}`;
instance.get(`/hospitals/${id}`).then((result) => {
dispatch(getHospital(result.data));
});
};
};
const getHospital = createAction(GET_HOSPITAL, (hospital) => ({ hospital }));
export default handleActions(
{
[GET_HOSPITAL]: (state, action) =>
produce(state, (draft) => {
draft.hospital = action.payload.hospital;
}),
},
initialState
);
const hospital = useSelector((state) => state.hospital.hospital);
2. 해당 병원이 작성된 리뷰 리스트 출력
const { id } = useParams();
useEffect(() => {
dispatch(actionCreators.getReviewDB(id));
}, []);
const getReviewDB = (id) => {
return function (dispatch, getState, { history }) {
const token = getCookie("token");
instance.defaults.headers.common["Authorization"] = `${token}`;
instance.get(`/hospitals/${id}/reviews`).then((result) => {
dispatch(getReview(result.data));
});
};
};
const getReview = createAction(GET_REVIEW, (review) => ({ review }));
export default handleActions(
{
[GET_REVIEW]: (state, action) => {
return produce(state, (draft) => {
draft.review_list = action.payload.review;
});
},
},
initialState
);
3. 리뷰 작성
const handleAddReview = (review) => {
dispatch(actionCreators.addReviewDB(id, review));
};
const addReviewDB = (id, review) => {
return function (dispatch, getState, { history }) {
const token = getCookie("token");
instance.defaults.headers.common["Authorization"] = `${token}`;
const { reviewContent, hospitalRate } = review;
const new_review = {
reviewContent,
hospitalRate,
};
instance.post(`/hospitals/${id}/reviews`, new_review).then((result) => {
dispatch(getReviewDB(id));
});
};
};
const { kakao } = window;
const hospital = useSelector((state) => state.hospital.hospital);
const { hospitalName, hospitalLocation, hospitalNumber } = hospital;
useEffect(() => {
const mapContainer = document.getElementById("myMap"), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3, // 지도의 확대 레벨
};
// 지도를 생성합니다
const map = new kakao.maps.Map(mapContainer, mapOption);
// 주소-좌표 변환 객체를 생성합니다
const geocoder = new kakao.maps.services.Geocoder();
// 주소로 좌표를 검색합니다
geocoder.addressSearch(hospitalLocation, function (result, status) {
// 정상적으로 검색이 완료됐으면
if (status === kakao.maps.services.Status.OK) {
const coords = new kakao.maps.LatLng(result[0].y, result[0].x);
// 결과값으로 받은 위치를 마커로 표시합니다
const marker = new kakao.maps.Marker({
map: map,
position: coords,
});
// 인포윈도우로 장소에 대한 설명을 표시합니다
const infowindow = new kakao.maps.InfoWindow({
content: `<div style="width:150px;text-align:center;padding:6px 0;">${hospitalName}</div>`,
});
infowindow.open(map, marker);
// 지도의 중심을 결과값으로 받은 위치로 이동시킵니다
map.setCenter(coords);
}
});
}, []);
1. 카테고리 클릭시 해당 카테고리 전문성을 지니고 있는 병원 리스트 출력
const search = (e, idx) => {
changeColor(e, idx);
const keyword = e.target.textContent;
// 한글로 보내면 에러나기때문에 인코딩 후 서버에서 디코딩 진행
const encode = encodeURIComponent(keyword);
instance.get(`/hospitals/search?subject=${encode}`).then((response) => {
setData(response.data);
console.log(response.data);
});
};
1. 반려견 이름, 예약 일정 그리고 요청사항을 작성 후 예약하기 누르면 병원 예약 완료
const reservate = () => {
dispatch(
reservationActions.addReservationDB(id, dogName, schedule, request)
);
};
const addReservationDB = (
hospitalId,
dogName,
reservationDate,
reservationDetail
) => {
return function (dispatch, getState, { history }) {
let new_reservation = {
hospitalId,
dogName,
reservationDate,
reservationDetail,
};
const token = getCookie("token");
instance.defaults.headers.common["Authorization"] = `${token}`;
instance
.post(
"/reservations",
new_reservation
// ... add other header lines like: 'Content-Type': 'application/json'
)
.then((response) => {
console.log(response);
switch (response.data.msg) {
case "success":
dispatch(addReservation(new_reservation));
window.alert(
"예약이 완료되었습니다. 마이페이지에서 예약 목록을 확인해 보세요 :)"
);
history.push("/pages/mypage");
break;
case "not_login":
window.alert("로그인이 필요합니다!");
history.replace("/login");
break;
default:
// window.alert("예약 신청 중 오류가 생겼네요! 다시 부탁드려요!");
dispatch(addReservation(new_reservation));
window.alert("예약이 완료되었습니다.");
history.push("/pages/mypage");
break;
}
})
.catch((error) => {
console.log("예약 저장 중 오류 발생!", error);
});
};
};
1. 프로필 이미지 변경
const selectFile = (e) => {
const reader = new FileReader();
const file = imageInput.current.files[0];
reader.readAsDataURL(file);
reader.onloadend = () => {
setPreview(reader.result);
};
};
const uploadFB = () => {
let image = imageInput.current.files[0];
dispatch(imageActions.uploadImageFB(image));
setTimeout(() => setDone(true), 500);
setTimeout(() => setDone(false), 3000);
};
const uploadImageFB = (image) => {
return function (dispatch, getState, { history }) {
const _upload = storage.ref(`images/${image.name}`).put(image);
_upload.then((snapshot) => {
console.log(snapshot);
snapshot.ref.getDownloadURL().then((url) => {
dispatch(uploadImage(url));
instance.post("/userinfo/image", { dogImage: url }).then((response) => {
dispatch(userActions.editUser(url));
});
});
});
};
};
2. 로그아웃
const logoutDB = () => {
return function (dispatch, getState, { history }) {
deleteCookie("token");
instance.defaults.headers.common["Authorization"] = null;
delete instance.defaults.headers.common["Authorization"];
dispatch(logout());
history.push("/pages/mainpage");
};
};
처음으로 리액트를 사용하여 백엔드와 협업을 하였는데, 매우 보람찬 시간이었던것 같다. 협업을 진행하면서 느꼈던것은 일반 개인프로젝트와는 다르게 커뮤니케이션이 매우 중요함을 느꼈다. 서로 원하는 기획의 방향성에 대해 합을 맞춰가는것도 중요하며, api 설계등 많은 부분에 대해 합의를 봐야한다. 이 과정에서 누군가 이기심으로 변하거나 어긋나는 경우는 프로젝트가 산으로 가게 될 것 같았다. 어떠한 부분이 어려우면 왜 어려운지 무작정 우기는 것이 아니라 잘 이해시키며 해결해나가는 능력도 필요할것같았다.
회사에 들어가게되면 개발은 절대 혼자하는것이아니다. 팀동료가 있을 것이며, 커뮤니케이션은 피해갈 수 없는 개발자의 중요한 숙명이다. 개인적인 개발 실력을 늘려가는 것도 중요하지만 남은 항해99 팀프로젝트를 하면서 이 커뮤니케이션 능력을 더욱 더 길러가는 시간을 보냈으면 좋겠다고 생각했다.
와이어프레임 - https://ovenapp.io/view/gMfUj8EvR4DBv1FG6g5BwpVkd6ur0LrA#sNWSj
notion 기획 - https://www.notion.so/26-ac379cabede94755ae8303bba6f9361c
github - https://github.com/Sparta-MungMung/mungmung_client
구현 도메인 - http://munghospital.shop/
프로젝트 소개 유튜브 url - https://www.youtube.com/watch?v=Sd98UjrPmB4