From 90193c160191b3db831bd45d3125414edabc0e9b Mon Sep 17 00:00:00 2001 From: myeongheonhong Date: Fri, 22 Dec 2023 13:41:01 +0900 Subject: [PATCH] =?UTF-8?q?[=20feat=20]=20=EC=BC=80=EC=9D=B4=ED=81=AC=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=EC=88=98=EB=8B=A8=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/public.ts | 21 ++- components/cakes/CakesForm.tsx | 125 ++++++++++++++ components/cakes/CakesPay.tsx | 151 +++++++++++++++++ components/cakes/CakesResult.tsx | 145 ++++++++++++++++ components/cakes/PayCakes.tsx | 3 - .../{approve => Result}/contribution.tsx | 0 .../{approve => Result}/successItemBox.tsx | 1 + components/cakes/index.tsx | 156 ++++++------------ .../common/Select/PaymentItemSelect.tsx | 91 ++++++++++ components/common/Select/index.tsx | 3 + components/common/box/imageBox.tsx | 1 + components/common/button/index.tsx | 2 +- components/common/progressBar.tsx | 2 +- hooks/common/useSelect.ts | 5 + hooks/queries/public.ts | 15 +- types/api/request.ts | 6 + types/api/response.ts | 9 + 17 files changed, 620 insertions(+), 116 deletions(-) create mode 100644 components/cakes/CakesForm.tsx create mode 100644 components/cakes/CakesPay.tsx create mode 100644 components/cakes/CakesResult.tsx delete mode 100644 components/cakes/PayCakes.tsx rename components/cakes/{approve => Result}/contribution.tsx (100%) rename components/cakes/{approve => Result}/successItemBox.tsx (99%) create mode 100644 components/common/Select/PaymentItemSelect.tsx create mode 100644 components/common/Select/index.tsx create mode 100644 hooks/common/useSelect.ts diff --git a/api/public.ts b/api/public.ts index a6e6958c..75382648 100644 --- a/api/public.ts +++ b/api/public.ts @@ -1,6 +1,10 @@ -import { PublicWishesDataResponseType } from '@/types/api/response'; +import { PostPublicCakesResponseType, PublicWishesDataResponseType } from '@/types/api/response'; import { client } from './common/axios'; import { API_VERSION_01, PATH_PUBLIC } from './path'; +import { PostPublicCakesRequestType } from '@/types/api/request'; +import { getAccessToken } from '@/utils/common/token'; + +const ACCESS_TOKEN = getAccessToken(); export const getPublicWishes = async (wishId: string | string[] | undefined) => { const data = await client.get( @@ -9,3 +13,18 @@ export const getPublicWishes = async (wishId: string | string[] | undefined) => return data.data.data; }; + +export const postPublicCakes = async (parameter: PostPublicCakesRequestType) => { + const data = await client.post( + `${API_VERSION_01}${PATH_PUBLIC.CAKES}`, + parameter, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${ACCESS_TOKEN}`, + }, + }, + ); + + return data.data.data; +}; diff --git a/components/cakes/CakesForm.tsx b/components/cakes/CakesForm.tsx new file mode 100644 index 00000000..71e7229e --- /dev/null +++ b/components/cakes/CakesForm.tsx @@ -0,0 +1,125 @@ +import { useEffect } from 'react'; +import TextareaBox from '../common/input/textareaBox'; +import styled from 'styled-components'; +import theme from '@/styles/theme'; +import { LIMIT_TEXT } from '@/constant/limitText'; +import SelectCakes from './SelectCakes'; +import Button from '../common/button'; +import Input from '../common/input/input'; +import { UseFormReturn } from 'react-hook-form'; +import InputContainer from '../common/input/inputContainer'; +import { CakesDataInputType } from '@/types/common/input/cakesInput'; +import { StyledBox } from '../common/box'; +import BackBtn from '../common/button/backBtn'; +import { CakeListType } from '@/types/cakes/cakeListType'; +import { useGetPublicWishes, usePostPublicCakes } from '@/hooks/queries/public'; +import { UseMutateFunction } from 'react-query'; + +interface CakesFormProps { + methods: UseFormReturn; + selectedCake: CakeListType; + selectedIndex: number; + selectCake: (index: number) => void; + wishesId: string | string[] | undefined; + postPublicCakesData: UseMutateFunction< + { + cakeId: number; + imageUrl: string; + hint: string; + initial: string; + contribute: string; + wisher: string; + }, + unknown, + void, + unknown + >; +} + +export default function CakesForm(props: CakesFormProps) { + const { methods, selectedCake, selectedIndex, selectCake, wishesId, postPublicCakesData } = props; + + const { publicWishesData } = useGetPublicWishes(wishesId); + + return ( + <> +
+ + + {`D-${publicWishesData?.dayCount}`} + + + {publicWishesData?.title} + + + + {publicWishesData?.hint} + + {/* */} + + + + + + + + + + + +
+ + + + + ); +} + +const Styled = { + Header: styled.header` + display: flex; + justify-content: space-between; + + width: 100%; + + color: ${theme.colors.main_blue}; + ${theme.fonts.headline20}; + `, + + Title: styled.h1` + ${theme.fonts.headline24_100}; + color: ${theme.colors.main_blue}; + margin: 2.4rem 0 3rem; + `, + + HintBox: styled(StyledBox)` + width: 100%; + height: 12.6rem; + + ${theme.fonts.body14}; + + padding: 1.2rem 1rem 1.2rem 1.2rem; + `, + + ButtonWrapper: styled.div` + padding-bottom: 4.6rem; + `, +}; diff --git a/components/cakes/CakesPay.tsx b/components/cakes/CakesPay.tsx new file mode 100644 index 00000000..ebf27616 --- /dev/null +++ b/components/cakes/CakesPay.tsx @@ -0,0 +1,151 @@ +import styled from 'styled-components'; +import theme from '@/styles/theme'; +import { BackBtnIc } from '@/public/assets/icons'; +import Image from 'next/image'; +import { CakeListType } from '@/types/cakes/cakeListType'; +import { convertMoneyText } from '@/utils/common/convertMoneyText'; +import PaymentItemSelect from '../common/Select/PaymentItemSelect'; +import { BANK_LIST } from '@/constant/bankList'; +import { BankListType } from '@/types/bankListType'; +import Button from '../common/button'; +import { useState } from 'react'; +import { useGetPublicWishes } from '@/hooks/queries/public'; +import router from 'next/router'; + +interface CakesPayProps { + handlePrevStep: () => void; + handleNextStep: () => void; + selectedCake: CakeListType; + wishesId: string | string[] | undefined; +} + +export default function CakesPay(props: CakesPayProps) { + const { handlePrevStep, handleNextStep, selectedCake, wishesId } = props; + + const { publicWishesData } = useGetPublicWishes(wishesId); + + const PAYMENTS: BankListType[] = [BANK_LIST[5], BANK_LIST[1]]; + const [selectedPayment, setSelected] = useState(); + + const handlePaymentSelect = (payment: BankListType) => { + setSelected(payment); + }; + + const handleDeepLink = (payment: BankListType | undefined) => { + const ua = navigator.userAgent.toLowerCase(); + + if (!selectedPayment) { + alert('결제수단을 선택해주세요!'); + return; + } + + if (window.confirm(`${payment?.name}(으)로 이동할까요?`)) { + if (payment?.name === '토스뱅크') { + window.open( + ua.indexOf('android') > -1 + ? 'https://play.google.com/store/apps/details?id=viva.republica.toss' + : 'https://apps.apple.com/app/id839333328', + ); + } + + if (payment?.name === '카카오뱅크') { + window.open( + ua.indexOf('android') > -1 + ? 'https://play.google.com/store/apps/details?id=com.kakaobank.channel' + : 'https://apps.apple.com/app/id1258016944', + ); + } + handleNextStep(); + } + }; + + return ( + <> + + 뒤로가기 + + + 주문 확인 내역 + {`${selectedCake.name} ${convertMoneyText( + String(selectedCake.price), + )}원을\n${publicWishesData?.name}님께 보내시겠습니까?`} + + + 케이크 이미지 + + + 결제수단 선택 + + + {PAYMENTS.map((payment: BankListType) => ( + + ))} + + + + + + + + ); +} + +const Styled = { + Header: styled.header` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 3rem; + `, + + Container: styled.section` + width: 100%; + height: 100%; + + margin: 2.4rem 0 2rem; + `, + + TitleText: styled.h1` + ${theme.fonts.headline24_130}; + color: ${theme.colors.main_blue}; + `, + + TextWrapper: styled.div` + ${theme.fonts.headline24_130}; + color: ${theme.colors.black}; + + margin-top: 0.7rem; + + white-space: pre-line; + `, + + ImageWrapper: styled.div` + margin: 1.5rem 0 2rem; + `, + + PaymentWrapper: styled.ul` + display: flex; + flex-direction: column; + + gap: 1.4rem; + + margin-top: 2rem; + `, + + ButtonWrapper: styled.div` + padding-bottom: 4.6rem; + `, +}; diff --git a/components/cakes/CakesResult.tsx b/components/cakes/CakesResult.tsx new file mode 100644 index 00000000..553d2110 --- /dev/null +++ b/components/cakes/CakesResult.tsx @@ -0,0 +1,145 @@ +import theme from '@/styles/theme'; +import styled from 'styled-components'; +import ImageBox from '@/components/common/box/imageBox'; +import ItemImageBox from '@/components/common/box/itemImageBox'; +import Contribution from './Result/contribution'; +import { CakeListType } from '@/types/cakes/cakeListType'; +import { useGetPublicWishes } from '@/hooks/queries/public'; +import Image from 'next/image'; +import Button from '../common/button'; +import { StyledBox } from '../common/box'; +import router from 'next/router'; + +interface CakesResultProps { + cakesResultData: + | { + cakeId: number; + imageUrl: string; + hint: string; + initial: string; + contribute: string; + wisher: string; + } + | undefined; + selectedCake: CakeListType; +} + +export default function CakesResult(props: CakesResultProps) { + const { cakesResultData, selectedCake } = props; + + const handleMoveHome = () => { + router.push('/'); + }; + + return ( + <> + + + {cakesResultData?.wisher}님께 + + 케이크 감사 이미지 + + {selectedCake.name} + 선물이 완료 되었어요! + + {cakesResultData?.cakeId === 1 ? ( + <> + + + ~선물 초성힌트~ + {cakesResultData?.initial} + + + 사실 내가 갖고 싶었던 건...비밀이야❤ + + ) : ( + <> + + 사실 내가 갖고 싶었던 건...이거야❤ + + )} + {cakesResultData && ( + + )} + + + + + + ); +} + +const Styled = { + Container: styled.section` + display: flex; + flex-direction: column; + align-items: center; + + margin-top: 5rem; + `, + WishText: styled.div` + color: ${theme.colors.main_blue}; + ${theme.fonts.body16}; + + margin-top: 1.4rem; + `, + + HintWrapper: styled.div` + display: flex; + flex-direction: column; + align-items: center; + + color: ${theme.colors.main_blue}; + ${theme.fonts.body16}; + + margin-top: 1.4rem; + `, + + HintText: styled.div` + ${theme.fonts.headline30}; + margin-top: 3.4rem; + `, + + CakeText: styled.span` + ${theme.fonts.headline30}; + color: ${theme.colors.main_blue}; + `, + + ImageWrapper: styled.div` + margin: 1.3rem 0 2.1rem; + `, + + TextWrapper: styled.div` + display: flex; + flex-direction: column; + align-items: center; + + ${theme.fonts.headline30}; + + margin-bottom: 3.6rem; + `, + + ButtonWrapper: styled.div` + padding-bottom: 4.6rem; + `, + + HintBox: styled.div` + display: flex; + justify-content: center; + + width: 100%; + height: 16rem; + + border-radius: 1.6rem; + + border: 1px solid ${theme.colors.main_blue}; + background-color: ${theme.colors.pastel_blue}; + `, +}; diff --git a/components/cakes/PayCakes.tsx b/components/cakes/PayCakes.tsx deleted file mode 100644 index 80ecb93c..00000000 --- a/components/cakes/PayCakes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function PayCakes() { - return; -} diff --git a/components/cakes/approve/contribution.tsx b/components/cakes/Result/contribution.tsx similarity index 100% rename from components/cakes/approve/contribution.tsx rename to components/cakes/Result/contribution.tsx diff --git a/components/cakes/approve/successItemBox.tsx b/components/cakes/Result/successItemBox.tsx similarity index 99% rename from components/cakes/approve/successItemBox.tsx rename to components/cakes/Result/successItemBox.tsx index d3f2f46b..ada0c2ce 100644 --- a/components/cakes/approve/successItemBox.tsx +++ b/components/cakes/Result/successItemBox.tsx @@ -9,6 +9,7 @@ import ImageBox from '@/components/common/box/imageBox'; import ItemImageBox from '@/components/common/box/itemImageBox'; export default function SuccessItemBox() { + const cakesData = useRecoilValue(CakesData); useEffect(() => { diff --git a/components/cakes/index.tsx b/components/cakes/index.tsx index 0bbc2fa8..f0da3349 100644 --- a/components/cakes/index.tsx +++ b/components/cakes/index.tsx @@ -1,35 +1,23 @@ -import { CakesData } from '@/recoil/cakes/cakesData'; -import { CakesDataType } from '@/types/cakes/cakesDataType'; -import { useEffect, useState } from 'react'; -import { useResetRecoilState } from 'recoil'; -import TextareaBox from '../common/input/textareaBox'; +import useWishesStep from '@/hooks/wishes/useWisehsStep'; +import CakesForm from './CakesForm'; import styled from 'styled-components'; import theme from '@/styles/theme'; -import { LIMIT_TEXT } from '@/constant/limitText'; -import useSelectCakes from '@/hooks/cakes/useSelectCakes'; -import SelectCakes from './SelectCakes'; -import Button from '../common/button'; -import Input from '../common/input/input'; +import CakesPay from './CakesPay'; import { useForm } from 'react-hook-form'; -import InputContainer from '../common/input/inputContainer'; import { CakesDataInputType } from '@/types/common/input/cakesInput'; -import { useGetPublicWishes } from '@/hooks/queries/public'; +import useSelectCakes from '@/hooks/cakes/useSelectCakes'; import { useRouter } from 'next/router'; -import { StyledBox } from '../common/box'; -import BackBtn from '../common/button/backBtn'; +import { useEffect, useState } from 'react'; +import CakesResult from './CakesResult'; +import { usePostPublicCakes } from '@/hooks/queries/public'; export default function CakesContainer() { + const wishesStep = { ...useWishesStep() }; const { selectedCake, selectedIndex, selectCake } = useSelectCakes(); - const resetCakesData = useResetRecoilState(CakesData); const [wishesId, setWishesId] = useState(''); const router = useRouter(); - useEffect(() => { - if (!router.isReady) return; - setWishesId(router.query.id); - }, [router.isReady]); - const methods = useForm({ defaultValues: { giverName: '', @@ -37,95 +25,52 @@ export default function CakesContainer() { }, }); - // useEffect(() => { - // if (!router.isReady) return; - // setCakesData((prev) => ({ - // ...prev, - // wishId: Number(router.query.id), - // })); - // }, [router.isReady]); - - // useEffect(() => { - // if (isSuccess) { - // const nextLink = data?.data?.data.next_redirect_pc_url; - // const tid = data?.data?.data.tid; - - // setCakesData((prevData) => ({ - // ...prevData, - // tid: tid, - // })); - // router.replace(nextLink); - // } - // }, [isSuccess]); + const { postPublicCakesData, cakesResultData, isSuccess } = usePostPublicCakes({ + name: methods.getValues('giverName'), + wishId: wishesId, + cakeId: selectedCake.cakeNumber, + message: methods.getValues('letter'), + }); useEffect(() => { - resetCakesData(); - }, []); - - const { publicWishesData } = useGetPublicWishes(wishesId); + isSuccess && wishesStep.handleNextStep(); + }, [isSuccess]); - const sendCake = () => {}; + useEffect(() => { + if (!router.isReady) return; + setWishesId(router.query.id); + }, [router.isReady]); return ( -
- - - {`D-${publicWishesData?.dayCount}`} - - - {publicWishesData?.title} - - - - {publicWishesData?.hint} - - {/* */} - - - - - - - - - - - -
- - - - + { + { + 1: ( + + ), + 2: ( + + ), + 3: , + }[wishesStep.stepIndex] + }
); } const Styled = { - Header: styled.header` - display: flex; - justify-content: space-between; - - width: 100%; - - color: ${theme.colors.main_blue}; - ${theme.fonts.headline20}; - `, - Container: styled.div` display: flex; flex-direction: column; @@ -134,19 +79,14 @@ const Styled = { height: 100svh; `, - Title: styled.h1` - ${theme.fonts.headline24_100}; - color: ${theme.colors.main_blue}; - margin: 2.4rem 0 3rem; - `, + Header: styled.header` + display: flex; + justify-content: space-between; - HintBox: styled(StyledBox)` width: 100%; - height: 12.6rem; - - ${theme.fonts.body14}; - padding: 1.2rem 1rem 1.2rem 1.2rem; + color: ${theme.colors.main_blue}; + ${theme.fonts.headline20}; `, ButtonWrapper: styled.div` diff --git a/components/common/Select/PaymentItemSelect.tsx b/components/common/Select/PaymentItemSelect.tsx new file mode 100644 index 00000000..88c00f76 --- /dev/null +++ b/components/common/Select/PaymentItemSelect.tsx @@ -0,0 +1,91 @@ +import styled from 'styled-components'; +import { StyledBox } from '../box'; +import theme from '@/styles/theme'; +import Image from 'next/image'; +import { BankListType } from '@/types/bankListType'; + +interface PaymentItemSelectProps { + payment: BankListType; + handleClickFn: (payment: BankListType) => void; + selectedPayment: BankListType | undefined; +} + +export default function PaymentItemSelect(props: PaymentItemSelectProps) { + const { payment, handleClickFn, selectedPayment } = props; + + const isSelected = (): boolean => { + return payment === selectedPayment; + }; + + return ( + + handleClickFn(payment)} isSelected={isSelected()}> + {isSelected() && } + + + + + 결제 수단 이미지 + + {payment.name} + + + ); +} + +const Styled = { + PaymentItem: styled(StyledBox)` + display: flex; + align-items: center; + gap: 0.8rem; + + width: 100%; + height: 6rem; + + background-color: ${theme.colors.pastel_blue}; + + border-radius: 1rem; + padding: 1rem; + `, + + SelectIcon: styled.div<{ isSelected: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + width: 1.6rem; + height: 1.6rem; + + border-radius: 50%; + background-color: ${theme.colors.white}; + border: 1px solid ${(props) => (props.isSelected ? theme.colors.main_blue : theme.colors.gray2)}; + `, + + SelectInnerIcon: styled.div` + width: 0.9rem; + height: 0.9rem; + + border-radius: 50%; + background-color: ${theme.colors.main_blue}; + `, + + BankItem: styled.div` + width: 3rem; + height: 3rem; + + background-color: ${theme.colors.white}; + border-radius: 0.4rem; + + overflow: hidden; + `, + + BankItemWrapper: styled.div` + display: flex; + align-items: center; + + gap: 1.2rem; + + ${theme.fonts.body14}; + color: ${theme.colors.dark_blue}; + `, +}; diff --git a/components/common/Select/index.tsx b/components/common/Select/index.tsx new file mode 100644 index 00000000..9aeca538 --- /dev/null +++ b/components/common/Select/index.tsx @@ -0,0 +1,3 @@ +export default function Select() { + return <>; +} diff --git a/components/common/box/imageBox.tsx b/components/common/box/imageBox.tsx index 01f15b3b..1481f308 100644 --- a/components/common/box/imageBox.tsx +++ b/components/common/box/imageBox.tsx @@ -44,6 +44,7 @@ const StyledImageBox = styled(StyledBox)<{ width?: number; height: number }>` padding: 0; border-radius: 1.6rem; + background-color: ${theme.colors.pastel_blue}; overflow: hidden; } diff --git a/components/common/button/index.tsx b/components/common/button/index.tsx index 55dc3d2e..81aa7e97 100644 --- a/components/common/button/index.tsx +++ b/components/common/button/index.tsx @@ -6,7 +6,7 @@ interface ButtonProps { width?: number; boxType: BtnBoxTypes; colorSystem: ColorSystemType; - handleClickFn: () => void | unknown; + handleClickFn: (parameter?: unknown) => void | unknown; children: ReactNode; } diff --git a/components/common/progressBar.tsx b/components/common/progressBar.tsx index 354a869e..6504d737 100644 --- a/components/common/progressBar.tsx +++ b/components/common/progressBar.tsx @@ -39,7 +39,7 @@ const Styled = { `} `, Progress: styled.div<{ percent: number }>` - width: ${(props) => props.percent}%; + width: ${(props) => (props.percent > 100 ? 100 : props.percent)}%; height: 100%; background-color: ${theme.colors.main_blue}; diff --git a/hooks/common/useSelect.ts b/hooks/common/useSelect.ts new file mode 100644 index 00000000..17530410 --- /dev/null +++ b/hooks/common/useSelect.ts @@ -0,0 +1,5 @@ +import { useState } from 'react'; + +export default function useSelect() { + const [selectedItem, setSelectedItem] = useState(); +} diff --git a/hooks/queries/public.ts b/hooks/queries/public.ts index 9ba581e2..c7dce5bb 100644 --- a/hooks/queries/public.ts +++ b/hooks/queries/public.ts @@ -1,5 +1,6 @@ -import { getPublicWishes } from '@/api/public'; -import { useQuery } from 'react-query'; +import { getPublicWishes, postPublicCakes } from '@/api/public'; +import { PostPublicCakesRequestType } from '@/types/api/request'; +import { useMutation, useQuery } from 'react-query'; export function useGetPublicWishes(wishId: string | string[] | undefined) { const { data: publicWishesData } = useQuery('publicWishes', () => getPublicWishes(wishId), { @@ -8,3 +9,13 @@ export function useGetPublicWishes(wishId: string | string[] | undefined) { return { publicWishesData }; } + +export function usePostPublicCakes(parameter: PostPublicCakesRequestType) { + const { + mutate: postPublicCakesData, + data: cakesResultData, + ...restProps + } = useMutation(() => postPublicCakes(parameter)); + + return { postPublicCakesData, cakesResultData, ...restProps }; +} diff --git a/types/api/request.ts b/types/api/request.ts index e69de29b..93e37f72 100644 --- a/types/api/request.ts +++ b/types/api/request.ts @@ -0,0 +1,6 @@ +export type PostPublicCakesRequestType = { + name: string; + cakeId: number; + message: string; + wishId: string | string[] | undefined; +}; diff --git a/types/api/response.ts b/types/api/response.ts index 2255da6e..73344fed 100644 --- a/types/api/response.ts +++ b/types/api/response.ts @@ -23,3 +23,12 @@ export type PublicWishesDataResponseType = DefaultResponseType<{ title: string; hint: string; }>; + +export type PostPublicCakesResponseType = DefaultResponseType<{ + cakeId: number; + imageUrl: string; + hint: string; + initial: string; + contribute: string; + wisher: string; +}>;