Skip to content

Latest commit

ย 

History

History
542 lines (433 loc) ยท 18.8 KB

README.md

File metadata and controls

542 lines (433 loc) ยท 18.8 KB

๐ŸŽ ์„œ๋น„์Šค ์†Œ๊ฐœ

์นด์นด์˜คํ†ก์œผ๋กœ ๊ธฐํ”„ํ‹ฐ์ฝ˜ ์„ ๋ฌผ ๋ฐ›๊ธฐ, ๋„ˆ๋ฌด ์ง„๋ถ€ํ•œ ์ƒ์ผ์ด์ง€ ์•Š๋‚˜์š”? 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 };
};