Skip to content

🎁 λ‚˜μ™€ 친ꡬ λͺ¨λ‘κ°€ ν–‰λ³΅ν•œ 생일선물 νŽ€λ”© ν”Œλž«νΌ

Notifications You must be signed in to change notification settings

HAB-DAY/Habday_Web

Folders and files

NameName
Last commit message
Last commit date

Latest commit

c073e01 Β· Oct 4, 2023

History

88 Commits
Sep 4, 2023
Aug 12, 2023
Sep 16, 2023
Sep 4, 2023
Oct 4, 2023
Sep 11, 2023
Sep 1, 2023
Jul 7, 2023
Sep 1, 2023
Jul 14, 2023
Jun 28, 2023
Jun 26, 2023
Jun 28, 2023
Sep 5, 2023
Aug 1, 2023
Jul 27, 2023
Jul 15, 2023
Jun 28, 2023
Jul 27, 2023

Repository files navigation

🎁 μ„œλΉ„μŠ€ μ†Œκ°œ

μΉ΄μΉ΄μ˜€ν†‘μœΌλ‘œ κΈ°ν”„ν‹°μ½˜ μ„ λ¬Ό λ°›κΈ°, λ„ˆλ¬΄ μ§„λΆ€ν•œ 생일이지 μ•Šλ‚˜μš”? HABDAYλ₯Ό μ΄μš©ν•΄ μΉœκ΅¬λ“€μ—κ²Œ μ„ λ¬Ό νŽ€λ”©μ„ λ°›μ•„λ³΄μ„Έμš”!

HABDAYλŠ” μΉœκ΅¬λ“€κ³Ό ν•¨κ»˜ν•˜λŠ” μ„ λ¬Ό νŽ€λ”© ν”Œλž«νΌμž…λ‹ˆλ‹€.
μžμ‹ μ΄ μ›ν•˜λŠ” 선물을 μΉœκ΅¬λ“€μ—κ²Œ νŽ€λ”©μ„ λ°›κ³ , κ·Έλ™μ•ˆ κ°–κ³  μ‹Άμ—ˆλ˜ κ³ κ°€μ˜ 선물을 ꡬ맀할 수 μžˆμŠ΅λ‹ˆλ‹€.
뿐만 μ•„λ‹ˆλΌ, μΉœκ΅¬λ“€μ˜ λ„μ›€μœΌλ‘œ κΏˆμ„ μ‹€ν˜„ν•  μˆ˜λ„ μžˆλŠ” ν˜μ‹ μ μΈ ν”Œλž«νΌμž…λ‹ˆλ‹€.


πŸ›  μ‚¬μš©κΈ°μˆ  및 라이브러리

Next.js

  • React 기반의 μ›Ή 개발 ν”„λ ˆμž„μ›Œν¬
  • 검색엔진 μ΅œμ ν™”(SEO)와 μ„œλ²„μ‚¬μ΄λ“œ λ Œλ”λ§(SSR)의 μž₯점을 가지고 있음
  • Routing의 νŽΈμ˜μ„±μ΄ μ„œλΉ„μŠ€ νŠΉμ§•κ³Ό 잘 맞물리기 λ•Œλ¬Έμ— μ‚¬μš©

Typescript

  • Javascript에 νƒ€μž…μ΄ μΆ”κ°€λœ 정적 νƒ€μž… μ–Έμ–΄
  • complie λ‹¨κ³„μ—μ„œ μ—λŸ¬λ₯Ό λ°œκ²¬ν•΄λ‚Ό 수 μžˆμ–΄ 효율적인 개발 κ°€λŠ₯

React-query

  • μ„œλ²„ μƒνƒœκ΄€λ¦¬λ₯Ό μœ„ν•œ 라이브러리

Recoil

  • μ „μ—­ ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœκ΄€λ¦¬λ₯Ό μœ„ν•œ 라이브러리

Styled-components

  • 동적 μŠ€νƒ€μΌλ§μ„ μš©μ΄ν•˜κ²Œ ν•΄μ£ΌλŠ” μŠ€νƒ€μΌλ§ 라이브러리

Axios

  • HTTP μš”μ²­μ„ μš©μ΄ν•˜κ²Œ ν•΄μ£ΌλŠ” Promise 기반 라이브러리

πŸ“Œ κΈ°λŠ₯ 및 λ·° μ„€λͺ…

둜그인 뷰

  • μ°Έμ—¬μžκ°€ μƒμ„±μžκ°€ κ³΅μœ ν•œ 링크둜 μ§„μž…ν•˜κ²Œ 되면, 둜그인 화면을 보여쀀닀.
  • λ„€μ΄λ²„λ‘œ μ‹œμž‘ν•˜κΈ° λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄, λ„€μ΄λ²„λ‘œκ·ΈμΈ 링크둜 μ ‘μ†ν•œλ‹€.
  • μ°Έμ—¬μžκ°€ 넀이버 아이디와 λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜μ—¬ λ‘œκ·ΈμΈμ— μ„±κ³΅ν•˜λ©΄ μΈκ°€μ½”λ“œλ₯Ό λ°œκΈ‰λ°›λŠ”λ‹€.
  • λ°œκΈ‰λ°›μ€ μΈκ°€μ½”λ“œλ₯Ό μ„œλ²„μ— 전달해, μ•‘μ„ΈμŠ€ 토큰을 λ°œκΈ‰ν•œλ‹€.
  • λ°œκΈ‰λ°›μ€ μ•‘μ„ΈμŠ€ 토큰은 μ•žμœΌλ‘œμ˜ μ„œλ²„ μš”μ²­ μ‹œ headers에 λ„£μ–΄ μ‚¬μš©μž 식별에 μ‚¬μš©λœλ‹€.
  • λ§Œμ•½ 졜초둜 λ‘œκ·ΈμΈν•œ μ‚¬μš©μžμ΄λ©΄, μΆ”κ°€ 정보λ₯Ό μž…λ ₯ν•˜μ—¬ κ°€μž…μ„ μ™„λ£Œν•œλ‹€.

νŽ€λ”© 상세보기 λ·°

  • λ‘œκ·ΈμΈμ— μ„±κ³΅ν•˜λ©΄ νŽ€λ”© 상세보기 뷰둜 μ§„μž…ν•˜λ©°, μƒμ„±μž 이름, νŽ€λ”© 이름, νŽ€λ”© 사진, λͺ¨μΈ κΈˆμ•‘μ΄ ν‘œμ‹œλœλ‹€.
  • νŽ€λ”©μ— μ°Έμ—¬ν• λž˜μš” λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ νŽ€λ”© μ°Έμ—¬λ₯Ό μœ„ν•œ 정보 μž…λ ₯ 뷰둜 μ΄λ™ν•œλ‹€.

νŽ€λ”© μ°Έμ—¬ λ·°

  • νŽ€λ”© μ°Έμ—¬μžμ˜ 이름, νŽ€λ”©ν•  κΈˆμ•‘, 응원 λ©”μ‹œμ§€ 등을 μž…λ ₯ν•΄ νŽ€λ”©μ— μ°Έμ—¬ν•  수 μžˆλ‹€.
  • μž…λ ₯ν•œ μ •λ³΄λŠ” μΆ”ν›„ νŽ€λ”© μƒμ„±μžμ—κ²Œ μ „λ‹¬λœλ‹€.
  • κ²°μ œμˆ˜λ‹¨μ€ 이전에 μž…λ ₯ν–ˆλ˜ μΉ΄λ“œκ°€ 있으면 μ„ νƒν•΄μ„œ κ²°μ œν•  수 μžˆλ‹€.
  • μ„ λ¬Όλœ κΈˆμ•‘μ΄ νŽ€λ”© μƒμ„±μžμ˜ 선택에 따라 λ‹€λ₯Έ μƒν’ˆ ꡬ맀에 쓰일 수 μžˆμœΌλ―€λ‘œ, ν•΄λ‹Ή 사항에 λ™μ˜ν•΄μ•Ό μ΅œμ’… κ²°μ œκ°€ κ°€λŠ₯ν•˜λ‹€.

μΉ΄λ“œ μΆ”κ°€ λ·°

  • 만일 아직 κ²°μ œμˆ˜λ‹¨μ„ μž…λ ₯ν•˜μ§€ μ•Šμ•˜κ±°λ‚˜ μƒˆλ‘œμš΄ 결제 μˆ˜λ‹¨μ„ μž…λ ₯ν•˜κ³  μ‹Άλ‹€λ©΄ μΉ΄λ“œμ •λ³΄λ₯Ό μž…λ ₯ν•  수 μžˆλ‹€.
  • μΉ΄λ“œ 정보λ₯Ό μ˜¬λ°”λ₯΄κ²Œ μž…λ ₯ν•˜μ§€ μ•Šμ„ 경우, μ•Œλ¦Όμ°½μ΄ 뜨며 μ˜¬λ°”λ₯Έ 값을 μž…λ ₯ν•˜λ„λ‘ μœ λ„ν•œλ‹€.

νŽ€λ”©μ°Έμ—¬ μ™„λ£Œ λ·°

  • μ΅œμ’…μ μœΌλ‘œ μ°Έμ—¬κ°€ μ™„λ£Œλ˜μ—ˆμŒμ„ μ•Œλ¦¬λŠ” 화면이닀.
  • μ°Έμ—¬λ‚΄μ—­ λ³΄λŸ¬κ°€κΈ°λ₯Ό 클릭해 μ°Έμ—¬ν•œ νŽ€λ”© 리슀트λ₯Ό μ‘°νšŒν•  수 μžˆλ‹€.

Untitled (33)

νŽ€λ”©μ°Έμ—¬ λͺ©λ‘ λ·°

  • μ°Έμ—¬ν–ˆλ˜ νŽ€λ”© 내역을 확인할 수 μžˆλŠ” 뷰이닀.
  • νŽ€λ”©μ„ 클릭해 μ°Έμ—¬ν–ˆλ˜ νŽ€λ”©μ„ μ·¨μ†Œν•  수 있으며, μ·¨μ†Œ 된 μ΄ν›„μ—λŠ” cancel μƒνƒœλ‘œ λ³€κ²½λœλ‹€.

νŽ€λ”© 인증 λ·°

  • νŽ€λ”© μƒμ„±μžκ°€ νŽ€λ”©μ΄ μ„±κ³΅ν•œ ν›„ 2μ£Ό μ΄λ‚΄λ‘œ 앱을 톡해 인증을 ν•˜λ©΄, 기쑴의 νŽ€λ”© url둜 μ§„μž…ν–ˆμ„ λ•Œ νŽ€λ”© λ·°κ°€ μ•„λ‹Œ 인증 λ·°κ°€ λœ¬λ‹€.
  • 인증 상세보기 λ·°μ—μ„œ μ‹€μ œλ‘œ 선물을 κ΅¬μž…ν–ˆλŠ”μ§€ 여뢀와 감사 λ©”μ‹œμ§€λ₯Ό 확인할 수 μžˆλ‹€.

πŸ—‚ 폴더 ꡬ쑰

πŸ“¦ 
β”œβ”€ .eslintrc.json
β”œβ”€ .gitignore
β”œβ”€ .prettierrc
β”œβ”€ README.md
β”œβ”€ api
β”œβ”€ assets
β”œβ”€ components
β”‚  β””─ common
β”‚     β”œβ”€ Greeting.tsx
β”‚     β”œβ”€ Layout.tsx
β”‚     β”œβ”€ Progress.tsx
β”‚     β””─ modal
β”œβ”€ hooks
β”œβ”€ pages
β”‚  β”œβ”€ _app.tsx
β”‚  β”œβ”€ _document.tsx
β”‚  β”œβ”€ card
β”‚  β”œβ”€ complet
β”‚  β”œβ”€ detai
β”‚  β”œβ”€ fun
β”‚  β”œβ”€ index.ts
β”‚  β”œβ”€ landing
β”‚  β”‚  β””─ [itemId].tsx  // Dynamic routing: 졜초 μ§„μž… νŽ˜μ΄μ§€
β”‚  β”œβ”€ list
β”‚  β”œβ”€ revie
β”‚  β””─ signu
β”œβ”€ public
β”œβ”€ states  // for atoms
β”œβ”€ styles  // for global styling
β”œβ”€ types   // for common types
β”œβ”€ util    // for constants
└─ yarn.lock

Β©generated by Project Tree Generator


πŸ“ μ½”λ“œ μ†Œκ°œ

  • μ»€μŠ€ν…€ν›…μ„ 컨트둀러 μ—­ν• λ‘œ λ‘” MVC νŒ¨ν„΄
  • μ»€μŠ€ν…€ν›…μ„ μ‚¬μš©ν•˜λ©΄ UI와 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ 뢄리할 수 μžˆμŠ΅λ‹ˆλ‹€.

React μ»€μŠ€ν…€ν›…(Custom Hook) μ΄λž€? React ν•¨μˆ˜ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μƒνƒœ 관리, 라이프사이클 κΈ°λŠ₯ 등을 μΆ”μƒν™”ν•˜μ—¬ μž¬μ‚¬μš© κ°€λŠ₯ν•œ λ‘œμ§μ„ κ΅¬ν˜„ν•˜κ³  κ³΅μœ ν•  수 있게 ν•΄μ£ΌλŠ” ν›…

νŽ€λ”© 상세보기

  • /landing/μ•„μ΄ν…œid둜 μ§„μž…ν•˜κ²Œ 되면, Landing μ»΄ν¬λ„ŒνŠΈλ₯Ό λ Œλ”λ§ν•©λ‹ˆλ‹€.
  • getServersizeProps λ©”μ†Œλ“œλ‘œ paramsλ₯Ό 받아와, μ„œλ²„μ— ν•΄λ‹Ή id의 νŽ€λ”©μƒμ„Έμ •λ³΄λ₯Ό μš”μ²­ν•©λ‹ˆλ‹€.
import React, { useEffect } from 'react';
import Layout from '../../components/common/Layout';
import { useFundDetail } from '../../hooks/fund/useFundDetail';
import { useRouter } from 'next/router';
import { useSetRecoilState } from 'recoil';
import { fundingIdState } from '../../states/atom';
import Greeting from '../../components/common/Greeting';

export interface ParamProps {
  params: ItemProps;
}

export interface ItemProps {
  itemId: string;
}

const STATUS = {
  PROGRESS: 'PROGRESS',
  FAILED: 'FAILED',
  SUCCESS: 'SUCCESS',
};

export default function Landing({ itemId }: ItemProps) {
  const router = useRouter();
  const { detail, isLoading, isError } = useFundDetail(parseInt(itemId));
  const setFundingId = useSetRecoilState(fundingIdState);

  const NAVER_AUTH_URL = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}&state=${process.env.NEXT_PUBLIC_LOGIN_STATE}&redirect_uri=${process.env.NEXT_PUBLIC_REDIRECT_URL}`;
  const onClickLogin = () => window.location.assign(NAVER_AUTH_URL);

  useEffect(() => {
    setFundingId(parseInt(itemId));
  }, [detail]);

  if (isLoading) {
    return <div>loading...</div>;
  }

  if (isError || detail?.status === STATUS.FAILED) {
    return <div>error! μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŽ€λ”©μž…λ‹ˆλ‹€</div>;
  }

  if (detail?.isConfirmation) {
    return (
      <Layout>
        <Greeting message="νŽ€λ”© 인증이 λ„μ°©ν–ˆμ–΄μš”!" isPing onClickIcon={onClickLogin} />
      </Layout>
    );
  }

  if (detail?.status === STATUS.SUCCESS) {
    return (
      <Layout link="참여이λ ₯ λ³΄λŸ¬κ°€κΈ°">
        <Greeting message="νŽ€λ”©μ— μ„±κ³΅ν–ˆμ–΄μš”, κ°μ‚¬ν•©λ‹ˆλ‹€!" />
      </Layout>
    );
  }

  return (
    <Layout isNaver buttons={['λ„€μ΄λ²„λ‘œ μ‹œμž‘ν•˜κΈ°']} link="HABDAYκ°€ μ²˜μŒμ΄μ„Έμš”?" onClickButton={onClickLogin}>
      <Greeting message={`${detail?.hostName}λ‹˜μ˜ νŽ€λ”©μ— μ°Έμ—¬ν•΄λ³΄μ„Έμš”!`} />
    </Layout>
  );
}

export async function getServerSideProps({ params }: ParamProps) {
  const itemId = params.itemId;
  return { props: { itemId } };
}
  • 자주 μ‚¬μš©ν•˜λŠ” λ₯Ό common/Layout으둜 μ„ μ–Έν•΄, 곡톡 μ»΄ν¬λ„ŒνŠΈν™” ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
interface LayoutProps {
  children: React.ReactNode;
  buttons?: string[];
  link?: string;
  onClickButton?: () => void;
  onClickLeftButton?: () => void;
  isNaver?: boolean;
}

export default function Layout(props: LayoutProps) {
  const { children, buttons, link, onClickButton, onClickLeftButton, isNaver } = props;
  return (
    <Styled.Root>
      <Styled.Main>{children}</Styled.Main>
      <Styled.Footer isButtons={buttons?.length === 2}>
        {buttons && buttons?.length == 2 && (
          <Styled.ButtonLeft onClick={onClickLeftButton}>{buttons[1]}</Styled.ButtonLeft>
        )}
        {buttons && buttons?.length >= 1 && (
          <Styled.Button isNaver={isNaver} onClick={onClickButton}>
            {isNaver && <Image alt="넀이버 둜고" src={NaverImg} height={42} width={42} />}
            {buttons[0]}
          </Styled.Button>
        )}
        {link && <Styled.Link>{link}</Styled.Link>}
      </Styled.Footer>
    </Styled.Root>
  );
}
  • μ»€μŠ€ν…€ν›… useFundDetail을 μ„ μ–Έν•΄ UI와 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ λΆ„λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • useFundDetail은 fetchFundDetail ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. axios 라이브러리λ₯Ό μ‚¬μš©ν•΄ λ”μš± 효율적인 REST API 톡신을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.
// useFundDetail.ts
import { useQuery } from 'react-query';
import { fetchFundDetail } from '../../api/fund';
import { useSetRecoilState } from 'recoil';
import { QUERY_KEY } from '..';

export const useFundDetail = (itemId: number) => {
  const { isLoading, isError, data } = useQuery([QUERY_KEY.fundDetail], () => fetchFundDetail(itemId));

  return { detail: data, isLoading, isError };
};

// fund.ts
import { client } from '.';
import { Response } from '../types';
import { DetailOutput } from '../types/responses/fund';

export const fetchFundDetail = async (itemId: number) => {
  const {
    data: { data },
  } = await client.get<Response<DetailOutput>>(`/funding/showFundingContent?itemId=${itemId}`);
  return data;
};

둜그인

  • νŽ€λ”© 상세보기 Detail λ·°μ—μ„œλŠ” getServersideProps둜 query param의 μΈκ°€μ½”λ“œλ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.
  • μΈκ°€μ½”λ“œλ₯Ό μ‚¬μš©ν•΄ useAccessToken을 ν˜ΈμΆœν•˜λ©΄, 자체 μ•‘μ„ΈμŠ€ 토큰을 λ°œκΈ‰ν•΄ recoil atom에 μ €μž₯ν•©λ‹ˆλ‹€.
interface codeProps {
  code: string;
}

export default function Detail({ code }: codeProps) {
  const router = useRouter();
  const itemId = useRecoilValue(fundingIdState);
  const { detail } = useFundDetail(itemId);
  const { accessToken, isLoading } = useAccessToken(code);
  // const signupStat = useRecoilValue(signupLogState);
  const { isRegister } = useIsRegister();

  useEffect(() => {
    if (code === undefined || isRegister === undefined) return;
    if (!isRegister) router.push('/signup');
    else if (detail?.isConfirmation) router.push('/review');
  }, [code, detail, accessToken, isRegister]);

  if (isLoading) return <div>λ‘œλ”©μ€‘...</div>;

  return (
    <Layout buttons={['νŽ€λ”©μ— μ°Έμ—¬ν• λž˜μš”']} onClickButton={() => router.push('/fund')}>
      <Styled.Titles>
        <Styled.Title>{detail?.hostName}λ‹˜μ€</Styled.Title>
        <Styled.BoldTitle>{detail?.fundingName}</Styled.BoldTitle>
        <Styled.Title>λ₯Ό(을) κ°–κ³ μ‹Άμ–΄ν•΄μš”</Styled.Title>
      </Styled.Titles>
      <Styled.Images>
        <Styled.ImageContainer>
          <Image
            src={detail?.fundingItemImg ?? AirpodImg}
            alt="νŽ€λ”©μ•„μ΄ν…œ 이미지"
            width={222}
            height={222}
            placeholder="blur"
            blurDataURL="asstes/default.svg"
            priority
          />
        </Styled.ImageContainer>
      </Styled.Images>
      <Styled.ProgressContainer>
        <Styled.ProgressTitle>ν˜„μž¬κΉŒμ§€ λͺ¨μΈ κΈˆμ•‘</Styled.ProgressTitle>
        <Styled.ProgressAmount>οΏ¦ {priceFormatter(detail?.totalPrice ?? 0)}</Styled.ProgressAmount>
        <Progress totalPrice={detail?.totalPrice ?? 0} goalPrice={detail?.goalPrice ?? 0} />
      </Styled.ProgressContainer>
    </Layout>
  );
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
  return { props: { code: context.query.code ?? '' } };
}
  • AxiosInterceptorμ—μ„œ λ°œκΈ‰λœ accessToken을 header에 λ„£μŠ΅λ‹ˆλ‹€.
export const BASE_URL = process.env.NEXT_PUBLIC_END;

const client = axios.create({
  baseURL: BASE_URL,
  headers: { 'Content-Type': 'application/json' },
});

function AxiosInterceptor({ children }: PropsWithChildren) {
  const router = useRouter();
  const accessToken = useRecoilValue(accessTokenState);

  const requestIntercept = client.interceptors.request.use((config) => {
    if (config.headers && !config.headers['accessToken']) {
      config.headers['accessToken'] = accessToken ? `${accessToken}` : '';

      return config;
    }

    return config;
  });

  const responseIntercept = client.interceptors.response.use(
    (config) => config,
    async (error) => {
      const config = error.config;
      console.log(error);
      if (error.response.status === 401) {
        alert('둜그인 ν›„ μ΄μš©ν•΄ μ£Όμ„Έμš”');
      }
      return Promise.reject(error);
    }
  );

  useEffect(() => {
    return () => {
      client.interceptors.request.eject(requestIntercept);
      client.interceptors.response.eject(responseIntercept);
    };
  }, [requestIntercept]);

  return <>{children}</>;
}

export { client, AxiosInterceptor };

νŽ€λ”© μ°Έμ—¬ν•˜κΈ°

  • /fund μ§„μž…μ‹œ νŽ€λ”©μ— μ°Έμ—¬ν•  수 μžˆλŠ” Fund μ»΄ν¬λ„ŒνŠΈλ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€.
  • λ‹€μ–‘ν•œ inputλ“€μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ useParticipantForm μ»€μŠ€ν…€ν›…μ„ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • usePaymentListλŠ” 기쑴에 등둝해둔 κ²°μ œμˆ˜λ‹¨ 정보λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.
export default function Fund() {
  const router = useRouter();

  const itemId = useRecoilValue(fundingIdState);
  const { detail } = useFundDetail(itemId);

  const { participant, setParticipantForm, submitPariticipant, toggleAgree } = useParticipantForm(async () => {
    router.push('/complete');
  });
  const { isError, isLoading, paymentList } = usePaymentList();

  useEffect(() => {
    paymentList.length && setParticipantForm({ paymentId: paymentList[0].paymentId });
  }, [paymentList]);

  return (
    <Layout buttons={['λ‹€μŒ']} onClickButton={submitPariticipant}>
      <Styled.Title>{detail?.hostName} λ‹˜μ—κ²Œ</Styled.Title>
      <Styled.Form>
        <Styled.Label>λ³΄λ‚΄λŠ” λΆ„ 성함</Styled.Label>
        <Styled.Input
          value={participant.name}
          id="buyer"
          type="text"
          onChange={(e) => setParticipantForm({ name: e.target.value })}
        />
      </Styled.Form>
      <Styled.Form>
        <Styled.Label>νŽ€λ”© κΈˆμ•‘</Styled.Label>
        <Progress
          goalPrice={detail?.goalPrice ?? 0}
          totalPrice={detail?.totalPrice ?? 0}
          isPing
          amount={participant.amount}
        />
        <Styled.Input
          id="amount"
          type="number"
          max={`${detail?.goalPrice ?? 0 - (detail?.totalPrice ?? 0)}`}
          placeholder={`μ΅œλŒ€ ${priceFormatter(detail?.goalPrice ?? 0 - (detail?.totalPrice ?? 0))}μ›κΉŒμ§€ κ°€λŠ₯ν•΄μš”`}
          onChange={(e) => setParticipantForm({ amount: parseInt(e.target.value) })}
        />
      </Styled.Form>
      <Styled.Form>
        <Styled.Label>응원 λ©”μ‹œμ§€</Styled.Label>
        <Styled.Textarea
          value={participant.message}
          onChange={(e) => setParticipantForm({ message: e.target.value })}
        />
        <Styled.Maxline>{participant.message.length || 0}/60</Styled.Maxline>
      </Styled.Form>
      <Styled.Form>
        <Styled.Label>
          μΉ΄λ“œ 결제
          <Styled.AddCardButton onClick={() => router.push('/card')}>μΉ΄λ“œ μΆ”κ°€</Styled.AddCardButton>
        </Styled.Label>
        {paymentList.length ? (
          <Styled.Select defaultValue={0}>
            {paymentList.map(({ paymentId, paymentName }, index) => (
              <option key={paymentId} onClick={() => setParticipantForm({ paymentId: paymentId })}>
                {paymentName}
              </option>
            ))}
          </Styled.Select>
        ) : (
          <Styled.Message>κ²°μ œμˆ˜λ‹¨μ„ μΆ”κ°€ν•΄μ£Όμ„Έμš”</Styled.Message>
        )}
        <Styled.Check>
          μ„ λ¬Όν•˜μ‹€ κΈˆμ•‘μ€ λͺ©μ κΈˆμ•‘ λ―Έλ‹¬μ„±μ‹œ λ‹€λ₯Έ μƒν’ˆκ΅¬λ§€μ—
          <br />
          μ‚¬μš©λ  수 μžˆμŠ΅λ‹ˆλ‹€. λ™μ˜ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
          <input type="checkbox" onClick={toggleAgree} />
        </Styled.Check>
      </Styled.Form>
    </Layout>
  );
}
  • μ‚¬μš©μž μž…λ ₯을 μ²˜λ¦¬ν•˜λŠ” useParticipantForm은 recoil atom을 μ‚¬μš©ν•˜μ—¬ μ‚¬μš©μžμ˜ μž…λ ₯값이 μœ μ§€λ˜λ„λ‘ ν•©λ‹ˆλ‹€.
  • submitPariticipant ν•¨μˆ˜μ—μ„œ μ—λŸ¬ν•Έλ“€λ§μ„ μˆ˜ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • react query 라이브러리λ₯Ό μ‚¬μš©ν•˜μ—¬, 데이터 패칭 μ„±κ³΅μ‹œμ— 기쑴에 있던 fundDetail 데이터λ₯Ό μΊμ‹±ν•˜κ³  성곡 후에 ν•΄μ•Όν•  일을 μˆ˜ν–‰ν•¨μœΌλ‘œμ¨ 데이터 정합성을 보μž₯ν•˜κ³  μ„œλ²„ μƒνƒœκ΄€λ¦¬λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.
export const useParticipateMutation = (onSuccessMutation: () => void) => {
  const participant = useRecoilValue(participantState);
  const queryClient = useQueryClient();

  return useMutation(() => postParticipate(participant), {
    onSuccess() {
      queryClient.invalidateQueries([QUERY_KEY.fundDetail]);
      onSuccessMutation();
    },
    onError({ response }: ParticipateErrorResponse) {
      alert(response.data.msg);
    },
  });
};

export const useParticipantForm = (onSuccessMutation: () => void) => {
  const [participant, setParticipant] = useRecoilState(participantSelector);
  const [isAgree, setIsAgree] = useState<boolean>(false);
  const participantMutation = useParticipateMutation(onSuccessMutation);

  const setParticipantForm = (input: Partial<ParticipateInput>) => {
    setParticipant({ ...participant, ...input });
  };

  const submitPariticipant = () => {
    if (participant.paymentId === -99) alert('κ²°μ œμˆ˜λ‹¨μ„ μ„ νƒν•΄μ£Όμ„Έμš”');
    else if (participant.amount < 101) alert('μ΅œμ†Œ κΈˆμ•‘μ€ 101μ›μž…λ‹ˆλ‹€');
    else if (!participant.name.length) alert('성함을 μž…λ ₯ν•΄μ£Όμ„Έμš”');
    else if (!isAgree) alert('약관에 λ™μ˜ν•΄μ£Όμ„Έμš”');
    else participantMutation.mutate();
  };

  const toggleAgree = () => setIsAgree((prev) => !prev);

  return { participant, setParticipantForm, submitPariticipant, toggleAgree };
};

About

🎁 λ‚˜μ™€ 친ꡬ λͺ¨λ‘κ°€ ν–‰λ³΅ν•œ 생일선물 νŽ€λ”© ν”Œλž«νΌ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published