์นด์นด์คํก์ผ๋ก ๊ธฐํํฐ์ฝ ์ ๋ฌผ ๋ฐ๊ธฐ, ๋๋ฌด ์ง๋ถํ ์์ผ์ด์ง ์๋์? HABDAY๋ฅผ ์ด์ฉํด ์น๊ตฌ๋ค์๊ฒ ์ ๋ฌผ ํ๋ฉ์ ๋ฐ์๋ณด์ธ์!
HABDAY๋ ์น๊ตฌ๋ค๊ณผ ํจ๊ปํ๋ ์ ๋ฌผ ํ๋ฉ ํ๋ซํผ์
๋๋ค.
์์ ์ด ์ํ๋ ์ ๋ฌผ์ ์น๊ตฌ๋ค์๊ฒ ํ๋ฉ์ ๋ฐ๊ณ , ๊ทธ๋์ ๊ฐ๊ณ ์ถ์๋ ๊ณ ๊ฐ์ ์ ๋ฌผ์ ๊ตฌ๋งคํ ์ ์์ต๋๋ค.
๋ฟ๋ง ์๋๋ผ, ์น๊ตฌ๋ค์ ๋์์ผ๋ก ๊ฟ์ ์คํํ ์๋ ์๋ ํ์ ์ ์ธ ํ๋ซํผ์
๋๋ค.
Next.js
- React ๊ธฐ๋ฐ์ ์น ๊ฐ๋ฐ ํ๋ ์์ํฌ
- ๊ฒ์์์ง ์ต์ ํ(SEO)์ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ ์ฅ์ ์ ๊ฐ์ง๊ณ ์์
- Routing์ ํธ์์ฑ์ด ์๋น์ค ํน์ง๊ณผ ์ ๋ง๋ฌผ๋ฆฌ๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ
Typescript
- Javascript์ ํ์ ์ด ์ถ๊ฐ๋ ์ ์ ํ์ ์ธ์ด
- complie ๋จ๊ณ์์ ์๋ฌ๋ฅผ ๋ฐ๊ฒฌํด๋ผ ์ ์์ด ํจ์จ์ ์ธ ๊ฐ๋ฐ ๊ฐ๋ฅ
React-query
- ์๋ฒ ์ํ๊ด๋ฆฌ๋ฅผ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Recoil
- ์ ์ญ ํด๋ผ์ด์ธํธ ์ํ๊ด๋ฆฌ๋ฅผ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Styled-components
- ๋์ ์คํ์ผ๋ง์ ์ฉ์ดํ๊ฒ ํด์ฃผ๋ ์คํ์ผ๋ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Axios
- HTTP ์์ฒญ์ ์ฉ์ดํ๊ฒ ํด์ฃผ๋ Promise ๊ธฐ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์ฐธ์ฌ์๊ฐ ์์ฑ์๊ฐ ๊ณต์ ํ ๋งํฌ๋ก ์ง์ ํ๊ฒ ๋๋ฉด, ๋ก๊ทธ์ธ ํ๋ฉด์ ๋ณด์ฌ์ค๋ค.
๋ค์ด๋ฒ๋ก ์์ํ๊ธฐ
๋ฒํผ์ ํด๋ฆญํ๋ฉด, ๋ค์ด๋ฒ๋ก๊ทธ์ธ ๋งํฌ๋ก ์ ์ํ๋ค.- ์ฐธ์ฌ์๊ฐ ๋ค์ด๋ฒ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ์ฌ ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด ์ธ๊ฐ์ฝ๋๋ฅผ ๋ฐ๊ธ๋ฐ๋๋ค.
- ๋ฐ๊ธ๋ฐ์ ์ธ๊ฐ์ฝ๋๋ฅผ ์๋ฒ์ ์ ๋ฌํด, ์ก์ธ์ค ํ ํฐ์ ๋ฐ๊ธํ๋ค.
- ๋ฐ๊ธ๋ฐ์ ์ก์ธ์ค ํ ํฐ์ ์์ผ๋ก์ ์๋ฒ ์์ฒญ ์ headers์ ๋ฃ์ด ์ฌ์ฉ์ ์๋ณ์ ์ฌ์ฉ๋๋ค.
- ๋ง์ฝ ์ต์ด๋ก ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ด๋ฉด, ์ถ๊ฐ ์ ๋ณด๋ฅผ ์ ๋ ฅํ์ฌ ๊ฐ์ ์ ์๋ฃํ๋ค.
- ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด ํ๋ฉ ์์ธ๋ณด๊ธฐ ๋ทฐ๋ก ์ง์ ํ๋ฉฐ, ์์ฑ์ ์ด๋ฆ, ํ๋ฉ ์ด๋ฆ, ํ๋ฉ ์ฌ์ง, ๋ชจ์ธ ๊ธ์ก์ด ํ์๋๋ค.
ํ๋ฉ์ ์ฐธ์ฌํ ๋์
๋ฒํผ์ ํด๋ฆญํ๋ฉด ํ๋ฉ ์ฐธ์ฌ๋ฅผ ์ํ ์ ๋ณด ์ ๋ ฅ ๋ทฐ๋ก ์ด๋ํ๋ค.
- ํ๋ฉ ์ฐธ์ฌ์์ ์ด๋ฆ, ํ๋ฉํ ๊ธ์ก, ์์ ๋ฉ์์ง ๋ฑ์ ์ ๋ ฅํด ํ๋ฉ์ ์ฐธ์ฌํ ์ ์๋ค.
- ์ ๋ ฅํ ์ ๋ณด๋ ์ถํ ํ๋ฉ ์์ฑ์์๊ฒ ์ ๋ฌ๋๋ค.
- ๊ฒฐ์ ์๋จ์ ์ด์ ์ ์ ๋ ฅํ๋ ์นด๋๊ฐ ์์ผ๋ฉด ์ ํํด์ ๊ฒฐ์ ํ ์ ์๋ค.
- ์ ๋ฌผ๋ ๊ธ์ก์ด ํ๋ฉ ์์ฑ์์ ์ ํ์ ๋ฐ๋ผ ๋ค๋ฅธ ์ํ ๊ตฌ๋งค์ ์ฐ์ผ ์ ์์ผ๋ฏ๋ก, ํด๋น ์ฌํญ์ ๋์ํด์ผ ์ต์ข ๊ฒฐ์ ๊ฐ ๊ฐ๋ฅํ๋ค.
- ๋ง์ผ ์์ง ๊ฒฐ์ ์๋จ์ ์ ๋ ฅํ์ง ์์๊ฑฐ๋ ์๋ก์ด ๊ฒฐ์ ์๋จ์ ์ ๋ ฅํ๊ณ ์ถ๋ค๋ฉด ์นด๋์ ๋ณด๋ฅผ ์ ๋ ฅํ ์ ์๋ค.
- ์นด๋ ์ ๋ณด๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ ๋ ฅํ์ง ์์ ๊ฒฝ์ฐ, ์๋ฆผ์ฐฝ์ด ๋จ๋ฉฐ ์ฌ๋ฐ๋ฅธ ๊ฐ์ ์ ๋ ฅํ๋๋ก ์ ๋ํ๋ค.
- ์ต์ข ์ ์ผ๋ก ์ฐธ์ฌ๊ฐ ์๋ฃ๋์์์ ์๋ฆฌ๋ ํ๋ฉด์ด๋ค.
์ฐธ์ฌ๋ด์ญ ๋ณด๋ฌ๊ฐ๊ธฐ
๋ฅผ ํด๋ฆญํด ์ฐธ์ฌํ ํ๋ฉ ๋ฆฌ์คํธ๋ฅผ ์กฐํํ ์ ์๋ค.
- ์ฐธ์ฌํ๋ ํ๋ฉ ๋ด์ญ์ ํ์ธํ ์ ์๋ ๋ทฐ์ด๋ค.
- ํ๋ฉ์ ํด๋ฆญํด ์ฐธ์ฌํ๋ ํ๋ฉ์ ์ทจ์ํ ์ ์์ผ๋ฉฐ, ์ทจ์ ๋ ์ดํ์๋ 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 };
};