diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e5fc96f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 washpedia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/app/car-wash-details/hydrated-page.tsx b/src/app/car-wash-details/hydrated-page.tsx new file mode 100644 index 00000000..537ca61d --- /dev/null +++ b/src/app/car-wash-details/hydrated-page.tsx @@ -0,0 +1,94 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ + +'use client'; + +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import CarDetails from '@components/additional-info/car-details/CarDetails'; +import DetailsLoading from '@components/additional-info/details-loading/DetailsLoading'; +import useCarWashCost from '@remote/queries/additional-info/car-wash-details/useCarWashCost'; +import useCarWashFrequency from '@remote/queries/additional-info/car-wash-details/useCarWashFrequency'; +import useCarWashInterest from '@remote/queries/additional-info/car-wash-details/useCarWashInterest'; +import Header from '@shared/header/Header'; +import ProgressBar from '@shared/progress-bar/ProgressBar'; +import Spacing from '@shared/spacing/Spacing'; + +function CarWashDetailsPage() { + const { data: carWashFrequencyData } = useCarWashFrequency(); + const { data: carWashCostData } = useCarWashCost(); + const { data: carWashInterestData } = useCarWashInterest(); + + const [step, setStep] = useState(1); + + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + register, getValues, formState: { dirtyFields }, + } = useForm(); + + const onNext = () => { + setStep((currentStep) => { return currentStep + 1; }); + }; + + // eslint-disable-next-line @typescript-eslint/require-await + const onSubmit = async () => { + onNext(); + // TODO: 쿼리훅 제작 + // console.log(getValues()); + }; + + // TODO: Loader 컴포넌트 제작 + if (carWashFrequencyData == null + || carWashCostData == null + || carWashInterestData == null + ) { + return
로딩중 입니다..
; + } + + return ( + <> + {step <= 3 && ( + <> +
+ + + + + )} + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + + )} + {step === 4 && } + + ); +} + +export default CarWashDetailsPage; diff --git a/src/app/car-wash-details/layout.tsx b/src/app/car-wash-details/layout.tsx new file mode 100644 index 00000000..4d902e0d --- /dev/null +++ b/src/app/car-wash-details/layout.tsx @@ -0,0 +1,7 @@ +function CarWashDetailsPageLayout({ children }: { children: React.ReactNode }) { + return ( +
{children}
+ ); +} + +export default CarWashDetailsPageLayout; diff --git a/src/app/car-wash-details/page.tsx b/src/app/car-wash-details/page.tsx new file mode 100644 index 00000000..81ec4d9d --- /dev/null +++ b/src/app/car-wash-details/page.tsx @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +// app/hydratedPosts.jsx +import { dehydrate, Hydrate } from '@tanstack/react-query'; + +import getQueryClient from '@lib/getQueryClient'; +import { + getCarWashFrequency, getCarWashCost, getCarWashInterest, +} from '@remote/api/requests/additional-info/additional-info.get.api'; + +import CarWashDetailsPage from './hydrated-page'; + +const HydratedCarWashDetails = async () => { + const queryClient = getQueryClient(); + + await Promise.all([ + queryClient.prefetchQuery({ queryKey: ['car-wash-frequency'], queryFn: getCarWashFrequency }), + queryClient.prefetchQuery({ queryKey: ['car-wash-cost'], queryFn: getCarWashCost }), + queryClient.prefetchQuery({ queryKey: ['car-wash-interest'], queryFn: getCarWashInterest }), + ]); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +}; + +export default HydratedCarWashDetails; diff --git a/src/app/find-id/layout.tsx b/src/app/find-id/layout.tsx new file mode 100644 index 00000000..1b58628e --- /dev/null +++ b/src/app/find-id/layout.tsx @@ -0,0 +1,7 @@ +function FindIdLayout({ children }: { children: React.ReactNode }) { + return ( +
{children}
+ ); +} + +export default FindIdLayout; diff --git a/src/app/find-id/page.tsx b/src/app/find-id/page.tsx new file mode 100644 index 00000000..aac88dfd --- /dev/null +++ b/src/app/find-id/page.tsx @@ -0,0 +1,75 @@ +/* eslint-disable no-alert */ +/* eslint-disable @typescript-eslint/no-misused-promises */ + +'use client'; + +import { useForm } from 'react-hook-form'; + +import dynamic from 'next/dynamic'; + +import VALIDATION_MESSAGE_MAP from '@constants/validationMessage'; +import { IFindId } from '@remote/api/types/auth'; +import useFindId from '@remote/queries/auth/useFindId'; +import Header from '@shared/header/Header'; +import Spacing from '@shared/spacing/Spacing'; +import TextField from '@shared/text-field/TextField'; +import Title from '@shared/title/Title'; + +const FixedBottomButton = dynamic(() => { return import('@shared/fixedBottomButton/FixedBottomButton'); }, { + ssr: false, +}); + +function FindIdPage() { + const { register, handleSubmit, formState: { isValid, errors, isDirty } } = useForm({ + mode: 'onBlur', + }); + const { mutate } = useFindId(); + + const onSubmit = (data: IFindId) => { + const { email } = data; + mutate({ email }, { + onError: (error) => { + console.error('Error:', error); + alert('다시 입력해주세요'); + // TODO: 아이디 찾기 실패 시 알림 메세지 바로 출력 + }, + onSuccess: () => { + alert('회원님의 이메일로 아이디 전송완료'); + // TODO: 아이디 전송완료 페이지 로드하기 + }, + }); + }; + + return ( + <> +
+ +
+ + <Spacing size={40} /> + <TextField + label="이메일" + required + placeholder="이메일" + {...register('email', { + required: true, + pattern: VALIDATION_MESSAGE_MAP.email.value, + })} + hasError={!!errors.email} + helpMessage={VALIDATION_MESSAGE_MAP.failedFindId.message} + /> + <div> + <FixedBottomButton + disabled={!isValid || !isDirty} + onClick={handleSubmit(onSubmit)} + size="medium" + > + 다음 + </FixedBottomButton> + </div> + </main> + </> + ); +} + +export default FindIdPage; diff --git a/src/components/additional-info/car-details/CarDetails.tsx b/src/components/additional-info/car-details/CarDetails.tsx index 0052a4c4..1e4f8953 100644 --- a/src/components/additional-info/car-details/CarDetails.tsx +++ b/src/components/additional-info/car-details/CarDetails.tsx @@ -28,21 +28,23 @@ function CarDetails({ <> <Description main={main} sub={sub} /> <Spacing size={40} /> - <Flex direction="column" gap={10}> - {options?.map((option) => { - return ( - <Radio - key={option.codeNo} - type="additionalInfo" - label={option.description} - value={option.codeName} - {...register(option.upperName, { - required: true, - })} - /> - ); - })} - </Flex> + <div style={{ margin: '0 20px' }}> + <Flex direction="column" gap={10}> + {options?.map((option) => { + return ( + <Radio + key={option.codeNo} + type="additionalInfo" + label={option.description} + value={option.codeName} + {...register(option.upperName, { + required: true, + })} + /> + ); + })} + </Flex> + </div> <FixedBottomButton disabled={!dirtyFields[options[0].upperName] ?? false} onClick={onClick} diff --git a/src/components/additional-info/description/Description.tsx b/src/components/additional-info/description/Description.tsx index def7765a..f54717aa 100644 --- a/src/components/additional-info/description/Description.tsx +++ b/src/components/additional-info/description/Description.tsx @@ -10,7 +10,7 @@ interface DescriptionProps { function Description({ main, sub }: DescriptionProps) { return ( <Flex direction="column" justify="center" align="center"> - <Text typography="t3" bold>{main}</Text> + <Text typography="t3" wordBreak="keep-all" textAlign="center" fontWeight={600}>{main}</Text> <Spacing size={10} /> <Text typography="t6" color="tertiary">{sub}</Text> </Flex> diff --git a/src/components/shared/fixedBottomButton/FixedBottomButton.module.scss b/src/components/shared/fixedBottomButton/FixedBottomButton.module.scss index 22de08a8..60f111ab 100644 --- a/src/components/shared/fixedBottomButton/FixedBottomButton.module.scss +++ b/src/components/shared/fixedBottomButton/FixedBottomButton.module.scss @@ -3,7 +3,7 @@ right: 0; bottom: 0; left: 0; - padding: 20px 10px 66px; + padding: 20px 24px 66px; // transform: translateY(100%); // animation: slideup 0.5s ease-in-out forwards; diff --git a/src/components/shared/fixedBottomButton/FixedBottomButton.tsx b/src/components/shared/fixedBottomButton/FixedBottomButton.tsx index e525ac12..0ce3b5cc 100644 --- a/src/components/shared/fixedBottomButton/FixedBottomButton.tsx +++ b/src/components/shared/fixedBottomButton/FixedBottomButton.tsx @@ -13,9 +13,12 @@ interface FixedBottomButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> children: React.ReactNode disabled?: boolean onClick: () => void + size?: 'large' | 'small' | 'medium' } -function FixedBottomButton({ children, disabled, onClick }: FixedBottomButtonProps) { +function FixedBottomButton({ + children, onClick, disabled, size = 'large', +}: FixedBottomButtonProps) { const PORTAL_ROOT = document.getElementById('portal-root'); if (PORTAL_ROOT == null) { return null; @@ -23,7 +26,7 @@ function FixedBottomButton({ children, disabled, onClick }: FixedBottomButtonPro return createPortal( <div className={cx('container')}> - <Button size="large" full disabled={disabled} onClick={onClick}> + <Button size={size} full disabled={disabled} onClick={onClick}> {children} </Button> </div>, diff --git a/src/components/shared/progress-bar/ProgressBar.module.scss b/src/components/shared/progress-bar/ProgressBar.module.scss index ab9b7086..dfa139a7 100644 --- a/src/components/shared/progress-bar/ProgressBar.module.scss +++ b/src/components/shared/progress-bar/ProgressBar.module.scss @@ -1,6 +1,5 @@ .container { position: relative; - width: 185px; height: 24px; margin: 0 auto; diff --git a/src/components/shared/progress-bar/ProgressBar.tsx b/src/components/shared/progress-bar/ProgressBar.tsx index eec604d8..9653b5d5 100644 --- a/src/components/shared/progress-bar/ProgressBar.tsx +++ b/src/components/shared/progress-bar/ProgressBar.tsx @@ -28,7 +28,7 @@ function ProgressBar({ progressCount = 5, currentStep = 1, setCurrentStep }: Pro }; return ( - <div className={cx('container')}> + <div className={cx('container')} style={{ width: progressCount * 40 }}> <div className={cx('progressBar')} /> <div className={cx('progress')} style={{ width: progressBarWidth }} /> <div> diff --git a/src/components/shared/text-field/TextField.tsx b/src/components/shared/text-field/TextField.tsx index 16b10004..9d64ee93 100644 --- a/src/components/shared/text-field/TextField.tsx +++ b/src/components/shared/text-field/TextField.tsx @@ -37,7 +37,7 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(function TextFiel <div> {label && <Text typography="t6" display="inline-block" color={labelColor}>{label}</Text>} {required && <Text typography="t6" display="inline-block" color="red">*</Text>} - <Spacing size={12} /> + <Spacing size={4} /> <Input ref={ref} aria-invalid={hasError} @@ -46,10 +46,10 @@ const TextField = forwardRef<HTMLInputElement, TextFieldProps>(function TextFiel isPasswordType={isPasswordType} {...props} /> - {hasError ? <Spacing size={6} /> : <Spacing size={20} />} + {hasError ? <Spacing size={4} /> : <Spacing size={20} />} {hasError && ( <> - <Text typography="t7" color={labelColor} display="inline-block">{helpMessage}</Text> + <Text typography="t8" color={labelColor} display="inline-block">{helpMessage}</Text> <Spacing size={6} /> </> )} diff --git a/src/components/shared/text/Text.tsx b/src/components/shared/text/Text.tsx index 69de7de4..8fc4099e 100644 --- a/src/components/shared/text/Text.tsx +++ b/src/components/shared/text/Text.tsx @@ -13,10 +13,11 @@ interface TextProps { children: React.ReactNode whiteSpace?: CSSProperties['whiteSpace'] className?: string + wordBreak?:CSSProperties['wordBreak'] } function Text({ - typography = 't5', color = 'black', display, textAlign, fontWeight, bold, children, whiteSpace = 'pre-line', className, + typography = 't5', color = 'black', display, textAlign, fontWeight, bold, children, whiteSpace = 'pre-line', wordBreak = 'normal', className, }: TextProps) { const styles = useMemo(() => { return { @@ -26,8 +27,9 @@ function Text({ textAlign, fontWeight: bold ? 'bold' : fontWeight, whiteSpace, + wordBreak, }; - }, [typography, color, display, textAlign, fontWeight, bold, whiteSpace]); + }, [typography, color, display, textAlign, fontWeight, bold, whiteSpace, wordBreak]); return ( <span className={className} style={styles}>{children}</span> ); diff --git a/src/components/shared/title/Title.tsx b/src/components/shared/title/Title.tsx index 285c4675..c5c89440 100644 --- a/src/components/shared/title/Title.tsx +++ b/src/components/shared/title/Title.tsx @@ -23,7 +23,7 @@ function Title({ {titleIcon} </Flex> <Spacing size={size} /> - <Text color={descriptionColor}>{description}</Text> + <Text typography="t6" color={descriptionColor}>{description}</Text> </Flex> ); } diff --git a/src/constants/validationMessage.ts b/src/constants/validationMessage.ts index cb499f7a..c4aa65c8 100644 --- a/src/constants/validationMessage.ts +++ b/src/constants/validationMessage.ts @@ -20,6 +20,7 @@ const VALIDATION_MESSAGE_MAP: { message: '비밀번호를 확인해주세요.', }, failedLogin: { message: '아이디 또는 비밀번호를 확인해주세요.' }, + failedFindId: { message: '잘못된 이메일입니다.' }, } as const; export default VALIDATION_MESSAGE_MAP; diff --git a/src/remote/api/requests/additional-info/additional-info.get.api.ts b/src/remote/api/requests/additional-info/additional-info.get.api.ts index f53cf673..71c30b27 100644 --- a/src/remote/api/requests/additional-info/additional-info.get.api.ts +++ b/src/remote/api/requests/additional-info/additional-info.get.api.ts @@ -30,3 +30,24 @@ export const getCarParking = async () => { return response; }; + +// 세차빈도 : frequency +export const getCarWashFrequency = async () => { + const response = await getRequest<IAdditionalInfo[]>('/commoncode/frequency'); + + return response; +}; + +// 지출비용 : cost +export const getCarWashCost = async () => { + const response = await getRequest<IAdditionalInfo[]>('/commoncode/cost'); + + return response; +}; + +// 주요관심사 : interest +export const getCarWashInterest = async () => { + const response = await getRequest<IAdditionalInfo[]>('/commoncode/interest'); + + return response; +}; diff --git a/src/remote/api/requests/auth/auth.post.api.ts b/src/remote/api/requests/auth/auth.post.api.ts index 8332e69c..c3eacc7c 100644 --- a/src/remote/api/requests/auth/auth.post.api.ts +++ b/src/remote/api/requests/auth/auth.post.api.ts @@ -1,4 +1,4 @@ -import { ISignIn, ISignUp } from '../../types/auth'; +import { IFindId, ISignIn, ISignUp } from '../../types/auth'; import { postRequest } from '../requests.api'; export const signup = async ({ @@ -20,3 +20,13 @@ export const login = async ({ return response; }; + +export const findId = async ({ + email, +}: IFindId) => { + const response = await postRequest<null, IFindId>('/member/find-id', { + email, + }); + + return response; +}; diff --git a/src/remote/api/types/auth.ts b/src/remote/api/types/auth.ts index 1f2e92d5..0489f15a 100644 --- a/src/remote/api/types/auth.ts +++ b/src/remote/api/types/auth.ts @@ -1,3 +1,7 @@ +export interface IFindId { + email:string +} + export interface ISignIn { id: string password: string diff --git a/src/remote/queries/additional-info/car-wash-details/useCarWashCost.tsx b/src/remote/queries/additional-info/car-wash-details/useCarWashCost.tsx new file mode 100644 index 00000000..49fa8a1f --- /dev/null +++ b/src/remote/queries/additional-info/car-wash-details/useCarWashCost.tsx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getCarWashCost } from '@remote/api/requests/additional-info/additional-info.get.api'; +import { IAdditionalInfo } from '@remote/api/types/additional-info'; + +function useCarWashCost() { + return useQuery<IAdditionalInfo[]>({ queryKey: ['car-wash-cost'], queryFn: getCarWashCost }); +} + +export default useCarWashCost; diff --git a/src/remote/queries/additional-info/car-wash-details/useCarWashFrequency.tsx b/src/remote/queries/additional-info/car-wash-details/useCarWashFrequency.tsx new file mode 100644 index 00000000..2dbf1eca --- /dev/null +++ b/src/remote/queries/additional-info/car-wash-details/useCarWashFrequency.tsx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getCarWashFrequency } from '@remote/api/requests/additional-info/additional-info.get.api'; +import { IAdditionalInfo } from '@remote/api/types/additional-info'; + +function useCarWashFrequency() { + return useQuery<IAdditionalInfo[]>({ queryKey: ['car-wash-frequency'], queryFn: getCarWashFrequency }); +} + +export default useCarWashFrequency; diff --git a/src/remote/queries/additional-info/car-wash-details/useCarWashInterest.tsx b/src/remote/queries/additional-info/car-wash-details/useCarWashInterest.tsx new file mode 100644 index 00000000..2a4ee1d7 --- /dev/null +++ b/src/remote/queries/additional-info/car-wash-details/useCarWashInterest.tsx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getCarWashInterest } from '@remote/api/requests/additional-info/additional-info.get.api'; +import { IAdditionalInfo } from '@remote/api/types/additional-info'; + +function useCarWashInterest() { + return useQuery<IAdditionalInfo[]>({ queryKey: ['car-wash-interest'], queryFn: getCarWashInterest }); +} + +export default useCarWashInterest; diff --git a/src/remote/queries/auth/useFindId.ts b/src/remote/queries/auth/useFindId.ts new file mode 100644 index 00000000..b89b2bbe --- /dev/null +++ b/src/remote/queries/auth/useFindId.ts @@ -0,0 +1,10 @@ +/* eslint-disable no-console */ +import { useMutation } from '@tanstack/react-query'; + +import { findId } from '@remote/api/requests/auth/auth.post.api'; + +function useFindId() { + return useMutation({ mutationFn: findId }); +} + +export default useFindId; diff --git a/src/styles/button.ts b/src/styles/button.ts index 46ff8994..48cfd675 100644 --- a/src/styles/button.ts +++ b/src/styles/button.ts @@ -42,7 +42,7 @@ export const buttonSizeMap = { padding: '8px 9px', }, medium: { - fontSize: '15px', + fontSize: '16px', padding: '10px 15px', }, large: {