[WIL 6주차] 항해 99 팀프로젝트 애견동물병원 예약 플랫폼 with 리액트

hoonie·2021년 7월 17일
0

WIL

목록 보기
6/7
post-thumbnail

이번 항해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));
  };
};

코드 설명

  • 로그인시 유저의 아이디와 패스워드를 파라미터로 서버로 전송 후, 서버에서 해당 유저가 DB에 존재하면 랜덤한 토큰값을 response.data에 응답값으로 전송 -> 클라이언트는 해당 토큰값을 받아 axios의 headers에 기본 Authorization 전송값 설정 -> token이라는 쿠키이름으로 해당 토큰값을 저장하는 setCookie 실행 -> 정상적으로 처리되면 메인페이지로 이동

메인페이지 (병원리스트 조회)

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
);

코드 설명

  • aixos를 인스턴스화하여 통신 후 getHospitals 라는 리덕스 액션 함수를 사용하여 리덕스 상태값에 병원 리스트들 저장 후 map 함수를 이용하여 클라이언트 단에 출력

병원 상세페이지

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);

  
  

코드 설명

  • useParams()를 이용하여 url 파라미터값 받아오기 -> useEffect를 이용하여 초기 렌더링시 getHospitalDB 함수안에 해당 id값을 넣어서 리덕스 액션함수 호출 -> 리덕스 상태값에 해당 병원 상세 데이터를 저장

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
);

코드 설명

  • useParams()를 이용하여 url 파라미터값 받아오기 -> useEffect를 이용하여 초기 렌더링시 getReviewDB 함수안에 해당 id값을 넣어서 리덕스 액션함수 호출 -> 리덕스 상태값에 해당 병원 리뷰 데이터를 저장

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));
    });
  };
};

코드 설명

  • handleAddReview 함수 안에 리뷰 평점과 내용을 담아 파라미터로 전송 -> axios post 메서드에 리뷰아이디, 평점과 내용을 전달 -> getReviewDB 실행 후 다시 리덕스 상태값에 반영된 state 적용
  1. 지도를 이용한 병원 위치 조회
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);
      }
    });
  }, []);
  

코드 설명

  • 리덕스 상태값에서 병원 주소를 불러와 카카오 API를 이용하여 지도에 표기할 주소 삽입

진료별 필터링 페이지

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);
    });
  };

코드 설명

  • 카테고리 클릭시 changeColor 함수로 클릭한 카테고리에 색깔을 입힘
  • 해당 카테고리 키워드를 인코딩하여 서버로 전송 후 데이터 받아와서 setDate 후 병원리스트 출력

예약페이지

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);
      });
  };
};

코드 설명

  • 예약하기 클릭시 reservate 함수 실행 -> 예약하기 정보들 변수로 담아서 addReservationDB 리덕스 함수 실행 -> 데이터베이스에 예약내역 저장

마이페이지

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));
        });
      });
    });
  };
};

코드 설명

  • 카메라 버튼을 클릭하여 selectFile 함수를 실행시켜 사진을 올리면 해당 이미지가 미리보기 형태로 사진 출력 -> 오른쪽 체크 버튼 클릭하여 uploadFB 실행 -> 해당 이미지 파일이 firebase 내 storage로 저장 -> 저장이 완료되면 이미지 url를 받아오고 그 url를 상태값에 저장 -> /userinfo/image api와 통신하여 디비에도 프로필 이미지 업데이트 -> 리덕스 user 상태값에도 프로필 이미지 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");
  };
};

코드 설명

  • 로그아웃 버튼 클릭시 logoutDB 함수 실행 -> jwt 토큰값 저장한 token 쿠키 삭제 -> axios header 값 null 로 변경 -> 메인페이지로 이동

느낀점

처음으로 리액트를 사용하여 백엔드와 협업을 하였는데, 매우 보람찬 시간이었던것 같다. 협업을 진행하면서 느꼈던것은 일반 개인프로젝트와는 다르게 커뮤니케이션이 매우 중요함을 느꼈다. 서로 원하는 기획의 방향성에 대해 합을 맞춰가는것도 중요하며, api 설계등 많은 부분에 대해 합의를 봐야한다. 이 과정에서 누군가 이기심으로 변하거나 어긋나는 경우는 프로젝트가 산으로 가게 될 것 같았다. 어떠한 부분이 어려우면 왜 어려운지 무작정 우기는 것이 아니라 잘 이해시키며 해결해나가는 능력도 필요할것같았다.

회사에 들어가게되면 개발은 절대 혼자하는것이아니다. 팀동료가 있을 것이며, 커뮤니케이션은 피해갈 수 없는 개발자의 중요한 숙명이다. 개인적인 개발 실력을 늘려가는 것도 중요하지만 남은 항해99 팀프로젝트를 하면서 이 커뮤니케이션 능력을 더욱 더 길러가는 시간을 보냈으면 좋겠다고 생각했다.


URL

와이어프레임 - 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

0개의 댓글