diff --git a/frontend/src/apis/petProfile.ts b/frontend/src/apis/petProfile.ts index 9fb423849..655fd7c20 100644 --- a/frontend/src/apis/petProfile.ts +++ b/frontend/src/apis/petProfile.ts @@ -5,8 +5,8 @@ import { GetPetReq, GetPetRes, GetPetsRes, - PostPetProfileReq, - PostPetProfileRes, + PostPetReq, + PostPetRes, PutPetReq, PutPetRes, } from '@/types/petProfile/remote'; @@ -31,8 +31,8 @@ export const getPets = async () => { return data; }; -export const postPetProfile = async (postPetProfileProps: PostPetProfileReq) => { - const { data } = await client.post('/pets', postPetProfileProps); +export const postPet = async (postPetProps: PostPetReq) => { + const { data } = await client.post('/pets', postPetProps); return data; }; diff --git a/frontend/src/components/PetProfile/PetInfoInForm.tsx b/frontend/src/components/PetProfile/PetInfoInForm.tsx index 4ff7742b9..05f9eab78 100644 --- a/frontend/src/components/PetProfile/PetInfoInForm.tsx +++ b/frontend/src/components/PetProfile/PetInfoInForm.tsx @@ -17,7 +17,7 @@ const PetInfoInForm = (petInfoInFormProps: PetInfoInFormProps) => { const { previewImage, imageUrl, uploadImage } = useImageUpload(); useEffect(() => { - onChangeImage(imageUrl); + if (imageUrl) onChangeImage(imageUrl); }, [imageUrl]); return ( diff --git a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.stories.tsx b/frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.stories.tsx similarity index 85% rename from frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.stories.tsx rename to frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.stories.tsx index df44e1674..c905105f0 100644 --- a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.stories.tsx +++ b/frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.stories.tsx @@ -6,28 +6,29 @@ import React from 'react'; import { reactRouterParameters } from 'storybook-addon-react-router-v6'; import { styled } from 'styled-components'; -import PetProfileEdition from './PetProfileEdition'; +import PetProfileProvider from '../../../context/petProfile/PetProfileContext'; +import PetProfileEditionForm from './PetProfileEditionForm'; const meta = { - title: 'PetProfile/Edition', - component: PetProfileEdition, + title: 'PetProfile/EditionForm', + component: PetProfileEditionForm, decorators: [ Story => ( - + - + ), ], parameters: { reactRouter: reactRouterParameters({ location: { - pathParams: { petId: '1' }, + pathParams: { petId: '2' }, }, routing: { path: '/pets/:petId/edit' }, }), }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -81,12 +82,3 @@ export const InvalidWeight: Story = { expect(weightErrorMessage).toBeVisible(); }, }; - -const FlexBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - width: 100%; - height: auto; -`; diff --git a/frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.tsx b/frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.tsx new file mode 100644 index 000000000..61f18ec21 --- /dev/null +++ b/frontend/src/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm.tsx @@ -0,0 +1,245 @@ +import { styled } from 'styled-components'; + +import EditIconLight from '@/assets/svg/edit_icon_light.svg'; +import TrashCanIcon from '@/assets/svg/trash_can_icon.svg'; +import Input from '@/components/@common/Input/Input'; +import Label from '@/components/@common/Label/Label'; +import { PET_ERROR_MESSAGE, PET_SIZES } from '@/constants/petProfile'; +import { usePetProfileEdition } from '@/hooks/petProfile/usePetProfileEdition'; +import { usePetProfileValidation } from '@/hooks/petProfile/usePetProfileValidation'; + +import PetAgeSelect from '../PetAgeSelect'; +import PetInfoInForm from '../PetInfoInForm'; + +const PetProfileEditionForm = () => { + const { isMixedBreed } = usePetProfileValidation(); + const { + pet, + isValidForm, + isValidNameInput, + isValidAgeSelect, + isValidWeightInput, + onChangeName, + onChangeAge, + onChangeWeight, + onChangePetSize, + onChangeImage, + onSubmitNewPetProfile, + onClickRemoveButton, + } = usePetProfileEdition(); + + return ( +
+ {pet && ( + <> + + + + + +
+ 이름 입력 + + {isValidNameInput ? '' : PET_ERROR_MESSAGE.INVALID_NAME} +
+
+ 나이 선택 + + {isValidAgeSelect ? '' : '나이를 선택해주세요!'} +
+
+ 몸무게 입력 + + + kg + + + {isValidWeightInput ? '' : PET_ERROR_MESSAGE.INVALID_WEIGHT} + +
+ {isMixedBreed(pet.breed) && ( +
+ 크기 선택 + + {PET_SIZES.map(size => ( + +
+ )} +
+ + + + + 수정 + + { + onClickRemoveButton(pet.id); + }} + > + + 삭제 + + + + )} +
+ ); +}; + +export default PetProfileEditionForm; + +const FormContainer = styled.form` + display: flex; + flex-direction: column; + gap: 2rem; + + padding: 2rem; +`; + +const PetInfoWrapper = styled.div` + margin-bottom: 2rem; +`; + +const InputLabel = styled.label` + display: block; + + margin-bottom: 0.4rem; + + font-size: 1.3rem; + font-weight: 500; + line-height: 1.7rem; + color: ${({ theme }) => theme.color.grey600}; + letter-spacing: -0.5px; +`; + +const ErrorCaption = styled.p` + min-height: 1.7rem; + margin-top: 1rem; + + font-size: 1.3rem; + font-weight: 500; + line-height: 1.7rem; + color: ${({ theme }) => theme.color.warning}; + letter-spacing: -0.5px; +`; + +const WeightInputContainer = styled.div` + position: relative; +`; + +const Kg = styled.p` + position: absolute; + top: 1.2rem; + right: 1.2rem; + + font-size: 1.3rem; + font-weight: 500; + line-height: 1.7rem; + color: ${({ theme }) => theme.color.grey600}; + letter-spacing: -0.5px; +`; + +const PetSizeContainer = styled.div` + cursor: pointer; + + display: flex; + gap: 0.8rem; + align-items: center; + + margin-top: 1.2rem; +`; + +const ButtonContainer = styled.div` + position: fixed; + bottom: 4rem; + left: 0; + + display: flex; + gap: 1.6rem; + + width: 100%; + padding: 0 2rem; +`; + +const BasicButton = styled.button<{ $isEditButton: boolean }>` + cursor: pointer; + + display: flex; + align-items: center; + justify-content: center; + + width: ${({ $isEditButton }) => ($isEditButton ? '70%' : '30%')}; + height: 5.1rem; + + font-size: 1.6rem; + font-weight: 700; + line-height: 2.4rem; + color: ${({ theme }) => theme.color.white}; + letter-spacing: 0.02rem; + + background-color: ${({ $isEditButton, theme }) => + $isEditButton ? theme.color.primary : '#E73846'}; + border: none; + border-radius: 16px; + + transition: all 100ms ease-in-out; + + &:active { + scale: 0.98; + } + + &:disabled { + cursor: not-allowed; + + background-color: ${({ theme }) => theme.color.grey300}; + } +`; + +const EditIconImage = styled.img` + margin-right: 0.4rem; + margin-bottom: 0.2rem; +`; diff --git a/frontend/src/components/PetProfile/PetProfileImageUploader.tsx b/frontend/src/components/PetProfile/PetProfileImageUploader.tsx index d9be41a34..2a28133a4 100644 --- a/frontend/src/components/PetProfile/PetProfileImageUploader.tsx +++ b/frontend/src/components/PetProfile/PetProfileImageUploader.tsx @@ -2,11 +2,11 @@ import { useEffect } from 'react'; import { styled } from 'styled-components'; import CameraIcon from '@/assets/svg/camera_icon.svg'; -import { usePetProfileContext } from '@/context/petProfile'; +import { usePetAdditionContext } from '@/context/petProfile/PetAdditionContext'; import { useImageUpload } from '@/hooks/common/useImageUpload'; const PetProfileImageUploader = () => { - const { updatePetProfile } = usePetProfileContext(); + const { updatePetProfile } = usePetAdditionContext(); const { previewImage, imageUrl, uploadImage } = useImageUpload(); useEffect(() => { diff --git a/frontend/src/constants/petProfile.ts b/frontend/src/constants/petProfile.ts index b77b5d6f5..c2635b78f 100644 --- a/frontend/src/constants/petProfile.ts +++ b/frontend/src/constants/petProfile.ts @@ -29,3 +29,8 @@ export const STEP_PATH: Record = { [PET_PROFILE_ADDITION_STEP.WEIGHT]: PATH.PET_PROFILE_WEIGHT_ADDITION, [PET_PROFILE_ADDITION_STEP.IMAGE_FILE]: PATH.PET_PROFILE_IMAGE_FILE_ADDITION, } as const; + +export const PET_ERROR_MESSAGE = { + INVALID_NAME: '아이의 이름은 1~10글자 사이의 한글, 영어, 숫자만 입력 가능합니다.', + INVALID_WEIGHT: '몸무게는 0kg초과, 100kg이하 소수점 첫째짜리까지 입력이 가능합니다.', +} as const; diff --git a/frontend/src/context/petProfile.tsx b/frontend/src/context/petProfile/PetAdditionContext.tsx similarity index 51% rename from frontend/src/context/petProfile.tsx rename to frontend/src/context/petProfile/PetAdditionContext.tsx index 282230c5a..1a5596d19 100644 --- a/frontend/src/context/petProfile.tsx +++ b/frontend/src/context/petProfile/PetAdditionContext.tsx @@ -1,16 +1,14 @@ -import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; +import { createContext, PropsWithChildren, useContext, useMemo, useState } from 'react'; -import { PetProfile, PetSize } from '@/types/petProfile/client'; +import { PostPetReq } from '@/types/petProfile/remote'; -interface PetProfileContext { +export interface PetProfileValue extends PostPetReq {} + +interface PetAdditionContext { petProfile: PetProfileValue; updatePetProfile: (newProfile: Partial) => void; } -interface PetProfileValue extends Omit { - petSize?: PetSize; -} - const initialPetProfile: PetProfileValue = { name: '', age: 0, @@ -20,21 +18,21 @@ const initialPetProfile: PetProfileValue = { imageUrl: '', }; -const PetProfileContext = createContext({ +const PetAdditionContext = createContext({ petProfile: initialPetProfile, updatePetProfile: () => {}, }); -export const usePetProfileContext = () => useContext(PetProfileContext); +export const usePetAdditionContext = () => useContext(PetAdditionContext); -export const PetProfileProvider = ({ children }: { children: ReactNode }) => { +export const PetAdditionProvider = ({ children }: PropsWithChildren) => { const [petProfile, setPetProfile] = useState(initialPetProfile); const updatePetProfile = (newProfile: Partial) => { setPetProfile(prev => ({ ...prev, ...newProfile })); }; - const petProfileContextValue = useMemo( + const PetAdditionContextValue = useMemo( () => ({ petProfile, updatePetProfile, @@ -43,8 +41,8 @@ export const PetProfileProvider = ({ children }: { children: ReactNode }) => { ); return ( - + {children} - + ); }; diff --git a/frontend/src/hooks/petProfile.ts b/frontend/src/hooks/petProfile.ts deleted file mode 100644 index 367775632..000000000 --- a/frontend/src/hooks/petProfile.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ChangeEvent, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { - FEMALE, - MALE, - MIXED_BREED, - PET_AGE_MAX, - PET_AGE_MIN, - PET_PROFILE_ADDITION_STEP, - STEP_PATH, -} from '@/constants/petProfile'; -import { routerPath } from '@/router/routes'; -import { Gender } from '@/types/petProfile/client'; - -export const usePetProfileStep = () => { - const navigate = useNavigate(); - - const [step, setStep] = useState(0); - const [isValidStep, setIsValidStep] = useState(false); - const [isMixedBreed, setIsMixedBreed] = useState(false); - - const totalStep = Object.values(PET_PROFILE_ADDITION_STEP).length; - const isLastStep = step === totalStep; - - const goBack = (): void => navigate(routerPath.back); - const goNext = () => { - if (step === PET_PROFILE_ADDITION_STEP.BREED && !isMixedBreed) { - navigate(STEP_PATH[step + 2]); - return; - } - - if (!isLastStep) navigate(STEP_PATH[step + 1]); - }; - - const updateIsMixedBreed = (isMixed: boolean) => setIsMixedBreed(isMixed); - const updateCurrentStep = (step: number) => setStep(step); - const updateIsValidStep = (isValid: boolean) => setIsValidStep(isValid); - - return { - step, - totalStep, - isLastStep, - isValidStep, - updateIsMixedBreed, - updateCurrentStep, - updateIsValidStep, - goBack, - goNext, - }; -}; - -export const usePetProfileValidation = () => { - const [isValidNameInput, setIsValidNameInput] = useState(true); - const [isValidAgeSelect, setIsValidAgeSelect] = useState(true); - const [isValidWeightInput, setIsValidWeightInput] = useState(true); - - const validateName = (e: ChangeEvent) => { - const petName = e.target.value; - - if (isValidName(petName)) { - setIsValidNameInput(true); - return true; - } - - setIsValidNameInput(false); - return false; - }; - - const validateAge = (e: ChangeEvent) => { - const petAge = Number(e.target.value); - - if (isValidAgeRange(petAge)) { - setIsValidWeightInput(true); - return true; - } - - setIsValidAgeSelect(false); - return false; - }; - - const validateWeight = (e: ChangeEvent) => { - const petWeight = e.target.value; - - if (isValidWeight(petWeight)) { - setIsValidWeightInput(true); - return true; - } - - setIsValidWeightInput(false); - return false; - }; - - const isValidName = (name: string) => { - const validNameCharacters = /^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]{1,10}$/; - - return validNameCharacters.test(name); - }; - - const isValidAgeRange = (age: number) => - typeof age === 'number' && age >= PET_AGE_MIN && age <= PET_AGE_MAX; - - const isValidGender = (gender: string): gender is Gender => gender === MALE || gender === FEMALE; - - const isValidWeight = (weight: string) => { - const validWeightCharacters = /^(?:100(?:\.0)?|\d{1,2}(?:\.\d)?)$/; // 100.0 또는 1~2자리 숫자.소수 첫째짜리 숫자 - - return validWeightCharacters.test(weight); - }; - - const isMixedBreed = (breed: string) => breed === MIXED_BREED; - - return { - isValidNameInput, - isValidAgeSelect, - isValidWeightInput, - - validateName, - validateAge, - validateWeight, - isValidName, - isValidAgeRange, - isValidGender, - isValidWeight, - isMixedBreed, - }; -}; diff --git a/frontend/src/hooks/petProfile/usePetProfileAddition.ts b/frontend/src/hooks/petProfile/usePetProfileAddition.ts new file mode 100644 index 000000000..692020a68 --- /dev/null +++ b/frontend/src/hooks/petProfile/usePetProfileAddition.ts @@ -0,0 +1,106 @@ +import { ChangeEvent, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; + +import { usePetAdditionContext } from '@/context/petProfile/PetAdditionContext'; +import { useAddPetMutation } from '@/hooks/query/petProfile'; +import { PetAdditionOutletContextProps, PetSize } from '@/types/petProfile/client'; + +import useEasyNavigate from '../@common/useEasyNavigate'; +import { usePetProfileValidation } from './usePetProfileValidation'; + +export const usePetProfileAddition = () => { + const { goHome } = useEasyNavigate(); + const { addPetMutation } = useAddPetMutation(); + const { isValidAgeRange, isValidGender, isValidName, isValidWeight } = usePetProfileValidation(); + + const { petProfile, updatePetProfile } = usePetAdditionContext(); + const { updateIsValidStep } = useOutletContext(); + + const [isValidInput, setIsValidInput] = useState(true); + + const onChangeAge = (e: ChangeEvent) => { + const selectedAge = Number(e.target.value); + + if (isValidAgeRange(selectedAge)) { + setIsValidInput(true); + updateIsValidStep(true); + updatePetProfile({ age: selectedAge }); + + return; + } + + setIsValidInput(false); + updateIsValidStep(false); + }; + + const onChangeBreed = (e: ChangeEvent) => { + const selectedBreed = e.target.value; + + updateIsValidStep(true); + setIsValidInput(true); + updatePetProfile({ breed: selectedBreed }); + }; + + const onChangeGender = (e: ChangeEvent) => { + const gender = e.target.value; + + if (isValidGender(gender)) { + updateIsValidStep(true); + updatePetProfile({ gender }); + } + }; + + const onChangeName = (e: ChangeEvent) => { + const petName = e.target.value; + + if (isValidName(petName)) { + setIsValidInput(true); + updateIsValidStep(true); + updatePetProfile({ name: petName }); + + return; + } + + setIsValidInput(false); + updateIsValidStep(false); + }; + + const onClickPetSize = (petSize: PetSize) => { + updateIsValidStep(true); + updatePetProfile({ petSize }); + }; + + const onChangeWeight = (e: ChangeEvent) => { + const petWeight = e.target.value; + + if (isValidWeight(petWeight)) { + setIsValidInput(true); + updateIsValidStep(true); + updatePetProfile({ weight: Number(petWeight) }); + + return; + } + + setIsValidInput(false); + updateIsValidStep(false); + }; + + const onSubmitPetProfile = () => { + addPetMutation.addPet(petProfile); + + goHome(); + }; + + return { + isValidInput, + petProfile, + updateIsValidStep, + onChangeAge, + onChangeBreed, + onChangeGender, + onChangeName, + onClickPetSize, + onChangeWeight, + onSubmitPetProfile, + }; +}; diff --git a/frontend/src/hooks/petProfile/usePetProfileEdition.ts b/frontend/src/hooks/petProfile/usePetProfileEdition.ts new file mode 100644 index 000000000..f7207f0e7 --- /dev/null +++ b/frontend/src/hooks/petProfile/usePetProfileEdition.ts @@ -0,0 +1,120 @@ +import { ChangeEvent, useState } from 'react'; + +import { PetProfile, PetSize } from '@/types/petProfile/client'; + +import useEasyNavigate from '../@common/useEasyNavigate'; +import { useValidParams } from '../@common/useValidParams'; +import { useEditPetMutation, usePetItemQuery, useRemovePetMutation } from '../query/petProfile'; +import { usePetProfileValidation } from './usePetProfileValidation'; + +interface PetInput extends Omit { + weight: number | string; +} + +export const usePetProfileEdition = () => { + const { goHome } = useEasyNavigate(); + const { isValidName, isValidAgeRange, isValidWeight } = usePetProfileValidation(); + + const { petId } = useValidParams(['petId']); + const { petItem } = usePetItemQuery({ petId: Number(petId) }); + const { editPetMutation } = useEditPetMutation(); + const { removePetMutation } = useRemovePetMutation(); + + const [pet, setPet] = useState(petItem); + const [isValidNameInput, setIsValidNameInput] = useState(true); + const [isValidAgeSelect, setIsValidAgeSelect] = useState(true); + const [isValidWeightInput, setIsValidWeightInput] = useState(true); + const isValidForm = isValidNameInput && isValidAgeSelect && isValidWeightInput; + + const onChangeName = (e: ChangeEvent) => { + const petName = e.target.value; + + setPet(prev => { + if (!prev) return prev; + + return { ...prev, name: petName }; + }); + + setIsValidNameInput(isValidName(petName)); + }; + + const onChangeAge = (e: ChangeEvent) => { + const petAge = Number(e.target.value); + + if (isValidAgeRange(petAge)) { + setIsValidAgeSelect(true); + setPet(prev => { + if (!prev) return prev; + + return { ...prev, age: petAge }; + }); + + return; + } + + setIsValidAgeSelect(false); + }; + + const onChangeWeight = (e: ChangeEvent) => { + const petWeight = e.target.value; + + setPet(prev => { + if (!prev) return prev; + + return { ...prev, weight: petWeight }; + }); + + if (isValidWeight(petWeight)) setIsValidWeightInput(true); + if (!isValidWeight(petWeight)) setIsValidWeightInput(false); + }; + + const onChangePetSize = (petSize: PetSize) => { + setPet(prev => { + if (!prev) return prev; + + return { ...prev, petSize }; + }); + }; + + const onChangeImage = (imageUrl: string) => { + setPet(prev => { + if (!prev) return prev; + + return { ...prev, imageUrl }; + }); + }; + + const onSubmitNewPetProfile = () => { + if (pet && isValidForm) { + const newPetProfile = { + ...pet, + weight: Number(pet.weight), + }; + + editPetMutation.editPet(newPetProfile); + + goHome(); + } + }; + + const onClickRemoveButton = (petId: number) => { + confirm('정말 삭제하시겠어요?') && removePetMutation.removePet({ petId }); + + goHome(); + }; + + return { + pet, + isValidForm, + isValidNameInput, + isValidAgeSelect, + isValidWeightInput, + onChangeName, + onChangeAge, + onChangeWeight, + onChangePetSize, + onChangeImage, + onSubmitNewPetProfile, + onClickRemoveButton, + }; +}; diff --git a/frontend/src/hooks/petProfile/usePetProfileStep.ts b/frontend/src/hooks/petProfile/usePetProfileStep.ts new file mode 100644 index 000000000..efde251c9 --- /dev/null +++ b/frontend/src/hooks/petProfile/usePetProfileStep.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { PET_PROFILE_ADDITION_STEP, STEP_PATH } from '@/constants/petProfile'; +import { usePetAdditionContext } from '@/context/petProfile/PetAdditionContext'; +import { routerPath } from '@/router/routes'; + +import { usePetProfileValidation } from './usePetProfileValidation'; + +export const usePetProfileStep = () => { + const navigate = useNavigate(); + const { isMixedBreed } = usePetProfileValidation(); + const { petProfile } = usePetAdditionContext(); + + const [step, setStep] = useState(0); + const [isValidStep, setIsValidStep] = useState(false); + + const totalStep = Object.values(PET_PROFILE_ADDITION_STEP).length; + const isLastStep = step === totalStep; + + const goBack = (): void => navigate(routerPath.back); + const goNext = () => { + if (step === PET_PROFILE_ADDITION_STEP.BREED && !isMixedBreed(petProfile.breed)) { + navigate(STEP_PATH[step + 2]); + return; + } + + if (!isLastStep) navigate(STEP_PATH[step + 1]); + }; + + const updateCurrentStep = (step: number) => setStep(step); + const updateIsValidStep = (isValid: boolean) => setIsValidStep(isValid); + + return { + step, + totalStep, + isValidStep, + isLastStep, + updateCurrentStep, + updateIsValidStep, + goBack, + goNext, + }; +}; diff --git a/frontend/src/hooks/petProfile/usePetProfileValidation.ts b/frontend/src/hooks/petProfile/usePetProfileValidation.ts new file mode 100644 index 000000000..11e196e23 --- /dev/null +++ b/frontend/src/hooks/petProfile/usePetProfileValidation.ts @@ -0,0 +1,33 @@ +import { FEMALE, MALE, MIXED_BREED, PET_AGE_MAX, PET_AGE_MIN } from '@/constants/petProfile'; +import { Gender } from '@/types/petProfile/client'; + +export const usePetProfileValidation = () => { + const isValidName = (name: string) => { + const validNameCharacters = /^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]{1,10}$/; + + return validNameCharacters.test(name); + }; + + const isValidAgeRange = (age: number) => + typeof age === 'number' && age >= PET_AGE_MIN && age <= PET_AGE_MAX; + + const isValidGender = (gender: string): gender is Gender => gender === MALE || gender === FEMALE; + + const isValidWeight = (weight: string) => { + const validWeightCharacters = /^(?:100(?:\.0)?|\d{1,2}(?:\.\d)?)$/; // 100.0 또는 1~2자리 숫자.소수 첫째짜리 숫자 + + if (Number(weight) <= 0) return false; + + return validWeightCharacters.test(weight); + }; + + const isMixedBreed = (breed: string) => breed === MIXED_BREED; + + return { + isValidName, + isValidAgeRange, + isValidGender, + isValidWeight, + isMixedBreed, + }; +}; diff --git a/frontend/src/hooks/query/petProfile.ts b/frontend/src/hooks/query/petProfile.ts index 6c77a5c4a..23f6983af 100644 --- a/frontend/src/hooks/query/petProfile.ts +++ b/frontend/src/hooks/query/petProfile.ts @@ -1,7 +1,19 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { deletePet, getBreeds, getPet, getPets, postPetProfile, putPet } from '@/apis/petProfile'; +import { deletePet, getBreeds, getPet, getPets, postPet, putPet } from '@/apis/petProfile'; +import { MIXED_BREED, PET_SIZES } from '@/constants/petProfile'; +import { usePetProfile } from '@/context/petProfile/PetProfileContext'; import { Parameter } from '@/types/common/utility'; +import { PetProfile } from '@/types/petProfile/client'; +import { + DeletePetReq, + DeletePetRes, + PostPetReq, + PostPetRes, + PutPetReq, + PutPetRes, +} from '@/types/petProfile/remote'; +import { zipgoLocalStorage } from '@/utils/localStorage'; const QUERY_KEY = { breedList: 'breedList', petItem: 'petItem', petList: 'petList' }; @@ -44,25 +56,89 @@ export const usePetListQuery = () => { }; }; -export const useAddPetProfileMutation = () => { - const { mutateAsync: addPetProfile, ...addPetProfileRestMutation } = useMutation({ - mutationFn: postPetProfile, +export const useAddPetMutation = () => { + const { updatePetProfile: updatePetProfileInHeader } = usePetProfile(); + const { mutate: addPet, ...addPetRestMutation } = useMutation< + PostPetRes, + unknown, + PostPetReq, + unknown + >({ + mutationFn: postPet, + onSuccess: (postPetProfileRes, petProfile, context) => { + const userInfo = zipgoLocalStorage.getUserInfo({ required: true }); + + const petProfileWithId = { + ...petProfile, + id: 1, + petSize: petProfile.breed === MIXED_BREED ? petProfile.petSize : PET_SIZES[0], + } as PetProfile; + + updatePetProfileInHeader(petProfileWithId); + + zipgoLocalStorage.setUserInfo({ ...userInfo, hasPet: true }); + + alert('반려동물 정보 등록이 완료되었습니다.'); + }, + onError: () => { + alert('반려동물 정보 등록에 실패했습니다.'); + }, }); - return { addPetProfileMutation: { addPetProfile, ...addPetProfileRestMutation } }; + return { addPetMutation: { addPet, ...addPetRestMutation } }; }; export const useEditPetMutation = () => { - const { mutateAsync: editPet, ...editPetRestMutation } = useMutation({ + const { resetPetItemQuery } = usePetItemQuery({ petId: 0 }); + const { updatePetProfile: updatePetProfileInHeader } = usePetProfile(); + + const { mutate: editPet, ...editPetRestMutation } = useMutation< + PutPetRes, + unknown, + PutPetReq, + unknown + >({ mutationFn: putPet, + onSuccess: (putPetRes, newPetProfile, context) => { + updatePetProfileInHeader(newPetProfile); + resetPetItemQuery(); + + zipgoLocalStorage.setPetProfile(newPetProfile); + + alert('반려동물 정보 수정이 완료되었습니다.'); + }, + onError: () => { + alert('반려동물 정보 수정에 실패했습니다.'); + }, }); return { editPetMutation: { editPet, ...editPetRestMutation } }; }; export const useRemovePetMutation = () => { - const { mutateAsync: removePet, ...removePetRestMutation } = useMutation({ + const { resetPetItemQuery } = usePetItemQuery({ petId: 0 }); + const { resetPetProfile: resetPetProfileInHeader } = usePetProfile(); + + const { mutate: removePet, ...removePetRestMutation } = useMutation< + DeletePetRes, + unknown, + DeletePetReq, + unknown + >({ mutationFn: deletePet, + onSuccess: (deletePetRes, deletePetReq, context) => { + const userInfo = zipgoLocalStorage.getUserInfo({ required: true }); + + zipgoLocalStorage.setUserInfo({ ...userInfo, hasPet: false }); + + resetPetProfileInHeader(); + resetPetItemQuery(); + + alert('반려동물 정보를 삭제했습니다.'); + }, + onError: () => { + alert('반려동물 정보 삭제에 실패했습니다.'); + }, }); return { removePetMutation: { removePet, ...removePetRestMutation } }; diff --git a/frontend/src/pages/PetProfile/PetProfileAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAddition.tsx similarity index 67% rename from frontend/src/pages/PetProfile/PetProfileAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAddition.tsx index 3dd742f3b..8403aec05 100644 --- a/frontend/src/pages/PetProfile/PetProfileAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAddition.tsx @@ -4,16 +4,14 @@ import { styled } from 'styled-components'; import BackBtnIcon from '@/assets/svg/back_btn.svg'; import Button from '@/components/@common/Button/Button'; import Template from '@/components/@common/Template'; -import { PetProfileProvider } from '@/context/petProfile'; -import { usePetProfileStep } from '@/hooks/petProfile'; +import { usePetProfileStep } from '@/hooks/petProfile/usePetProfileStep'; const PetProfileAddition = () => { const { step, totalStep, - isLastStep, isValidStep, - updateIsMixedBreed, + isLastStep, updateCurrentStep, updateIsValidStep, goBack, @@ -21,32 +19,29 @@ const PetProfileAddition = () => { } = usePetProfileStep(); return ( - - - + ); }; diff --git a/frontend/src/pages/PetProfile/PetProfileAgeAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAgeAddition.tsx similarity index 64% rename from frontend/src/pages/PetProfile/PetProfileAgeAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAgeAddition.tsx index c482d9ca5..3f2b83774 100644 --- a/frontend/src/pages/PetProfile/PetProfileAgeAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileAgeAddition.tsx @@ -1,34 +1,22 @@ -import { ChangeEvent, useEffect } from 'react'; +import { useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import PetAgeSelect from '@/components/PetProfile/PetAgeSelect'; import { PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; const PetProfileAgeAddition = () => { - const { updateCurrentStep, updateIsValidStep } = useOutletContext(); - const { petProfile, updatePetProfile } = usePetProfileContext(); - const { isValidAgeRange } = usePetProfileValidation(); + const { petProfile, onChangeAge } = usePetProfileAddition(); + const { updateIsValidStep, updateCurrentStep } = + useOutletContext(); useEffect(() => { - updateCurrentStep(PET_PROFILE_ADDITION_STEP.AGE); updateIsValidStep(false); + updateCurrentStep(PET_PROFILE_ADDITION_STEP.AGE); }, []); - const onChangeAge = (e: ChangeEvent) => { - const selectedAge = Number(e.target.value); - - if (isValidAgeRange(selectedAge)) { - updateIsValidStep(true); - updatePetProfile({ age: selectedAge }); - } - - if (!isValidAgeRange(selectedAge)) updateIsValidStep(false); - }; - return ( diff --git a/frontend/src/pages/PetProfile/PetProfileBreedAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileBreedAddition.tsx similarity index 64% rename from frontend/src/pages/PetProfile/PetProfileBreedAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileBreedAddition.tsx index 88b211cf1..de75fcf85 100644 --- a/frontend/src/pages/PetProfile/PetProfileBreedAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileBreedAddition.tsx @@ -1,35 +1,23 @@ -import { ChangeEvent, useEffect } from 'react'; +import { useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import PetBreedSelect from '@/components/PetProfile/PetBreedSelect'; import { PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; import { getTopicParticle } from '@/utils/getTopicParticle'; const PetProfileBreedAddition = () => { - const { petProfile, updatePetProfile } = usePetProfileContext(); - const { updateIsMixedBreed, updateCurrentStep, updateIsValidStep } = - useOutletContext<PetProfileOutletContextProps>(); - const { isMixedBreed } = usePetProfileValidation(); + const { petProfile, onChangeBreed } = usePetProfileAddition(); + const { updateCurrentStep, updateIsValidStep } = + useOutletContext<PetAdditionOutletContextProps>(); useEffect(() => { - updateCurrentStep(PET_PROFILE_ADDITION_STEP.BREED); updateIsValidStep(false); + updateCurrentStep(PET_PROFILE_ADDITION_STEP.BREED); }, []); - const onChangeBreed = (e: ChangeEvent<HTMLSelectElement>) => { - const selectedBreed = e.target.value; - - if (isMixedBreed(selectedBreed)) updateIsMixedBreed(true); - if (!isMixedBreed(selectedBreed)) updateIsMixedBreed(false); - - updateIsValidStep(true); - updatePetProfile({ breed: selectedBreed }); - }; - return ( <Container> <PetName>{petProfile.name}</PetName> diff --git a/frontend/src/pages/PetProfile/PetProfileGenderAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileGenderAddition.tsx similarity index 73% rename from frontend/src/pages/PetProfile/PetProfileGenderAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileGenderAddition.tsx index 4e3e5fcda..40863d581 100644 --- a/frontend/src/pages/PetProfile/PetProfileGenderAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileGenderAddition.tsx @@ -1,28 +1,20 @@ -import { ChangeEvent, useEffect } from 'react'; +import { useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import GenderRadioInput from '@/components/PetProfile/GenderRadioInput'; import { GENDERS, PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; const PetProfileGenderAddition = () => { - const { updateCurrentStep } = useOutletContext<PetProfileOutletContextProps>(); - const { petProfile, updatePetProfile } = usePetProfileContext(); - const { isValidGender } = usePetProfileValidation(); + const { petProfile, onChangeGender } = usePetProfileAddition(); + const { updateCurrentStep } = useOutletContext<PetAdditionOutletContextProps>(); useEffect(() => { updateCurrentStep(PET_PROFILE_ADDITION_STEP.GENDER); }, []); - const onChangeGender = (e: ChangeEvent<HTMLInputElement>) => { - const gender = e.target.value; - - if (isValidGender(gender)) updatePetProfile({ gender }); - }; - return ( <Container> <PetName>{petProfile.name}</PetName> diff --git a/frontend/src/pages/PetProfile/PetProfileImageAdditionContent.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileImageAddition.tsx similarity index 51% rename from frontend/src/pages/PetProfile/PetProfileImageAdditionContent.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileImageAddition.tsx index e4cee4134..f311849c7 100644 --- a/frontend/src/pages/PetProfile/PetProfileImageAdditionContent.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileImageAddition.tsx @@ -1,54 +1,21 @@ import { useEffect } from 'react'; -import { useNavigate, useOutletContext } from 'react-router-dom'; +import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import PetProfileImageUploader from '@/components/PetProfile/PetProfileImageUploader'; import { PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfile } from '@/context/petProfile/PetProfileContext'; -import { useAddPetProfileMutation, useBreedListQuery } from '@/hooks/query/petProfile'; -import { routerPath } from '@/router/routes'; -import { PetProfile, PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; import { getTopicParticle } from '@/utils/getTopicParticle'; -import { zipgoLocalStorage } from '@/utils/localStorage'; -const PetProfileImageAdditionContent = () => { - const navigate = useNavigate(); - const { petProfile } = usePetProfileContext(); - const { updateCurrentStep } = useOutletContext<PetProfileOutletContextProps>(); - const { addPetProfileMutation } = useAddPetProfileMutation(); - const { breedList } = useBreedListQuery(); - const { updatePetProfile } = usePetProfile(); // 에디가 만든 context +const PetProfileImageAddition = () => { + const { petProfile, onSubmitPetProfile } = usePetProfileAddition(); + const { updateCurrentStep } = useOutletContext<PetAdditionOutletContextProps>(); useEffect(() => { updateCurrentStep(PET_PROFILE_ADDITION_STEP.IMAGE_FILE); }, [updateCurrentStep]); - const onSubmitPetProfile = () => { - addPetProfileMutation - .addPetProfile(petProfile) - .then(async res => { - const userInfo = zipgoLocalStorage.getUserInfo({ required: true }); - - const userPetBreed = breedList?.find(breed => breed.name === petProfile.breed); - const petProfileWithId = { - ...petProfile, - id: 1, - petSize: userPetBreed?.name === '믹스견' ? petProfile.petSize : undefined, - } as PetProfile; - - updatePetProfile(petProfileWithId); // 헤더 유저 프로필 정보 업데이트 - zipgoLocalStorage.setUserInfo({ ...userInfo, hasPet: true }); - - alert('반려동물 정보 등록이 완료되었습니다.'); - }) - .catch(error => { - alert('반려동물 정보 등록에 실패했습니다.'); - }); - - navigate(routerPath.home()); - }; - return ( <Container> <PetName>{petProfile.name}</PetName> @@ -63,7 +30,7 @@ const PetProfileImageAdditionContent = () => { ); }; -export default PetProfileImageAdditionContent; +export default PetProfileImageAddition; const Container = styled.div` margin-top: 4rem; diff --git a/frontend/src/pages/PetProfile/PetProfileNameAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileNameAddition.tsx similarity index 59% rename from frontend/src/pages/PetProfile/PetProfileNameAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileNameAddition.tsx index a5d1ccee4..1a8fe68a5 100644 --- a/frontend/src/pages/PetProfile/PetProfileNameAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileNameAddition.tsx @@ -1,38 +1,22 @@ -import { ChangeEvent, useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import Input from '@/components/@common/Input/Input'; import { PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; const PetProfileNameAddition = () => { - const [isValidInput, setIsValidInput] = useState(true); - const { updateCurrentStep, updateIsValidStep } = useOutletContext<PetProfileOutletContextProps>(); - const { updatePetProfile } = usePetProfileContext(); - const { isValidName } = usePetProfileValidation(); + const { isValidInput, onChangeName } = usePetProfileAddition(); + const { updateCurrentStep, updateIsValidStep } = + useOutletContext<PetAdditionOutletContextProps>(); useEffect(() => { + updateIsValidStep(false); updateCurrentStep(PET_PROFILE_ADDITION_STEP.NAME); }, []); - const onChangeName = (e: ChangeEvent<HTMLInputElement>) => { - const petName = e.target.value; - - if (isValidName(petName)) { - setIsValidInput(true); - updateIsValidStep(true); - updatePetProfile({ name: petName }); - } - - if (!isValidName(petName)) { - updateIsValidStep(false); - setIsValidInput(false); - } - }; - return ( <Container> <Title>아이의 이름이 무엇인가요? @@ -49,11 +33,9 @@ const PetProfileNameAddition = () => { design="underline" fontSize="1.3rem" /> - {!isValidInput && ( - - 아이의 이름은 1~10글자 사이의 한글, 영어, 숫자만 입력 가능합니다. - - )} + + {isValidInput ? '' : '아이의 이름은 1~10글자 사이의 한글, 영어, 숫자만 입력 가능합니다.'} + ); }; diff --git a/frontend/src/pages/PetProfile/PetProfilePetSizeAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfilePetSizeAddition.tsx similarity index 82% rename from frontend/src/pages/PetProfile/PetProfilePetSizeAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfilePetSizeAddition.tsx index fc2b5423d..3a6ce9e38 100644 --- a/frontend/src/pages/PetProfile/PetProfilePetSizeAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfilePetSizeAddition.tsx @@ -4,19 +4,19 @@ import { styled } from 'styled-components'; import Label from '@/components/@common/Label/Label'; import { PET_PROFILE_ADDITION_STEP, PET_SIZES } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { PetProfileOutletContextProps, PetSize } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; const PetProfilePetSizeAddition = () => { - const { updateCurrentStep } = useOutletContext(); - const { petProfile, updatePetProfile } = usePetProfileContext(); + const { petProfile, onClickPetSize } = usePetProfileAddition(); + const { updateCurrentStep, updateIsValidStep } = + useOutletContext(); useEffect(() => { + updateIsValidStep(false); updateCurrentStep(PET_PROFILE_ADDITION_STEP.PET_SIZE); }, []); - const onClickPetSize = (size: PetSize) => updatePetProfile({ petSize: size }); - return ( {petProfile.name} diff --git a/frontend/src/pages/PetProfile/PetProfileWeightAddition.tsx b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileWeightAddition.tsx similarity index 66% rename from frontend/src/pages/PetProfile/PetProfileWeightAddition.tsx rename to frontend/src/pages/PetProfile/PetProfileAddition/PetProfileWeightAddition.tsx index 53af4bb74..7c0ae4a05 100644 --- a/frontend/src/pages/PetProfile/PetProfileWeightAddition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileAddition/PetProfileWeightAddition.tsx @@ -1,39 +1,22 @@ -import { ChangeEvent, useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { styled } from 'styled-components'; import Input from '@/components/@common/Input/Input'; import { PET_PROFILE_ADDITION_STEP } from '@/constants/petProfile'; -import { usePetProfileContext } from '@/context/petProfile'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { PetProfileOutletContextProps } from '@/types/petProfile/client'; +import { usePetProfileAddition } from '@/hooks/petProfile/usePetProfileAddition'; +import { PetAdditionOutletContextProps } from '@/types/petProfile/client'; const PetProfileWeightAddition = () => { - const [isValidInput, setIsValidInput] = useState(true); - const { updateCurrentStep, updateIsValidStep } = useOutletContext(); - const { petProfile, updatePetProfile } = usePetProfileContext(); - const { isValidWeight } = usePetProfileValidation(); + const { petProfile, isValidInput, onChangeWeight } = usePetProfileAddition(); + const { updateCurrentStep, updateIsValidStep } = + useOutletContext(); useEffect(() => { - updateCurrentStep(PET_PROFILE_ADDITION_STEP.WEIGHT); updateIsValidStep(false); + updateCurrentStep(PET_PROFILE_ADDITION_STEP.WEIGHT); }, []); - const onChangeWeight = (e: ChangeEvent) => { - const petWeight = e.target.value; - - if (isValidWeight(petWeight)) { - setIsValidInput(true); - updateIsValidStep(true); - updatePetProfile({ weight: Number(petWeight) }); - } - - if (!isValidWeight(petWeight)) { - updateIsValidStep(false); - setIsValidInput(false); - } - }; - return ( {petProfile.name} @@ -55,11 +38,9 @@ const PetProfileWeightAddition = () => { /> kg - {!isValidInput && ( - - 몸무게는 0kg초과, 100kg이하 소수점 첫째짜리까지 입력이 가능합니다. - - )} + + {isValidInput ? '' : '몸무게는 0kg초과, 100kg이하 소수점 첫째짜리까지 입력이 가능합니다.'} + ); }; diff --git a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.tsx b/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.tsx index 3c2e241ec..20bd3f3a1 100644 --- a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.tsx +++ b/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEdition.tsx @@ -1,5 +1,61 @@ -import PetProfileEditionContent from './PetProfileEditionContent'; +import { useNavigate } from 'react-router-dom'; +import { styled } from 'styled-components'; -const PetProfileEdition = () => ; +import BackBtnIcon from '@/assets/svg/back_btn.svg'; +import Template from '@/components/@common/Template'; +import PetProfileEditionForm from '@/components/PetProfile/PetProfileEditionForm/PetProfileEditionForm'; +import { routerPath } from '@/router/routes'; + +const PetProfileEdition = () => { + const navigate = useNavigate(); + + const goBack = (): void => navigate(routerPath.back); + + return ( + + + + + + + + + ); +}; export default PetProfileEdition; + +const StaticHeader = styled.header` + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + height: 4rem; + padding: 2.8rem; +`; + +const BackButtonWrapper = styled.button` + cursor: pointer; + + position: absolute; + top: 2rem; + left: 0.8rem; + + display: flex; + align-items: center; + justify-content: center; + + width: 4rem; + height: 4rem; + + background-color: transparent; + border: none; +`; + +const BackBtnImage = styled.img` + width: 3.2rem; + height: 3.2rem; + + object-fit: cover; +`; diff --git a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEditionContent.tsx b/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEditionContent.tsx deleted file mode 100644 index edac01212..000000000 --- a/frontend/src/pages/PetProfile/PetProfileEdition/PetProfileEditionContent.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import { ChangeEvent, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { styled } from 'styled-components'; - -import BackBtnIcon from '@/assets/svg/back_btn.svg'; -import EditIconLight from '@/assets/svg/edit_icon_light.svg'; -import TrashCanIcon from '@/assets/svg/trash_can_icon.svg'; -import Input from '@/components/@common/Input/Input'; -import Label from '@/components/@common/Label/Label'; -import Template from '@/components/@common/Template'; -import PetAgeSelect from '@/components/PetProfile/PetAgeSelect'; -import PetInfoInForm from '@/components/PetProfile/PetInfoInForm'; -import { PET_SIZES } from '@/constants/petProfile'; -import { usePetProfile } from '@/context/petProfile/PetProfileContext'; -import { useValidParams } from '@/hooks/@common/useValidParams'; -import { usePetProfileValidation } from '@/hooks/petProfile'; -import { - useEditPetMutation, - usePetItemQuery, - useRemovePetMutation, -} from '@/hooks/query/petProfile'; -import { PATH, routerPath } from '@/router/routes'; -import { PetSize } from '@/types/petProfile/client'; -import { zipgoLocalStorage } from '@/utils/localStorage'; - -const PetProfileEditionContent = () => { - const navigate = useNavigate(); - const { petId } = useValidParams(['petId']); - const { petItem, resetPetItemQuery } = usePetItemQuery({ petId: Number(petId) }); - const { removePetMutation } = useRemovePetMutation(); - const { updatePetProfile, resetPetProfile } = usePetProfile(); - const { - isValidNameInput, - isValidAgeSelect, - isValidWeightInput, - isMixedBreed, - validateName, - validateAge, - validateWeight, - } = usePetProfileValidation(); - - const [petName, setPetName] = useState(petItem?.name); - const [petAge, setPetAge] = useState(String(petItem?.age)); - const [petWeight, setPetWeight] = useState(String(petItem?.weight)); - const [petSize, setPetSize] = useState(petItem?.petSize); - const [petImageUrl, setPetImageUrl] = useState(petItem?.imageUrl); - - const { editPetMutation } = useEditPetMutation(); - - const onClickRemoveButton = (petId: number) => { - confirm('정말 삭제하시겠어요?') && - removePetMutation.removePet({ petId }).then(() => { - const userInfo = zipgoLocalStorage.getUserInfo({ required: true }); - - zipgoLocalStorage.setUserInfo({ ...userInfo, hasPet: false }); - - resetPetProfile(); - - alert('반려동물 정보를 삭제했습니다.'); - }); - - navigate(PATH.HOME); - }; - - const onChangeName = (e: ChangeEvent) => { - validateName(e); - setPetName(e.target.value); - }; - - const onChangeAge = (e: ChangeEvent) => { - if (validateAge(e)) setPetAge(e.target.value); - }; - - const onChangeWeight = (e: ChangeEvent) => { - validateWeight(e); - setPetWeight(e.target.value); - }; - - const onChangePetSize = (petSize: PetSize) => { - setPetSize(petSize); - }; - - const onChangeImage = (imageUrl: string) => { - setPetImageUrl(imageUrl); - }; - - const onSubmit = () => { - if (petItem && isValidNameInput && isValidAgeSelect && isValidWeightInput) { - const newPetProfile = { - ...petItem, - name: petName || petItem.name, - age: Number(petAge) || petItem.age, - weight: Number(petWeight) || petItem.weight, - petSize: petSize || petItem.petSize, - imageUrl: petImageUrl || petItem.imageUrl, - }; - - editPetMutation - .editPet(newPetProfile) - .then(() => { - updatePetProfile(newPetProfile); - resetPetItemQuery(); - alert('반려동물 정보 수정이 완료되었습니다.'); - navigate(PATH.HOME); - }) - .catch(() => { - alert('반려동물 정보 수정에 실패했습니다.'); - navigate(PATH.HOME); - }); - - return; - } - - alert('올바른 정보를 입력해주세요!'); - }; - - const goBack = (): void => navigate(routerPath.back); - - return ( - - - - - - - {petItem && ( - <> - - - - - -
- 이름 입력 - - {!isValidNameInput && ( - - 아이의 이름은 1~10글자 사이의 한글, 영어, 숫자만 입력 가능합니다. - - )} -
-
- 나이 선택 - - {!isValidAgeSelect && 나이를 선택해주세요!} -
-
- 몸무게 입력 - - - kg - - {!isValidWeightInput && ( - - 몸무게는 0kg초과, 100kg이하 소수점 첫째짜리까지 입력이 가능합니다. - - )} -
- {isMixedBreed(petItem.breed) && ( -
- 크기 선택 - - {PET_SIZES.map(size => ( - -
- )} -
- - - - - - 수정 - - { - onClickRemoveButton(petItem.id); - }} - > - - 삭제 - - - - )} -
- ); -}; - -export default PetProfileEditionContent; - -const StaticHeader = styled.header` - display: flex; - align-items: center; - justify-content: center; - - width: 100%; - height: 4rem; - padding: 2.8rem; -`; - -const FormContainer = styled.form` - display: flex; - flex-direction: column; - gap: 2rem; - - padding: 2rem; -`; - -const PetInfoWrapper = styled.div` - margin-bottom: 2rem; -`; - -const BackButtonWrapper = styled.button` - cursor: pointer; - - position: absolute; - top: 2rem; - left: 0.8rem; - - display: flex; - align-items: center; - justify-content: center; - - width: 4rem; - height: 4rem; - - background-color: transparent; - border: none; -`; - -const BackBtnImage = styled.img` - width: 3.2rem; - height: 3.2rem; - - object-fit: cover; -`; - -const ContentLayout = styled.div` - margin: 0 2rem; -`; - -const InputLabel = styled.label` - display: block; - - margin-bottom: 0.4rem; - - font-size: 1.3rem; - font-weight: 500; - line-height: 1.7rem; - color: ${({ theme }) => theme.color.grey600}; - letter-spacing: -0.5px; -`; - -const ErrorCaption = styled.p` - margin-top: 1rem; - - font-size: 1.3rem; - font-weight: 500; - line-height: 1.7rem; - color: ${({ theme }) => theme.color.warning}; - letter-spacing: -0.5px; -`; - -const WeightInputContainer = styled.div` - position: relative; -`; - -const Kg = styled.p` - position: absolute; - top: 1.2rem; - right: 1.2rem; - - font-size: 1.3rem; - font-weight: 500; - line-height: 1.7rem; - color: ${({ theme }) => theme.color.grey600}; - letter-spacing: -0.5px; -`; - -const PetSizeContainer = styled.div` - cursor: pointer; - - display: flex; - gap: 0.8rem; - align-items: center; - - margin-top: 1.2rem; -`; - -const ButtonContainer = styled.div` - position: fixed; - bottom: 4rem; - left: 0; - - display: flex; - gap: 1.6rem; - - width: 100%; - padding: 0 2rem; -`; - -const BasicButton = styled.button<{ $isEditButton: boolean }>` - cursor: pointer; - - display: flex; - align-items: center; - justify-content: center; - - width: ${({ $isEditButton }) => ($isEditButton ? '70%' : '30%')}; - height: 5.1rem; - - font-size: 1.6rem; - font-weight: 700; - line-height: 2.4rem; - color: ${({ theme }) => theme.color.white}; - letter-spacing: 0.02rem; - - background-color: ${({ $isEditButton, theme }) => - $isEditButton ? theme.color.primary : '#E73846'}; - border: none; - border-radius: 16px; - - transition: all 100ms ease-in-out; - - &:active { - scale: 0.98; - } - - &:disabled { - cursor: not-allowed; - - background-color: ${({ theme }) => theme.color.grey300}; - } -`; - -const EditIconImage = styled.img` - margin-right: 0.4rem; - margin-bottom: 0.2rem; -`; diff --git a/frontend/src/pages/PetProfile/PetProfileImageAddition.tsx b/frontend/src/pages/PetProfile/PetProfileImageAddition.tsx deleted file mode 100644 index 8f832826a..000000000 --- a/frontend/src/pages/PetProfile/PetProfileImageAddition.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import PetProfileImageAdditionContent from './PetProfileImageAdditionContent'; - -const PetProfileImageAddition = () => ; - -export default PetProfileImageAddition; diff --git a/frontend/src/router/Router.tsx b/frontend/src/router/Router.tsx index 633dd040d..1b9fd6694 100644 --- a/frontend/src/router/Router.tsx +++ b/frontend/src/router/Router.tsx @@ -3,20 +3,21 @@ import { ThemeProvider } from 'styled-components'; import App from '@/App'; import GlobalStyle from '@/components/@common/GlobalStyle'; +import { PetAdditionProvider } from '@/context/petProfile/PetAdditionContext'; import PetProfileProvider from '@/context/petProfile/PetProfileContext'; import ErrorPage from '@/pages/Error/ErrorPage'; import FoodDetail from '@/pages/FoodDetail/FoodDetail'; import Landing from '@/pages/Landing/Landing'; import Login from '@/pages/Login/Login'; -import PetProfileAddition from '@/pages/PetProfile/PetProfileAddition'; -import PetProfileAgeAddition from '@/pages/PetProfile/PetProfileAgeAddition'; -import PetProfileBreedAddition from '@/pages/PetProfile/PetProfileBreedAddition'; +import PetProfileAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileAddition'; +import PetProfileAgeAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileAgeAddition'; +import PetProfileBreedAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileBreedAddition'; +import PetProfileGenderAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileGenderAddition'; +import PetProfileImageAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileImageAddition'; +import PetProfileNameAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileNameAddition'; +import PetProfilePetSizeAddition from '@/pages/PetProfile/PetProfileAddition/PetProfilePetSizeAddition'; +import PetProfileWeightAddition from '@/pages/PetProfile/PetProfileAddition/PetProfileWeightAddition'; import PetProfileEdition from '@/pages/PetProfile/PetProfileEdition/PetProfileEdition'; -import PetProfileGenderAddition from '@/pages/PetProfile/PetProfileGenderAddition'; -import PetProfileImageAddition from '@/pages/PetProfile/PetProfileImageAddition'; -import PetProfileNameAddition from '@/pages/PetProfile/PetProfileNameAddition'; -import PetProfilePetSizeAddition from '@/pages/PetProfile/PetProfilePetSizeAddition'; -import PetProfileWeightAddition from '@/pages/PetProfile/PetProfileWeightAddition'; import ReviewAddition from '@/pages/ReviewAddition/ReviewAddition'; import ReviewStarRating from '@/pages/ReviewStarRating/ReviewStarRating'; import theme from '@/styles/theme'; @@ -55,7 +56,11 @@ export const router = createBrowserRouter([ }, { path: PATH.PET_PROFILE_ADDITION, - element: , + element: ( + + + + ), children: [ { index: true, diff --git a/frontend/src/types/petProfile/client.ts b/frontend/src/types/petProfile/client.ts index 0637051ee..067e861c6 100644 --- a/frontend/src/types/petProfile/client.ts +++ b/frontend/src/types/petProfile/client.ts @@ -20,10 +20,9 @@ type PetSize = (typeof PET_SIZES)[number]; type Gender = (typeof GENDERS)[number]; -interface PetProfileOutletContextProps { - updateIsMixedBreed: (isMixed: boolean) => void; +interface PetAdditionOutletContextProps { updateCurrentStep: (step: number) => void; updateIsValidStep: (isValid: boolean) => void; } -export type { Breed, Gender, PetProfile, PetProfileOutletContextProps, PetSize }; +export type { Breed, Gender, PetAdditionOutletContextProps, PetProfile, PetSize }; diff --git a/frontend/src/types/petProfile/remote.ts b/frontend/src/types/petProfile/remote.ts index 94a564337..31952e5e1 100644 --- a/frontend/src/types/petProfile/remote.ts +++ b/frontend/src/types/petProfile/remote.ts @@ -16,11 +16,11 @@ interface GetPetsRes { pets: PetProfile[]; } -interface PostPetProfileReq extends Omit { +interface PostPetReq extends Omit { petSize?: PetSize; } -interface PostPetProfileRes {} +interface PostPetRes {} interface PutPetReq extends PetProfile {} @@ -38,8 +38,8 @@ export type { GetPetRes, GetPetsReq, GetPetsRes, - PostPetProfileReq, - PostPetProfileRes, + PostPetReq, + PostPetRes, PutPetReq, PutPetRes, };