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

Β 

History

88 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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