코드스테이츠 부트캠프

Main Project를 마치고 - 1

als982001 2023. 5. 28. 14:56

1. 개요

Main Project가 5월 25일자로 끝이 났다. 결과물을 점검하며 처음 프로젝트 시작 때의 목표와 비교해보았다. 이 때의 목표와 결과물을 비교하니, 어쩐지 초라해보이는 것 같다. 캠핑장 등록이 무사히 완료되면 공간의 분야를 넓히기로 했었는데, 겨우 캠핑장 등록만을 완성했다. 어쩌면 나의 실력을 너무 과신하였는지도 모른다. 하지만, 프로젝트를 진행하며 나의 실력에 대해 다시금 깨닫게 되었다. 그리고 이를 인지하고 결과물을 평가해보자면, 나쁘지는 않은 것 같다. 결과물은 아래에서 확인할 수 있다.

- http://aircamp-codestates-019.s3-website.ap-northeast-2.amazonaws.com/

그리고 팀원의 구성은 다음과 같다.

👩‍💻 Team

주재민
(FE, FE팀장)
오준석
(FE)
김정환
(FE)
유한별
(BE, BE팀장)
조현우
(BE)
변상현
(BE)
주재민 오준석 김정환 유한별 조현우 변상현
@als982001 @JS2L @wjdghksdigh @exertivestar @mikiehw @SHyeonCoding
- Front - Front - Front - Back - Back - Back
           

2. 아키텍처 다이어그램

3. 이용 가능한 서비스

이제 우리 팀에서 구현한 기능들을 간단하게 설명할 것이다. 간단히 설명할 기능들은 회원가입, 로그인&로그아웃, 메인페이지, 캠핑장 상세검색, 상품 상세 페이지, 예약 페이지, 마이 페이지, 판매 등록 페이지, 결제 페이지, 리뷰, AI 채팅, 회원 탈퇴, 관리자 페이지, 다크모드이다. 이 중 본인이 담당한 기능은 회원가입, 로그인&로그아웃, 캠핑장 상세 검색, 마이 페이지 속 기능들, 판매 등록, 리뷰, AI 채팅, 관리자 페이지, 다크모드이다. 그리고 프론트엔드의 오준석님은 상품 상세 페이지, 예약(결제)를 담당하였고 프론트엔드의 김정환님은 전체적인 디자인을 담당하였다.

 

3-1. 회원가입

회원가입폼

회원가입을 위해서 이메일, 비밀번호, 이름, 전화번호, 생년월일 5가지를 입력 받는다. 이 때, 생년월일은 현재 날짜보다 미래의 날짜이면 회원가입을 진행할 수 없다. 그리고 비밀번호는 최소 8자리 이상이어야 하며 대문자, 소문자, 특수문자를 하나씩 포함해야 한다. 그리고 전화번호는 010/011로 시작해야 하며, '-'를 기준으로 3글자-3글자-4글자 혹은 3글자-4글자-4글자이어야 한다. 이 때, 입력한 비밀번호와 전화번오의 패턴을 if문으로 하나씩 검사한다면 매우 거추장스러운 코드가 될 것이기에, 이는 정규표현식을 이용하여 다음과 같이 검사하였다.

// 비밀번호가 유효한지 판별하는 함수입니다.
export const checkValidPassword = (password) => {
  const passwordPattern = /^(?=.*[A-Z])(?=.*[a-z])(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

  return passwordPattern.test(password);
};

// 전화번호가 유효한지 판별하는 함수입니다.
export const checkValidPhone = (phone) => {
  const phonePattern = /^(010|011|016|017|018|019)-\d{3,4}-\d{4}$/;

  return phonePattern.test(phone);
};

그리고 이메일 인증 버튼을 누르면 입력한 이메일로 인증 번호가 다음과 같이 발송된다.

인증코드

그리고 전송된 인증번호를 입력한 후, 회원가입을 완료하면 아래와 같이 회원가입이 완료되었다고 메일이 오게 된다.

회원가입 완료

 

3-2. 로그인&로그아웃

로그인폼

로그인을 하기 위해서는 이메일과 비밀번호가 필요하다. 로그인을 하기 위해서는 두 가지 방법이 있다. 우선, 회원가입을 통해 생성한 계정의 이메일과 비밀번호를 입력해 로그인을 진행하는 것이고, 두 번째는 카카오 계정을 통해 로그인하는 방법이다. 카카오 로그인을 구현하는 과정은 나중에 다시 이야기할 예정이다. 이 때, 만약 카카오 게정과 같은 이메일의 계정이 존재한다면 다음과 같이 카카오 로그인을 진행할 수 없게 구현하였다.

이미 존재하는 계정이라고 안내한다

로그인을 할 경우, Authorization와 Refresh라는 두 개의 JWT 토큰 키 값을 받는다. 그리고 이를 해독하여 회원 정보를 받는다. 그리고 본래라면 리프레시 토큰을 이용해 로그인이 만료된다면 이를 갱신할 수 있겠지만, API 명세서에는 이 기능이 없었을 뿐더러 리프레시 토큰을 해독해도 회원의 이메일만 있어서 써먹을 방법이 없었다. 그래서 토큰의 기간이 만료될 경우, 토큰이 만료되었다는 알림과 함꼐 강제로 로그아웃하는 것으로 처리했다.

 그리고 로그인한 회원 정보는 redux를 이용해 저장하였다. 그런데 단순히 redux만 이용할 경우, 새로고침만 해도 로그인한 정보가 사라진다. 그렇기에 redux와 redux-persist를 같이 이용하여 로그인 정보를 저장하였다. 

저장된 회원 정보

그리고 우측 상단의 로그아웃 버튼을 누르면 로그아웃을 할 수 있다.

우측 상단의 로그아웃 버튼과 마이페이지 버튼

 

3-3. 메인 페이지

메인 페이지에서는 등록된 상품들을 바로 볼 수 있게 하였다. 메인 페이지에서 상품을 바로 볼 수 있게 하는 부분은 마이리얼트립을 참고하였다. 처음에는 무한스크롤을 이용해 등록된 상품들을 메인에서 전부 볼 수 있게 하려 했다. 하지만 이는 너무 단조로워보이기도 했고, 참고한 마이리얼트립은 메인 페이지에서 카테고리 별로 상품들을 보여주는데, 이게 더 보기 좋다고 생각하여 우리 프로젝트에서도 카테고리 별로 상품들을 보여주기로 하였다. 현재 이미지에는 없지만, 1,2인실과 강원도 지역으로 두 가지 카테고리가 더 존재한다. 원래라면 카테고리가 더 많아야 한다. 하지만 이 부분은 프로젝트 구현에 있어 우선순위가 낮았기도 했고 등록된 상품 데이터가 적어 구현을 미처 하지 못했다.

그리고 각 상품들이 한 줄에 4개씩 보여지는데, 저 하나하나를 카드라고 칭하였다. 카드의 앞면에는 캠핑장 사진과 이름, 가격을 보여준다. 그리고 마우스를 카드 위에 올리면 세 번째 카드처럼 카드의 뒷면을 보여준다. 카드의 뒷면에는 (판매자가 등록한) 상품의 간단한 설명과 가격, 전화번호, 주소를 볼 수 있다. 처음에는 이런 CSS 코드를 작성해본 적이 없어서 어려웠으나, 참고 코드를 보며 공부할 겸 완성하였다. 간단히 말하자면, 이 부분은 transform 등을 적절히 이용해 구현하였다.

 

3-4. 상세검색

상세검색 할 카테고리 선택
휴토피아 검색 결과
'가격' 선택 후, 150000을 검색한 결과

상세 검색을 위한 태그(카테고리)를 선택한 후 검색을 할 수 있다. 태그의 기본 값은 이름이며, 이름, 위치, 인원수, 가격을 선택하여 검색할 수 있다. 이 때, 인원수와 가격을 선택한 경우, 오직 숫자만 검색할 수 있게 코드를 작성하였다.

 <Input
          type={
            searchCategory === "productName" || searchCategory === "location"
              ? "text"
              : "number"
          }
          placeholder="Search..."
          ... />

그리고 이름, 위치, 인원수의 경우는 딱 입력한 값만 나오게 하였지만, 가격의 경우 입력한 가격의 90%에서 110%에 포함되는 가격에 해당하는 상품들을 보여주었다. 하지만, 이를 서비스 이용자가 알 수 있는 방법이 없기에, 이 부분을 어딘가에 실어야 할 것이다.

export const checkPrice = (productPrice, requiredPrice) => {
  return (
    productPrice * 0.9 <= requiredPrice && requiredPrice <= productPrice * 1.1
  );
};

 

3-5. 상품 상세 페이지&리뷰

상품 상세 페이지
위의 이미지에서 제대로 캡쳐되지 않은 부분
날짜를 선택하는 부분

이번 프로젝트를 진행하면서 가장 에러가 많이 발생한 곳이 아닌가 싶은 부분이다. 이 부분에서 크게 주목할만한 부분은 지도와 날짜 선택이라고 생각한다. 이 부분을 구현한 것은 내가 아니라 다른 팀원이기에, 구현 과정에서 있었던 일을 자세히는 알지 못하지만, 지도의 설정부터 CSS까지 정말 다사다난했다는 것은 알 수 있었다. 개인적으로 이 페이지에서 신경쓴 부분이 있다. 만약 특정 일자에 이미 예약이 되어 있다면, 그 날짜는 예약을 할 수 없게 했다. 처음에는 백엔드 분들께 이를 구현하고 싶은데, 모든 예약 정보와 하나씩 대조하는 방식으로도 구현이 가능하니 시간적으로 여유가 있다면 API를 만들어 줄 것을 요청했는데, 손쉽게 만들어 주셨었다. 물론, 이미 예약이 된 날짜는 달력에서 선택을 할 수 없게 하는 것이 훨씬 좋겠지만, 이 부분을 정말 이 글을 작성하며 깨달았다.

 그리고 리뷰는 한 사람이 하나의 리뷰만 작성할 수 있게 하였다. 한 상품에 한 사람이 여러 개의 리뷰를 쓰는 것은 상식적으로 올바른 상황이 아니기 때문이다. 또한 리뷰의 수정과 삭제 역시 가능하게 하였다. 그리고 리뷰를 작성한 사람의 이름은 보이지 않게 하였다.

 

3-6. 예약(결제) 페이지

예약 페이지
카카오페이 결제

바로 위에서 날짜를 선택한 후, 예약하기를 누르면 넘어가는 페이지이다. 이 부분은 다른 분이 담당한 곳이라 자세히는 모르기에 개인적으로 구현해볼 예정이다.

 

3-7. 마이 페이지

마이페이지
회원정보 수정/탈퇴
예약 정보 확인
사업자번호와 등록 날짜 입력/수정

 이 서비스에서 회원은 일반유저, 판매자, 어드민으로 나눌 수 있다. 만약 판매자 등록이 되지 않은 사람이라면 아래의 '판매 등록하러 가기'는 보이지 않게 하였다. 그리고 현재 이미지에서는 예약을 취소한 상황이라 '취소가 완료된 에약입니다.'라고 나오지만, 예약을 취소하지 않았다면 취소하기 버튼과 수정하기 버튼이 보인다. 그리고 사업자번호와 등록날짜 역시 정규표현식을 이용해 검사하였다.

// 판매자일 경우 '판매 등록하러 가기'가 보인다.
{userState.userInfo.roles.includes("SELLER") && (
            <SellArea>
              <SellMent isDark={isDark}>
                판매등록을 원하신다면 아래 링크를 눌러주세요👇🏻
              </SellMent>
              <SellLink isDark={isDark}>
                <span onClick={() => navigate("/sell")}>
                  판매 등록하러 가기↪️
                </span>
              </SellLink>
            </SellArea>
          )}
          
// 사업자 등록 번호의 패턴이 유효한지 판별하는 함수입니다.
export const validBusinessNumber = (code) => {
  const codePattern = /^\d{3}-\d{2}-\d{5}$/;

  return codePattern.test(code);
};

// 사업자 등록 일자의 패턴이 유효한지 판별하는 함수입니다.
export const validBusinessDate = (date) => {
  const codePattern = /^\d{4}-\d{2}-\d{2}$/;

  return codePattern.test(date);
};

 

3-8. 판매 등록 페이지

판매등록페이지

 판매 등록을 위해서는 상품(캠핑장)의 이미지, 이름, 주소, 위치, 가격, 취소기한, 수용인원, 간단한 소개문이 필요하다. 이 부분을 구현하는 데에 있어 고민을 한 부분이 두 가지가 있다. 우선 이미지이다. FormData를 이용하여 이미지를 보낼 수 있다는 것은 알았지만 이미지와 다른 정보로 구성된 객체를 같이 보내는 방법에 대해 고민했었다. 하지만 이 부분은 같은 FormData 객체에 싣는 것으로 해결하였다. 그리고 위도/경도 문제였다. 상품 상세 페이지에서 캠핑장의 위치를 지도를 통해 보여주는데 이를 위해서는 위도와 경도가 필요했다. 하지만 상품 정보를 입력할 때 위도와 경도를 일일이 입력받는 것은 이상하다고 생각했기에 고민했다. 하지만 백엔드 분들이 주소를 입력하면 알아서 위도와 경도 값을 설정되게 해주었다.

const postProduct = async (data) => {
 	// ... 이전 생략
    
    const jsonData = {
      productName,
      address,
      location,
      content,
      capacity: +capacity,
      cancellationDeadline: cancellationDeadline,
      productPrice,
      productPhone: "010-1111-1111",
      memberId: userState.userInfo.memberId,
    };

    const formData = new FormData();
    formData.append("images", image);
    formData.append("jsonData", JSON.stringify(jsonData));

    const success = await handlePostCampground(formData, userState.userInfo);

   	// ...이후 생략
  };

 

 

3-9. AI와 채팅

AI 채팅

 오른쪽 아래의 상담자 아이콘을 누를 경우, 위와 같이 작은 채팅창이 나온다. 여기서 AI와의 채팅을 진행할 수 있다. 원래는 상담자나 판매자와의 채팅을 구현하려 했다. 그리고 이를 위해 웹소켓에 대해 조금 공부도 했었다. 하지만 이를 구현하기 위해서는 결국 백엔드 쪽의 업무가 늘어날 것이라 생각해 AI와의 채팅으로 계획을 변경하였다. AI와의 채팅을 구현하는 것은 크게 어렵지 않았다. OpenAI의 API를 이용하여 구현하였다. 그리고 여기서 특별히 신경써야 할 부분은 feature 값 설정이었다. feature의 값은 0부터 1 사이인데, 값이 작으면 작을수록 답변이 보수적이고 일관성있고 값이 클수록 답변이 창의적이다. 그래서 이용자들에게 더 다양한 경험을 선사하기 위해 feature의 값을 1로 설정하였다.

const handlePostChat = async (event) => {
	// ... 이전 생략

    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [{ role: "user", content: `${chat}` }],
        temperature: 1,
      }),
    });
    const json = await response.json();
    const answer = json.choices[0].message.content; // 채팅창에 보여줄 답변
	
    // ... 이하 생략
  };

 

3-10. 다크모드

다크모드 적용 예시

const Container = styled.main`
	// ...
  background-color: ${(props) =>
    props.isDark ? "var(--black-700)" : "var(--white-50)"};
	// ...
`;

const isDark = useSelector((state) => state.modeReducer);

 다크모드는 리덕스를 이용하여 원하는 곳이라면 어디든지 현재 모드를 확인할 수 있고, 이를 색깔 선택이 이용할 수 있게 해 구현하였다. 만약 isDark가 true라면 현재 다크모드이고 false라면 라이트모드이다. 그리고 모드 변경은 왼쪽 위의 토글을 통해 할 수 있다.