패키지 그룹 여행 서비스 (Feat. Next, TypeScript)

2023. 5. 3. 18:55기록/Projects

    목차

배포 링크 : go-together
원본 저장소: Github

마지막 UXUI + 프론트엔드 + 백엔드 + 기업 협업 프로젝트는...!!

바로 시니어층 패키지 그룹 여행 서비스 고투게더 리뉴얼!

 

이렇게 많은 부분을 협업하는 건 처음이라 걱정이 한바가지였다....ㅠㅠㅠㅠ

 

UXUI 분들께서 피그마로 화면을 만들어주신 후에....

백엔드 분들께서 api 를 만들어주신 후에.....

프론트엔드에서 웹 페이지를 제대로 만들 수 있기 때문에

약 한 달이라는 기간이 너무 빠듯했다... 🥹

 

(거의 3주가 넘어서야 피그마가 나왔고... 심지어 안나온 화면도 있어서 프론트엔드에서 만들어야 했다....ㅎㅎㅎㅎ허허허허허ㅓ)

 

또르륵 ...

 

그래도 팀원들끼리 열심히

..........

ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

 

으!!!!!!!!!!!!!!!!!!!!!!!!!!

쌰!!!!!!!!!!!!!!!!!

하면서 프로젝트를 완성할 수 있었다!!!!!!

 

이번에도 내가 팀장을 맡게 되어서.........(팀장 3연속...)

프로젝트 셋팅을 한 후에 개발을 진행했다.

 

이번에는 Next.js 를 써보는 것이 어떤 지 팀원분들과 상의해서

Next.js 로 셋팅하게 되었다!

 

api 호출 url / 라우팅 url 상수 파일들과

axios / eslint / prettier 등의 설치는 금융 상품 추천 서비스와 동일하게 진행하였고

새롭게 초기셋팅한 부분에 대해서 작성해 보려 한다! 화이팅!!!!!!

 

중간 중간 구현 코드들에는 생략된 부분도 있기 때문에, 저장소로 이동해서 보시는걸 추천합니다!


Next.js + typeScript 프로젝트 초기화

npx create-next-app --typescript .

명령어 실행 후 아래처럼 설정해 주기!

 


husky / lint-staged 설치

husky

git commit 또는 git push와 같은 git 이벤트가 일어나기 전

원하는 스크립트를 실행하기 위해서 git 이벤트 사이에 갈고리(hook)를 걸어주기 위해 사용하는 라이브러리!

 

lint-staged

git add로 커밋 대상이 된 상태인 stage 상태의 git 파일에 대해서

lint와 설정해둔 명령어를 실행해주는 라이브러리!

 

mrm

오픈소스 프로젝트의 환경 설정을 동기화 하기 위한 도구

lint-staged와 husky를 간편하게 설정 가능!

npx mrm lint-staged

 

package.json

"lint-staged": { 
  "*.{ts,tsx}": [ 
    "prettier --write", 
    "eslint --fix" 
  ] 
}

 

.husky 폴더에 있는 pre-commit 파일에 있는 코드

git commit을 하기전에 npx lint-staged 명령어를 실행하게 된다!

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

여행 유형 테스트

총 4단계의 질문으로 이루어져 있고, 다음 버튼을 누르면 다음 단계로 이동한다.

뒤로가기 버튼 클릭 시 이전 단계로 돌아가고

완료 버튼 클릭 시 해당 사용자의 타입이 저장되며 추천 상품이 출력된다.

 

MBTI를 이용해서 기획되었고, mui 의 스텝퍼를 사용해 구현하였다.

처음에는 질문과 선택지들을 어떻게 코드로 구현해야 할 지 막막했는데...

배열을 사용하여 각 단계의 질문과 선택지들을 같은 인덱스에 저장해서 사용하는 방법으로 구현했다!

const Servey = () => {
  const [product, setProduct] = useState<IProduct[]>([]);
  const [isSelected, setIsSelected] = useState({ 1: false, 2: false });
  const [activeStep, setActiveStep] = useState(0);
  const steps = [
    `이어지는 네 번의 선택이\n즐거운 여행의 동반자를 찾는데 도움이 됩니다.`,
    `두 번째 선택입니다.\n이번 여행의 목적과 가까운 것을 골라보세요.`,
    `절반을 지났네요.\n이번엔 소비패턴 또는 이번 여행의 경비와 연관이 있을까요?`,
    `먹는 것도 중요하죠!\n마지막 선택은 바로 즐거운 식시시간 관련입니다.`,
  ];
  const [answer, setAnswer] = useState<string[]>([]);
  const [question, setQuestion] = useState([
    {
      1: { text: '나는 내향형이다.', icon: <UserMinus />, value: 'I' },
      2: { text: '나는 외향형이다.', icon: <UserPlus />, value: 'E' },
    },
    {
      1: { text: '휴식과 여유있는 여행이 좋다.', icon: <Beach />, value: 'N' },
      2: { text: '다양한 활동과 체험이 좋다.', icon: <Diving />, value: 'S' },
    },
    {
      1: { text: '모처럼 여행이니 맘 먹고 써도 좋다.', icon: <Flex />, value: 'F' },
      2: { text: '적당한 비용으로도 충분히 즐길 수 있다.', icon: <Saving />, value: 'T' },
    },
    {
      1: { text: '하루 한 끼 한식이 필요해!', icon: <Rice />, value: 'J' },
      2: { text: '여행인데 현지인 입맛을 따라야지!', icon: <Dish />, value: 'P' },
    },
  ]);
  
  ...

  return (
    <Container>
      <PageTitle title="여행 유형 테스트" />
     
      ...
     
        <CardContainer>
          <QuestionText>{steps[activeStep]}</QuestionText>
          <CardWrap>
            <SurveyCard isSelected={isSelected[1]} onClick={handleClickFirst}>
              {question[activeStep][1]['icon']}
              <SurveyText>{question[activeStep][1]['text']}</SurveyText>
            </SurveyCard>
            <SurveyCard isSelected={isSelected[2]} onClick={handleClickSecond}>
              {question[activeStep][2]['icon']}
              <SurveyText>{question[activeStep][2]['text']}</SurveyText>
            </SurveyCard>
          </CardWrap>
          <Box sx={{ width: '100%', display: 'flex', justifyContent: 'space-between' }}>
            <Button disabled={activeStep === 0} onClick={handleBack}>
              뒤로가기
            </Button>
            <Button
              onClick={() =>
                handleNext(
                  isSelected['1']
                    ? question[activeStep][1]['value']
                    : question[activeStep][2]['value'],
                )
              }
            >
              {activeStep === steps.length - 1 ? '완료' : '다음'}
            </Button>
          </Box>
        </CardContainer>
      }
    </Container>
  );
};

 

다음 버튼을 누르면 답변 배열에 선택한 값을 이어붙인 후

현재 선택한 값을 모두 false 로 초기화하고

단계에 + 1 을 해주어 다음 단계로 넘어간다.

만약 선택한 값이 없을 경우 답변을 선택하라는 모달 창을 출력한다.

const handleNext = async (value: string) => {
  if (!isSelected[1] && !isSelected[2]) {
    return dispatch(
      setModal({
        isOpen: true,
        onClickOk: () => dispatch(setModal({ isOpen: false })),
        text: MESSAGES.SURVEY.CHECK_ANSWER,
      }),
    );
  }
  
  setAnswer((prev) => prev.concat(value));
  setIsSelected({ 1: false, 2: false });
  setActiveStep((prevActiveStep) => prevActiveStep + 1);
};

 

뒤로가기 버튼을 누르면 답변 배열에서 마지막 요소를 제거한 후

현재 선택한 값을 초기화한 후 이전 단계로 이동한다.

const handleBack = () => {
  setAnswer((prev) => prev.slice(0, -1));
  setIsSelected({ 1: false, 2: false });
  setActiveStep((prevActiveStep) => prevActiveStep - 1);
};

 

첫 번째 선택지를 누르면 현재 선택 값 객체에서 1을 키로 가진 값을 true 로 변경한다.

두 번째 선택지를 누르면 2를 키로 가진 값을 true 로 변경한다.

const handleClickFirst = () => {
  setIsSelected({ 1: true, 2: false });
};

const handleClickSecond = () => {
  setIsSelected({ 1: false, 2: true });
};

 

단계가 변경될 때, 현재 단계가 마지막 단계일 경우

useEffect(() => {
  if (activeStep === steps.length) {
    handleUserType();   
  }
}, [activeStep]);

 

답변 배열들의 요소를 이어붙여 문자열로 만들고

사용자의 여행 유형을 저장한 후, 유형별 추천 상품 데이터를 가지고 와서 product 상태값으로 넣어준다.

const handleUserType = async () => {
  try {
    const type = answer.join('');
    const reqData = { userType: type };
    await patchUserType(reqData);

    const data = await getProductByType();
    setProduct(data);
  } catch {
    return dispatch(
      setModal({
        isOpen: true,
        text: MESSAGES.SURVEY.ERROR_GET_PRODUCT,
        onClickOk: () => dispatch(setModal({ isOpen: false })),
      }),
    );
  }
};

 

다시하기 버튼을 클릭하면

답변 배열을 비워주고 현재 선택 값을 초기화한 후

현재 단계를 0번째로 넣어준다.

const handleReset = () => {
  setAnswer([]);
  setIsSelected({ 1: false, 2: false });
  setActiveStep(0);
};

상품 검색

검색어 입력 후 엔터키를 누르거나 검색 버튼을 클릭하면 해당 검색어에 해당하는 상품들이 출력된다.

추천 검색어를 누르면 해당 검색어로 바로 상품이 검색된다.

 

검색을 할 때 마다 최근 검색어가 저장되고 (localStorage 이용)

전체 삭제를 누르면 최근 검색어가 전체 삭제된다. 개별 삭제도 가능하다.

 

자동저장 끄기 버튼을 클릭하면 검색어가 저장되지 않는다.

자동저장 켜기 버튼을 클릭하면 저장되어 있던 검색어가 출력되고 앞으로 검색하는 검색어가 저장된다.


상품 목록

카테고리를 클릭하면 해당 카테고리에 해당하는 상품들이 출력된다.

출발일자, 인원, 정렬순서를 지정할 수 있고 초기화 버튼을 클릭하면 모든 조건이 초기화된다.

페이징을 적용하여 숫자 버튼 클릭 시 해당 페이지의 상품 목록들을 가져와서 출력한다.


장바구니

 

인원과 싱글룸을 변경할 때 마다 총 예약 금액이 바로 반영되어 출력된다.

여행일자, 인원, 싱글룸을 변경한 후 변경 저장 버튼을 누르면 해당 데이터가 저장된다.

전체 선택 버튼을 클릭하고 선택 삭제 버튼을 누르면 전체 삭제가 가능하고

x 버튼을 클릭해 단일 삭제도 가능하다.

 

바로 예약 버튼을 누르면 해당 상품을 예약하는 페이지로 이동하고,

예약하기 버튼을 누르면 장바구니에 있는 모든 상품 데이터를 가지고 예약 페이지로 이동한다.


상품 카테고리 관리

짜잔 전체적인 관리자 페이지는 이렇게 생겼다...!

관리자 페이지는 피그마가 안나와서.... 내가 mui 로 기본적인 틀만 만들었다..허허허

 

왼쪽에는 상품 카테고리 목록을 1, 2, 3 뎁스별로 색상을 다르게 보여준다.

카테고리 이름을 누르면 오른쪽에서 카테고리명 변경이 가능하다.

 

신규 등록 버튼을 누르고 대분류/중분류를 선택하여 새로운 카테고리 등록이 가능하다.

삭제 시에는 하위 카테고리가 없는 경우에만 삭제가 가능하다.


상품 관리

대망의 상품 관리... ⭐️

가뜩이나 상품 데이터가 많은데... 카테고리와 옵션때문에 애를 많이 썼다 ㅠㅠㅠㅠ

그래도 구현을 하고 나니 세상 뿌듯하다!!!

 

[상품 목록]

등록된 상품 목록들을 페이징해서 보여준다.

상품 클릭 시 상세 페이지로 이동하여 상세 정보를 보여준다.

 

[상품 등록]

상품 목록에서 등록 버튼을 클릭하면 등록 페이지로 이동한다.

카테고리를 대분류/중분류/소분류로 선택할 수 있다.

하위 카테고리가 없는, 가장 마지막 뎁스의 카테고리를 선택할 경우에만 해당 카테고리가 선택된다.

 

상품 옵션의 추가 버튼을 누르면 새로운 옵션 input 이 나타난다.

추가된 옵션 오른쪽의 삭제 버튼을 누르면 해당 옵션이 삭제된다.

 

[상품 수정]

상세 페이지에서 수정 버튼을 클릭하면 수정 페이지로 이동한다.

아래의 수정 완료 버튼을 클릭하면 변경된 데이터가 저장된다.

 

상품 옵션을 작성하고 추가 버튼을 클릭하면 바로 옵션이 추가된다.

옵션에서 저장/삭제 버튼을 클릭하면 해당 옵션의 저장 및 삭제가 바로 가능하다.

 

[상품 삭제]

상품 삭제 시에는 데이터가 숨김 처리된다.


추천 상품 관리

추천 상품 목록에서 상품을 클릭하면 상세 페이지로 이동한다.

등록/수정/삭제가 가능하며 우선순위가 높은 순서대로 데이터를 받아와서 보여준다.


이렇게 마지막 협업 프로젝트가 끝이났다!!!!!!!!!!!!!!!!!!!!!!

수료식도 하고... 7개월간의 프론트엔드 과정이 끝을 맺었다 😃

 

드디어 나도 백엔드가 아닌 프론트엔드로 !!!!!!!!!!!!!

 

그동안 다들 너무너무 수고하셨고 감사했습니다...!!

이제부터가 시작이당... 화이팅 합쉬다 !!!!!!!!! ❤️❤️❤️