diff --git a/apis/data-contracts.ts b/apis/data-contracts.ts index feb4d50b..365231db 100644 --- a/apis/data-contracts.ts +++ b/apis/data-contracts.ts @@ -76,7 +76,6 @@ export interface UpdateReservationRequest { /** * 운영 시간 (형식: HH:MM ~ HH:MM) * @minLength 1 - * @pattern ^([01]\d|2[0-3]):([0-5]\d) ~ ([01]\d|2[0-3]):([0-5]\d)$ * @example "15:00 ~ 16:00" */ operationHour: string; diff --git a/apis/http-client.ts b/apis/http-client.ts index 0649301a..8932f811 100644 --- a/apis/http-client.ts +++ b/apis/http-client.ts @@ -74,7 +74,7 @@ export class HttpClient { }: ApiConfig = {}) { this.instance = axios.create({ ...axiosConfig, - baseURL: axiosConfig.baseURL || "http://52.79.221.101:8000", + baseURL: axiosConfig.baseURL || "http://13.125.207.84:8000", }); this.secure = secure; this.format = format; diff --git a/package.json b/package.json index 3b21ca54..6e34c5d3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "chromatic": "npx chromatic --project-token=chpt_b45a4ae4e58f49f", - "swagger-typescript-api": "swagger-typescript-api generate -p http://52.79.221.101:8000/v3/api-docs -r -o ./apis --modular -d --extract-request-body --extract-response-body --extract-response-error --axios --clean-output", + "swagger-typescript-api": "swagger-typescript-api generate -p http://13.125.207.84:8000/v3/api-docs -r -o ./apis --modular -d --extract-request-body --extract-response-body --extract-response-error --axios --clean-output", "update-icon-ids": "tsx src/shared/utils/extract-icon-ids.ts" }, "dependencies": { diff --git a/src/pages/@owner/estimate/hooks/use-estimate-form.ts b/src/pages/@owner/estimate/hooks/use-estimate-form.ts index 98ee82a4..1e26a965 100644 --- a/src/pages/@owner/estimate/hooks/use-estimate-form.ts +++ b/src/pages/@owner/estimate/hooks/use-estimate-form.ts @@ -6,7 +6,7 @@ import type { } from 'apis/data-contracts'; import { zodResolver } from '@hookform/resolvers/zod'; -import { formatEstimateDatesToAvailableDates } from '@utils/date'; +import { formatStringDatesToAvailableDates } from '@utils/date'; import { estimateSchema, type EstimateFormData, @@ -67,7 +67,7 @@ export const useEstimateForm = ( { location: estimateData.address ?? '', detailLocation: estimateData.detailAddress ?? '', - availableDates: formatEstimateDatesToAvailableDates( + availableDates: formatStringDatesToAvailableDates( estimateData.reservationDates ?? [] ), activeTime: estimateData.operationHour ?? undefined, diff --git a/src/pages/@owner/estimate/utils/format-estimate.ts b/src/pages/@owner/estimate/utils/format-estimate.ts index ebf8648e..ab46f3ba 100644 --- a/src/pages/@owner/estimate/utils/format-estimate.ts +++ b/src/pages/@owner/estimate/utils/format-estimate.ts @@ -2,6 +2,7 @@ import type { CreateReservationRequest, UpdateReservationRequest, } from 'apis/data-contracts'; +import { formatAvailableDatesToString } from '@utils/date'; import type { EstimateFormData } from '@pages/@owner/estimate/schemas/estimate.schema'; // 예약 견적서 작성 요청 body 형식으로 포맷 @@ -11,22 +12,15 @@ export const formatCreateEstimate = ( reservationUserId: number, estimateFormData: EstimateFormData ) => { - const formattedDates = estimateFormData.availableDates - .filter(date => date.startDate) - .map(date => { - if (date.endDate) { - return `${date.startDate} ~ ${date.endDate}`; - } - return `${date.startDate} ~ ${date.startDate}`; - }); - const formattedEstimate: CreateReservationRequest = { foodTruckId, chatRoomId, reservationUserId, address: estimateFormData.location, detailAddress: estimateFormData.detailLocation, - reservationDates: formattedDates, + reservationDates: formatAvailableDatesToString( + estimateFormData.availableDates + ), operationHour: estimateFormData.activeTime, menu: estimateFormData.food, deposit: estimateFormData.price, @@ -39,19 +33,12 @@ export const formatCreateEstimate = ( // 예약 견적서 수정 요청 body 형식으로 포맷 export const formatUpdateEstimate = (estimateFormData: EstimateFormData) => { - const formattedDates = estimateFormData.availableDates - .filter(date => date.startDate) - .map(date => { - if (date.endDate) { - return `${date.startDate} ~ ${date.endDate}`; - } - return `${date.startDate} ~ ${date.startDate}`; - }); - const formattedEstimate: UpdateReservationRequest = { address: estimateFormData.location, detailAddress: estimateFormData.detailLocation, - reservationDates: formattedDates, + reservationDates: formatAvailableDatesToString( + estimateFormData.availableDates + ), operationHour: estimateFormData.activeTime, menu: estimateFormData.food, deposit: estimateFormData.price, diff --git a/src/pages/@owner/food-truck-form/@section/basic-info-section/FoodTruckName.tsx b/src/pages/@owner/food-truck-form/@section/basic-info-section/FoodTruckName.tsx index e2bc585a..a5a2c803 100644 --- a/src/pages/@owner/food-truck-form/@section/basic-info-section/FoodTruckName.tsx +++ b/src/pages/@owner/food-truck-form/@section/basic-info-section/FoodTruckName.tsx @@ -6,15 +6,22 @@ import Button from '@ui/button/Button'; import ErrorText from '@form/error-text/ErrorText'; import { Icon } from '@icon/Icon'; -export default function FoodTruckNameInput() { +interface FoodTruckNameInputProps { + previousName?: string; +} + +export default function FoodTruckNameInput({ + previousName, +}: FoodTruckNameInputProps) { const { name, nameError, updateName, checkNameDuplicated, canCheckNameDuplicate, - nameDuplicate, - } = useBasicInfo(); + isNameDuplicated, + isNameChecked, + } = useBasicInfo(previousName); return ( updateName(e.target.value)} rightComponent={ } + handleRightClick={ + canCheckNameDuplicate ? checkNameDuplicated : undefined + } /> {nameError && } - {nameDuplicate && ( + {isNameChecked && !isNameDuplicated && (

사용 가능한 이름입니다.

diff --git a/src/pages/@owner/food-truck-form/@section/region-section/RegionSection.tsx b/src/pages/@owner/food-truck-form/@section/region-section/RegionSection.tsx index 602475af..fbc2102b 100644 --- a/src/pages/@owner/food-truck-form/@section/region-section/RegionSection.tsx +++ b/src/pages/@owner/food-truck-form/@section/region-section/RegionSection.tsx @@ -1,10 +1,10 @@ import { useNavigate } from 'react-router-dom'; import { useFormContext } from 'react-hook-form'; + import FormLayout from '@components/layout/form-layout/FormLayout'; import { ROUTES } from '@router/constant/routes'; import type { FoodTruckFormData } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; import { getNavigateState } from '@pages/@owner/food-truck-form/utils/navigate-state'; -import { useRegion } from '@pages/@owner/set-region/hooks/use-region'; import RegionButton from '@pages/@owner/food-truck-form/components/RegionButton'; import useToast from '@hooks/use-toast'; @@ -29,7 +29,8 @@ export default function RegionSection({ foodTruckId }: RegionSectionProps) { state: getNavigateState(formData), }); }; - const { regionCodes } = useRegion(foodTruckId); + + const regionCodes = formData.regionCodes ?? []; return ( diff --git a/src/pages/@owner/food-truck-form/FoodTruckForm.tsx b/src/pages/@owner/food-truck-form/FoodTruckForm.tsx index c23b610e..1c13fdcf 100644 --- a/src/pages/@owner/food-truck-form/FoodTruckForm.tsx +++ b/src/pages/@owner/food-truck-form/FoodTruckForm.tsx @@ -4,6 +4,9 @@ import { FormProvider } from 'react-hook-form'; import Navigation from '@layout/navigation/Navigation'; import { Icon } from '@icon/Icon'; import Button from '@ui/button/Button'; +import { ROUTES } from '@router/constant/routes'; +import useToast from '@hooks/use-toast'; + import { FoodTruckName, FoodTruckDescription, @@ -12,7 +15,6 @@ import { FoodTruckOption, FoodTruckPhoto, } from '@pages/@owner/food-truck-form/@section/basic-info-section/index'; - import { AvailableQuantity, NeedElectricity, @@ -30,8 +32,6 @@ import { } from '@pages/@owner/food-truck-form/constants/food-truck'; import ActiveTime from '@components/active-time/ActiveTime'; import ActiveDate from '@components/active-date/ActiveDate'; -import useFoodTruckDetail from '@pages/food-truck-detail/hooks/use-food-truck-detail'; -import { ROUTES } from '@router/constant/routes'; // 메인 컴포넌트 export default function FoodTruckForm() { @@ -40,9 +40,10 @@ export default function FoodTruckForm() { const navigate = useNavigate(); const location = useLocation(); + const toast = useToast(); - // TODO: id 값이 있을 시 푸드트럭 정보 가져오기 - const { methods, reset, isFormValid, handleSubmit } = useFoodTruckForm(); + const { isEdit, methods, reset, isFormValid, handleSubmit, previousName } = + useFoodTruckForm(foodTruckIdNumber); const { formActiveTime, @@ -58,8 +59,6 @@ export default function FoodTruckForm() { handleActiveDateSetValue, handleActiveDateError, } = useFoodTruckFormDate(methods); - // 서버에서 활동 가능 지역은 지역코드로 받아야함 - const { foodTruckDetailData } = useFoodTruckDetail(foodTruckIdNumber); useEffect(() => { if (location.state?.formData && location.state?.from) { @@ -67,6 +66,12 @@ export default function FoodTruckForm() { } }, [location.state, reset]); + if (!foodTruckId || isNaN(foodTruckIdNumber)) { + toast.error('잘못된 접근입니다.'); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + return null; + } + const handleNavigateBack = () => { navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); }; @@ -74,14 +79,12 @@ export default function FoodTruckForm() { return ( } handleLeftClick={handleNavigateBack} />
- + - + diff --git a/src/pages/@owner/food-truck-form/api/index.ts b/src/pages/@owner/food-truck-form/api/index.ts new file mode 100644 index 00000000..b9fe0b5f --- /dev/null +++ b/src/pages/@owner/food-truck-form/api/index.ts @@ -0,0 +1,30 @@ +import type { BaseResponseFoodTruckIdResponse } from 'apis/data-contracts'; +import { apiRequest } from '@api/apiRequest'; + +export interface UpdateFoodTruckInfoApiRequest { + name: string; + description: string; + phoneNumber: string; + activeTime: string; + timeDiscussRequired: boolean; + foodTruckServiceAreas: number[]; + menuCategories: string[]; + availableQuantity: string; + needElectricity: string; + paymentMethod: string; + availableDates: string[]; + photoUrls: string[]; + operatingInfo?: string; + option?: string; +} +export const updateMyFoodTruckInfoApi = async ( + foodTruckId: number, + data: UpdateFoodTruckInfoApiRequest +) => { + const response = await apiRequest({ + endPoint: `/food-trucks/${foodTruckId}`, + method: 'PUT', + data, + }); + return response.data; +}; diff --git a/src/pages/@owner/food-truck-form/constants/food-truck.ts b/src/pages/@owner/food-truck-form/constants/food-truck.ts index 67639459..3fef08e4 100644 --- a/src/pages/@owner/food-truck-form/constants/food-truck.ts +++ b/src/pages/@owner/food-truck-form/constants/food-truck.ts @@ -23,7 +23,7 @@ export const FOOD_TRUCK_MAX_LENGTH = { }, availableDates: { min: 1, - max: 2, + max: 4, }, photoUrls: { min: 1, diff --git a/src/pages/@owner/food-truck-form/hooks/use-basic-info.ts b/src/pages/@owner/food-truck-form/hooks/use-basic-info.ts index e983af77..e58652ba 100644 --- a/src/pages/@owner/food-truck-form/hooks/use-basic-info.ts +++ b/src/pages/@owner/food-truck-form/hooks/use-basic-info.ts @@ -6,41 +6,60 @@ import { CANNOT_UPLOAD_FILE_MB, NOT_ALLOWED_FILE_TYPE } from '@constant/image'; import { formatPhoneNumber } from '@utils/phone-number'; import { FOOD_TRUCK_ERROR_MESSAGE } from '@pages/@owner/food-truck-form/constants/food-truck'; import { useFoodTruckImage } from '@pages/@owner/upload-food-truck-images/hooks/use-food-truck-image'; +import { useFoodTruckName } from '@pages/@owner/food-truck-onboarding/hooks/use-food-truck-name'; //푸드트럭 이름, 한줄소개, 전화번호, 푸드트럭 사진, 운영정보, 기타 필드 -export const useBasicInfo = () => { +export const useBasicInfo = (previousName?: string) => { const { setValue, watch, formState: { errors }, setError, + clearErrors, } = useFormContext(); + const { handleCheckName } = useFoodTruckName(); + const formData = watch(); const name = watch('name') ?? ''; + const isChecked = watch('isNameChecked'); // 중복체크 버튼을 누를 수 있는 상태: 이름이 있고, 중복체크가 완료되지 않은 경우 - const canCheckNameDuplicate = name.trim() !== '' && !formData.nameDuplicate; + const canCheckNameDuplicate = + name.trim() !== '' && name !== previousName && !isChecked; const updateName = (name: string) => { setValue('name', name, { shouldValidate: true }); - setValue('nameDuplicate', false, { shouldValidate: true }); + if (previousName && name === previousName) { + setValue('isNameChecked', true, { shouldValidate: true }); + setValue('isNameDuplicated', false, { shouldValidate: true }); + } else { + setValue('isNameChecked', false, { shouldValidate: true }); + setValue('isNameDuplicated', false, { shouldValidate: true }); + } }; const updateDescription = (description: string) => { setValue('description', description, { shouldValidate: true }); }; - const checkNameDuplicated = () => { - //TODO: 추후 중복확인 로직 추가 - const isDuplicateSuccess = Math.random() > 0.5; + const checkNameDuplicated = async () => { + const name = formData.name; + try { + const response = await handleCheckName(name); - if (isDuplicateSuccess) { - setValue('nameDuplicate', true, { shouldValidate: true }); - } else { - setValue('nameDuplicate', false, { shouldValidate: true }); - setError('name', { - message: FOOD_TRUCK_ERROR_MESSAGE.nameDuplicate.duplicated, - }); + if (response?.duplicated) { + setValue('isNameDuplicated', true, { shouldValidate: true }); + setValue('isNameChecked', true, { shouldValidate: true }); + setError('name', { + message: FOOD_TRUCK_ERROR_MESSAGE.nameDuplicate.duplicated, + }); + } else { + setValue('isNameChecked', true, { shouldValidate: true }); + setValue('isNameDuplicated', false, { shouldValidate: true }); + clearErrors('name'); + } + } catch { + // useFoodTruckName의 onError에서 토스트 처리됨 } }; @@ -118,7 +137,8 @@ export const useBasicInfo = () => { photoUrls: formData.photoUrls, operatingInfo: formData.operatingInfo, option: formData.option, - nameDuplicate: formData.nameDuplicate, + isNameChecked: formData.isNameChecked, + isNameDuplicated: formData.isNameDuplicated, canCheckNameDuplicate, // Errors nameError: errors.name?.message, diff --git a/src/pages/@owner/food-truck-form/hooks/use-food-truck-form.ts b/src/pages/@owner/food-truck-form/hooks/use-food-truck-form.ts index b635a584..8dfb923c 100644 --- a/src/pages/@owner/food-truck-form/hooks/use-food-truck-form.ts +++ b/src/pages/@owner/food-truck-form/hooks/use-food-truck-form.ts @@ -1,14 +1,24 @@ -import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { FOOD_TRUCK_ERROR_MESSAGE } from '@pages/@owner/food-truck-form/constants/food-truck'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import useToast from '@hooks/use-toast'; + +import useFoodTruckDetail from '@pages/food-truck-detail/hooks/use-food-truck-detail'; +import { useMenusQuery } from '@pages/@owner/menu/hooks/use-menus-query'; import { foodTruckSchema, type FoodTruckFormData, } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; +import { resetFoodTruckFormValue } from '@pages/@owner/food-truck-form/utils/reset-food-truck-form-value'; +import { FOOD_TRUCK_ERROR_MESSAGE } from '@pages/@owner/food-truck-form/constants/food-truck'; +import { useMutationFoodTruckForm } from '@pages/@owner/food-truck-form/hooks/use-mutation-food-truck-form'; +import { formatFoodTruckForm } from '@pages/@owner/food-truck-form/utils/format-food-truck-form'; const initialData = { name: '', - nameDuplicate: false, + isNameChecked: false, + isNameDuplicated: true, description: '', phoneNumber: '', regionCodes: [], @@ -25,38 +35,72 @@ const initialData = { menus: false, }; -export const useFoodTruckForm = (prevData?: FoodTruckFormData) => { +export const useFoodTruckForm = (foodTruckIdNumber: number) => { + const toast = useToast(); const methods = useForm({ resolver: zodResolver(foodTruckSchema), - defaultValues: prevData ?? initialData, + defaultValues: initialData, mode: 'onChange', }); const { handleSubmit, reset, + setValue, formState: { isValid }, setError, } = methods; - const onSubmit = async (formData: FoodTruckFormData) => { - if (!formData.nameDuplicate) { + // 기존 등록 푸드트럭 데이터 조회 + const { foodTruckDetailData } = useFoodTruckDetail(foodTruckIdNumber); + + // 메뉴 등록 여부를 위한 조회 + const { data: menuData } = useMenusQuery(foodTruckIdNumber, '최신순'); + const menus = useMemo( + () => menuData?.pages.flatMap(page => page?.content ?? []) ?? [], + [menuData?.pages] + ); + + // 나의 푸드트럭 정보 업데이트 + const { updateFoodTruckInfo } = useMutationFoodTruckForm(); + + const isEdit = !!foodTruckDetailData; + + useEffect(() => { + if (!foodTruckDetailData) { + setValue('menus', menus && menus.length > 0, { shouldValidate: true }); + return; + } + + const result = resetFoodTruckFormValue(foodTruckDetailData, menus); + + if (result.isError) { + toast.error('잘못된 정보입니다. 다시 시도해주세요.'); + return; + } + reset(result.values); + }, [foodTruckDetailData, menus, toast, reset, setValue]); + + const handleSubmitFoodTruckInfo = async (formData: FoodTruckFormData) => { + if (!formData.isNameChecked) { setError('name', { message: FOOD_TRUCK_ERROR_MESSAGE.nameDuplicate.required, }); return; } - if (isValid && formData) { - //TODO: 계좌 등록 제출 - alert('푸드트럭 등록 제출'); - } + updateFoodTruckInfo({ + foodTruckId: foodTruckIdNumber, + data: formatFoodTruckForm(formData), + }); }; return { // Form methods + isEdit, methods, - handleSubmit: handleSubmit(onSubmit), + handleSubmit: handleSubmit(handleSubmitFoodTruckInfo), reset, isFormValid: isValid, + previousName: foodTruckDetailData?.name, }; }; diff --git a/src/pages/@owner/food-truck-form/hooks/use-mutation-food-truck-form.ts b/src/pages/@owner/food-truck-form/hooks/use-mutation-food-truck-form.ts new file mode 100644 index 00000000..5f191622 --- /dev/null +++ b/src/pages/@owner/food-truck-form/hooks/use-mutation-food-truck-form.ts @@ -0,0 +1,38 @@ +import { useNavigate } from 'react-router-dom'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import useToast from '@hooks/use-toast'; +import { + updateMyFoodTruckInfoApi, + type UpdateFoodTruckInfoApiRequest, +} from '@pages/@owner/food-truck-form/api'; +import { ROUTES } from '@router/constant/routes'; +import { FOOD_TRUCKS_QUERY_KEY } from '@shared/querykey/food-trucks'; + +export const useMutationFoodTruckForm = () => { + const navigate = useNavigate(); + const toast = useToast(); + const queryClient = useQueryClient(); + + const { mutate: updateFoodTruckInfo } = useMutation({ + mutationFn: ({ + foodTruckId, + data, + }: { + foodTruckId: number; + data: UpdateFoodTruckInfoApiRequest; + }) => updateMyFoodTruckInfoApi(foodTruckId, data), + onSuccess: () => { + toast.success('푸드트럭 정보가 업데이트 되었습니다.'); + queryClient.invalidateQueries({ queryKey: FOOD_TRUCKS_QUERY_KEY.ALL }); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + }, + onError: error => { + toast.error(`업데이트 실패 : ${error.message}`); + }, + }); + + return { + updateFoodTruckInfo, + }; +}; diff --git a/src/pages/@owner/food-truck-form/schemas/food-truck-form.schema.ts b/src/pages/@owner/food-truck-form/schemas/food-truck-form.schema.ts index db19d344..152dbac6 100644 --- a/src/pages/@owner/food-truck-form/schemas/food-truck-form.schema.ts +++ b/src/pages/@owner/food-truck-form/schemas/food-truck-form.schema.ts @@ -5,26 +5,31 @@ import { FOOD_TRUCK_ERROR_MESSAGE, FOOD_TRUCK_MAX_LENGTH, } from '@pages/@owner/food-truck-form/constants/food-truck'; -import { AVAILABLE_QUANTITY } from '@constant/available-quantity'; + +import type { AvailableDate } from '@type/available-date'; +import { validateFoodTruckFormTime } from '@pages/@owner/food-truck-form/utils/validate-food-truck-form-time'; import { NEED_ELECTRICITY } from '@constant/need-electricity'; import { PAYMENT_METHOD } from '@constant/payment-method'; +import { AVAILABLE_QUANTITY } from '@constant/available-quantity'; import { FOOD_CATEGORIES } from '@constant/food-categories'; -import type { AvailableDate } from '@type/available-date'; -import { validateFoodTruckFormTime } from '@pages/@owner/food-truck-form/utils/validate-food-truck-form-time'; export const foodTruckSchema = z.object({ name: z .string() .min(FOOD_TRUCK_MAX_LENGTH.name.min, FOOD_TRUCK_ERROR_MESSAGE.name.required) - .max(FOOD_TRUCK_MAX_LENGTH.name.max), - nameDuplicate: z.boolean(), + .max(FOOD_TRUCK_MAX_LENGTH.name.max, FOOD_TRUCK_ERROR_MESSAGE.name.max), + isNameChecked: z.boolean().refine(v => v === true), + isNameDuplicated: z.boolean().refine(v => v === false), description: z .string() .min( FOOD_TRUCK_MAX_LENGTH.description.min, FOOD_TRUCK_ERROR_MESSAGE.description.required ) - .max(FOOD_TRUCK_MAX_LENGTH.description.max), + .max( + FOOD_TRUCK_MAX_LENGTH.description.max, + FOOD_TRUCK_ERROR_MESSAGE.description.max + ), timeDiscussRequired: z.boolean(), activeTime: z.string().superRefine(validateFoodTruckFormTime), phoneNumber: z diff --git a/src/pages/@owner/food-truck-form/utils/format-food-truck-form.ts b/src/pages/@owner/food-truck-form/utils/format-food-truck-form.ts new file mode 100644 index 00000000..868f0048 --- /dev/null +++ b/src/pages/@owner/food-truck-form/utils/format-food-truck-form.ts @@ -0,0 +1,25 @@ +import { formatAvailableDatesToString } from '@utils/date'; + +import type { UpdateFoodTruckInfoApiRequest } from '@pages/@owner/food-truck-form/api'; +import type { FoodTruckFormData } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; + +export const formatFoodTruckForm = ( + formData: FoodTruckFormData +): UpdateFoodTruckInfoApiRequest => { + return { + name: formData.name, + description: formData.description, + phoneNumber: formData.phoneNumber, + activeTime: formData.activeTime, + timeDiscussRequired: formData.timeDiscussRequired, + foodTruckServiceAreas: formData.regionCodes.map(region => region.id!), + menuCategories: formData.menuCategories, + availableQuantity: formData.availableQuantity, + needElectricity: formData.needElectricity, + paymentMethod: formData.paymentMethod, + availableDates: formatAvailableDatesToString(formData.availableDates), + photoUrls: formData.photoUrls, + operatingInfo: formData.operatingInfo, + option: formData.option, + }; +}; diff --git a/src/pages/@owner/food-truck-form/utils/reset-food-truck-form-value.ts b/src/pages/@owner/food-truck-form/utils/reset-food-truck-form-value.ts new file mode 100644 index 00000000..3926bc83 --- /dev/null +++ b/src/pages/@owner/food-truck-form/utils/reset-food-truck-form-value.ts @@ -0,0 +1,63 @@ +import type { + FoodTruckDetailResponse, + MyFoodTruckMenuResponse, +} from 'apis/data-contracts'; + +import { normalizeEnumValue } from '@utils/normalize-enum-value'; +import { formatStringDatesToAvailableDates } from '@utils/date'; +import { AVAILABLE_QUANTITY } from '@constant/available-quantity'; +import { NEED_ELECTRICITY } from '@constant/need-electricity'; +import { PAYMENT_METHOD } from '@constant/payment-method'; +import type { FOOD_CATEGORIES } from '@constant/food-categories'; + +export const resetFoodTruckFormValue = ( + foodTruckDetailData: FoodTruckDetailResponse, + menus?: MyFoodTruckMenuResponse[] +) => { + const availableQuantity = normalizeEnumValue( + AVAILABLE_QUANTITY, + foodTruckDetailData.availableQuantity + ); + const needElectricity = normalizeEnumValue( + NEED_ELECTRICITY, + foodTruckDetailData.needElectricity + ); + const payment = normalizeEnumValue( + PAYMENT_METHOD, + foodTruckDetailData.paymentMethod + ); + if (!availableQuantity || !needElectricity || !payment) { + return { + isError: true, + }; + } + + return { + isError: false, + values: { + name: foodTruckDetailData.name, + isNameChecked: true, + isNameDuplicated: false, + description: foodTruckDetailData.description, + phoneNumber: foodTruckDetailData.phoneNumber, + regionCodes: foodTruckDetailData.regionCodes, + availableQuantity: + availableQuantity as (typeof AVAILABLE_QUANTITY)[keyof typeof AVAILABLE_QUANTITY], + needElectricity: + needElectricity as (typeof NEED_ELECTRICITY)[keyof typeof NEED_ELECTRICITY], + paymentMethod: + payment as (typeof PAYMENT_METHOD)[keyof typeof PAYMENT_METHOD], + menuCategories: + foodTruckDetailData.menuCategories as (typeof FOOD_CATEGORIES)[keyof typeof FOOD_CATEGORIES][], + photoUrls: foodTruckDetailData.photoUrl, + operatingInfo: foodTruckDetailData.operatingInfo ?? '', + option: foodTruckDetailData.option ?? '', + availableDates: formatStringDatesToAvailableDates( + foodTruckDetailData.availableDates ?? [] + ), + activeTime: foodTruckDetailData.activeTime, + timeDiscussRequired: foodTruckDetailData.timeDiscussRequired, + menus: menus && menus.length > 0, + }, + }; +}; diff --git a/src/pages/@owner/menu/hooks/use-menu-edit.ts b/src/pages/@owner/menu/hooks/use-menu-edit.ts index f6b722c2..aad2df7e 100644 --- a/src/pages/@owner/menu/hooks/use-menu-edit.ts +++ b/src/pages/@owner/menu/hooks/use-menu-edit.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ROUTES } from '@/router/constant/routes'; import type { MenuFormData } from '@pages/@owner/menu/hooks/use-form-validation'; import { @@ -7,11 +7,11 @@ import { useDeleteMenuMutation, } from '@pages/@owner/menu/hooks/use-menu-mutations'; -export const useEditMenu = ( - foodTruckId: number, - menuId: number, -) => { +export const useEditMenu = (foodTruckId: number, menuId: number) => { const navigate = useNavigate(); + const location = useLocation(); + + const foodTruckFormData = location.state?.formData; const [isModalOpen, setIsModalOpen] = useState(false); @@ -23,7 +23,7 @@ export const useEditMenu = ( }; const handleCloseModal = () => { - setIsModalOpen(false); + setIsModalOpen(false); }; const handleClickDelete = () => { @@ -36,8 +36,10 @@ export const useEditMenu = ( }; const handleClickBack = () => { - navigate(ROUTES.MENU_LIST(foodTruckId.toString())); - } + navigate(ROUTES.MENU_LIST(foodTruckId.toString()), { + state: { formData: foodTruckFormData }, + }); + }; return { isModalOpen, diff --git a/src/pages/@owner/menu/hooks/use-menu-list.ts b/src/pages/@owner/menu/hooks/use-menu-list.ts index 37730adb..d62a94d4 100644 --- a/src/pages/@owner/menu/hooks/use-menu-list.ts +++ b/src/pages/@owner/menu/hooks/use-menu-list.ts @@ -15,6 +15,7 @@ export const useMenuList = (foodTruckId: number) => { const navigate = useNavigate(); const location = useLocation(); const toast = useToast(); + const foodTruckFormData = location.state?.formData; const { isSorted, handleSortByLatest, handleSortByOldest } = useMenuSort(); const { isBottomSheetOpen, handleOpenBottomSheet, handleCloseBottomSheet } = @@ -44,9 +45,12 @@ export const useMenuList = (foodTruckId: number) => { // 네비게이션 핸들러 const handleClickBack = () => { - const formData = location.state?.formData; + if (!foodTruckFormData) { + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + return; + } const updatedFormData = { - ...formData, + ...foodTruckFormData, menus: menus.length > 0, }; navigate(ROUTES.FOOD_TRUCK_FORM(String(foodTruckId)), { @@ -55,9 +59,8 @@ export const useMenuList = (foodTruckId: number) => { }; const handleRegister = (foodTruckId: string) => { - const formData = location.state?.formData; navigate(ROUTES.MENU_REGISTER(foodTruckId), { - state: getNavigateState(formData), + state: getNavigateState(foodTruckFormData), }); }; @@ -69,7 +72,7 @@ export const useMenuList = (foodTruckId: number) => { const selectedMenu = menus.find(menu => menu.menuId === Number(menuId)); navigate(ROUTES.MENU_EDIT(foodTruckId, menuId), { - state: { menuData: selectedMenu }, + state: { menuData: selectedMenu, formData: foodTruckFormData }, }); }; diff --git a/src/pages/@owner/menu/hooks/use-menu-mutations.ts b/src/pages/@owner/menu/hooks/use-menu-mutations.ts index afb7016e..a3c48285 100644 --- a/src/pages/@owner/menu/hooks/use-menu-mutations.ts +++ b/src/pages/@owner/menu/hooks/use-menu-mutations.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; -import { ROUTES } from '@/router/constant/routes'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { ROUTES } from '@router/constant/routes'; import { postFoodTruckMenu, editFoodTruckMenu, @@ -18,6 +18,9 @@ export const useRegisterMenuMutation = (foodTruckId: number) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const toast = useToast(); + const location = useLocation(); + + const foodTruckFormData = location.state?.formData; return useMutation({ mutationFn: async (formData: MenuFormData) => { @@ -50,7 +53,9 @@ export const useRegisterMenuMutation = (foodTruckId: number) => { queryClient.invalidateQueries({ queryKey: FOOD_TRUCKS_QUERY_KEY.MENUS.SORTED_LIST(foodTruckId), }); - navigate(ROUTES.MENU_LIST(foodTruckId.toString())); + navigate(ROUTES.MENU_LIST(foodTruckId.toString()), { + state: { formData: foodTruckFormData }, + }); toast.success('메뉴가 등록되었습니다.'); }, onError: () => { @@ -64,6 +69,9 @@ export const useEditMenuMutation = (foodTruckId: number, menuId: number) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const toast = useToast(); + const location = useLocation(); + + const foodTruckFormData = location.state?.formData; return useMutation({ mutationFn: async (formData: MenuFormData) => { @@ -92,7 +100,9 @@ export const useEditMenuMutation = (foodTruckId: number, menuId: number) => { queryClient.invalidateQueries({ queryKey: FOOD_TRUCKS_QUERY_KEY.MENUS.SORTED_LIST(foodTruckId), }); - navigate(ROUTES.MENU_LIST(foodTruckId.toString())); + navigate(ROUTES.MENU_LIST(foodTruckId.toString()), { + state: { formData: foodTruckFormData }, + }); toast.success('메뉴가 수정되었습니다.'); }, onError: () => { @@ -106,6 +116,9 @@ export const useDeleteMenuMutation = (foodTruckId: number, menuId: number) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const toast = useToast(); + const location = useLocation(); + + const foodTruckFormData = location.state?.formData; return useMutation({ mutationFn: () => deleteFoodTruckMenu({ foodTruckId, menuId }), @@ -113,7 +126,9 @@ export const useDeleteMenuMutation = (foodTruckId: number, menuId: number) => { queryClient.invalidateQueries({ queryKey: FOOD_TRUCKS_QUERY_KEY.MENUS.SORTED_LIST(foodTruckId), }); - navigate(ROUTES.MENU_LIST(foodTruckId.toString())); + navigate(ROUTES.MENU_LIST(foodTruckId.toString()), { + state: { formData: foodTruckFormData }, + }); toast.success('메뉴가 삭제되었습니다.'); }, onError: () => { diff --git a/src/pages/@owner/menu/hooks/use-menu-register.ts b/src/pages/@owner/menu/hooks/use-menu-register.ts index b3d5bbf5..f5066a97 100644 --- a/src/pages/@owner/menu/hooks/use-menu-register.ts +++ b/src/pages/@owner/menu/hooks/use-menu-register.ts @@ -1,10 +1,13 @@ -import { useNavigate } from 'react-router-dom'; -import { ROUTES } from '@/router/constant/routes'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { ROUTES } from '@router/constant/routes'; import type { MenuFormData } from '@pages/@owner/menu/hooks/use-form-validation'; import { useRegisterMenuMutation } from '@pages/@owner/menu/hooks/use-menu-mutations'; export const useRegisterMenu = (foodTruckId: number) => { const navigate = useNavigate(); + const location = useLocation(); + + const foodTruckFormData = location.state?.formData; const { mutate: registerMenu } = useRegisterMenuMutation(foodTruckId); @@ -13,7 +16,9 @@ export const useRegisterMenu = (foodTruckId: number) => { }; const handleClickBack = () => { - navigate(ROUTES.MENU_LIST(foodTruckId.toString())); + navigate(ROUTES.MENU_LIST(foodTruckId.toString()), { + state: { formData: foodTruckFormData }, + }); }; return { diff --git a/src/pages/@owner/set-region/SetRegion.tsx b/src/pages/@owner/set-region/SetRegion.tsx index 157ffa8c..79626350 100644 --- a/src/pages/@owner/set-region/SetRegion.tsx +++ b/src/pages/@owner/set-region/SetRegion.tsx @@ -1,18 +1,26 @@ import { useNavigate, useLocation, useParams } from 'react-router-dom'; -import { FormProvider, useFormContext } from 'react-hook-form'; +import { FormProvider } from 'react-hook-form'; +import useToast from '@hooks/use-toast'; import { Icon } from '@icon/Icon'; import Navigation from '@layout/navigation/Navigation'; -import Region from '@shared/components/region/Region'; +import Region from '@components/region/Region'; import { ROUTES } from '@router/constant/routes'; import { useRegion } from '@pages/@owner/set-region/hooks/use-region'; import { useFoodTruckForm } from '@pages/@owner/food-truck-form/hooks/use-food-truck-form'; -import type { FoodTruckFormData } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; export default function SetRegion() { - const location = useLocation(); - const formData = location.state?.formData; - const methods = useFoodTruckForm(formData); + const navigate = useNavigate(); + const toast = useToast(); + + const { foodTruckId } = useParams(); + const foodTruckIdNumber = Number(foodTruckId); + const methods = useFoodTruckForm(foodTruckIdNumber); + + if (!foodTruckId || isNaN(foodTruckIdNumber)) { + toast.error('잘못된 접근입니다.'); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + } return ( @@ -25,7 +33,7 @@ function SetRegionContent() { const { foodTruckId } = useParams(); const navigate = useNavigate(); const location = useLocation(); - const { getValues } = useFormContext(); + const formData = location.state.formData; const handleLeftClick = () => { if (!foodTruckId) { @@ -36,13 +44,20 @@ function SetRegionContent() { navigate(ROUTES.FOOD_TRUCK_FORM(foodTruckId), { state: { from: fromPage || 'food-truck-form', - formData: getValues(), + formData: formData, }, }); }; - const { regionCodes, handleSubmitRegion, handleResetRegionFoodTruck } = - useRegion(foodTruckId); + const { handleSubmitRegion, handleResetRegionFoodTruck } = useRegion( + formData, + foodTruckId + ); + + if (!formData) { + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + return null; + } return ( <> @@ -52,7 +67,7 @@ function SetRegionContent() { handleLeftClick={handleLeftClick} /> diff --git a/src/pages/@owner/set-region/hooks/use-region.ts b/src/pages/@owner/set-region/hooks/use-region.ts index a1ced8bf..29f9d87c 100644 --- a/src/pages/@owner/set-region/hooks/use-region.ts +++ b/src/pages/@owner/set-region/hooks/use-region.ts @@ -1,51 +1,54 @@ -import { useFormContext } from 'react-hook-form'; import type { FoodTruckFormData } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; import type { RegionResponse } from 'apis/data-contracts'; import { ROUTES } from '@router/constant/routes'; import { useNavigate } from 'react-router-dom'; -import { getNavigateState } from '@pages/@owner/food-truck-form/utils/navigate-state'; + import useToast from '@hooks/use-toast'; -export const useRegion = (foodTruckId?: string) => { +export const useRegion = ( + formData: FoodTruckFormData, + foodTruckId?: string +) => { const navigate = useNavigate(); const toast = useToast(); - const { - setValue, - watch, - getValues, - formState: { errors }, - } = useFormContext(); - - const formData = watch(); - - const updateRegionCodes = (regions: RegionResponse[]) => { - setValue('regionCodes', regions, { shouldValidate: true }); - }; const handleSubmitRegion = (regions: RegionResponse[]) => { - updateRegionCodes(regions); - // setValue 후 최신 값을 가져오기 위해 getValues() 사용 - const updatedFormData = getValues(); if (!foodTruckId) { toast.error('잘못된 접근입니다.'); navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); return; } navigate(ROUTES.FOOD_TRUCK_FORM(foodTruckId), { - state: getNavigateState(updatedFormData), + state: { + from: 'set-region', + formData: { + ...formData, + regionCodes: regions, + }, + }, }); }; const handleResetRegionFoodTruck = () => { - updateRegionCodes([]); + if (!foodTruckId) { + toast.error('잘못된 접근입니다.'); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + return; + } + navigate(ROUTES.FOOD_TRUCK_FORM(foodTruckId), { + state: { + from: 'set-region', + formData: { + ...formData, + regionCodes: [], + }, + }, + }); }; return { - regionCodes: formData.regionCodes ?? [], - - regionCodesError: errors.regionCodes?.message, + regionCodes: formData?.regionCodes ?? [], - updateRegionCodes, handleSubmitRegion, handleResetRegionFoodTruck, }; diff --git a/src/pages/@owner/upload-food-truck-images/UploadFoodTruckImages.tsx b/src/pages/@owner/upload-food-truck-images/UploadFoodTruckImages.tsx index 62e78560..38116327 100644 --- a/src/pages/@owner/upload-food-truck-images/UploadFoodTruckImages.tsx +++ b/src/pages/@owner/upload-food-truck-images/UploadFoodTruckImages.tsx @@ -1,10 +1,12 @@ -import { useLocation } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { FormProvider } from 'react-hook-form'; + +import useToast from '@hooks/use-toast'; +import { ROUTES } from '@router/constant/routes'; import Navigation from '@components/layout/navigation/Navigation'; import { Icon } from '@components/icon/Icon'; - import ErrorText from '@components/form/error-text/ErrorText'; -import { useFoodTruckForm } from '@pages/@owner//food-truck-form/hooks/use-food-truck-form'; +import { useFoodTruckForm } from '@pages/@owner/food-truck-form/hooks/use-food-truck-form'; import { useUploadImages } from '@pages/@owner/upload-food-truck-images/hooks/use-upload-images'; import { UploadDescription, @@ -13,9 +15,17 @@ import { } from '@pages/@owner/upload-food-truck-images/components'; export default function UploadFoodTruckImages() { - const location = useLocation(); - const formData = location.state?.formData; - const methods = useFoodTruckForm(formData); + const navigate = useNavigate(); + const toast = useToast(); + const { foodTruckId } = useParams(); + const foodTruckIdNumber = Number(foodTruckId); + + if (!foodTruckId || isNaN(foodTruckIdNumber)) { + toast.error('잘못된 접근입니다.'); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + } + + const methods = useFoodTruckForm(foodTruckIdNumber); return ( diff --git a/src/pages/@owner/upload-food-truck-images/hooks/use-food-truck-image.ts b/src/pages/@owner/upload-food-truck-images/hooks/use-food-truck-image.ts index bec2655b..291e4391 100644 --- a/src/pages/@owner/upload-food-truck-images/hooks/use-food-truck-image.ts +++ b/src/pages/@owner/upload-food-truck-images/hooks/use-food-truck-image.ts @@ -6,6 +6,7 @@ import { } from '@pages/@owner/upload-food-truck-images/api'; import type { FoodTruckImageUrl } from '@pages/@owner/upload-food-truck-images/types/food-truck-image-url'; import { FOOD_TRUCKS_QUERY_KEY } from '@shared/querykey/food-trucks'; +import useToast from '@hooks/use-toast'; export const useFoodTruckImage = () => { return useMutation({ @@ -39,10 +40,17 @@ export const useFoodTruckImage = () => { }; export const useUploadImage = () => { + const toast = useToast(); return useMutation({ mutationFn: async ({ presignedUrl, file }) => { await uploadImage(presignedUrl, file); }, + onSuccess: () => { + toast.success('이미지 추가 완료'); + }, + onError: () => { + toast.error('이미지 업로드 오류'); + }, }); }; diff --git a/src/pages/@owner/upload-food-truck-images/hooks/use-upload-images.ts b/src/pages/@owner/upload-food-truck-images/hooks/use-upload-images.ts index 7fcbfc76..198305e1 100644 --- a/src/pages/@owner/upload-food-truck-images/hooks/use-upload-images.ts +++ b/src/pages/@owner/upload-food-truck-images/hooks/use-upload-images.ts @@ -1,9 +1,9 @@ import { useEffect, useState, type ChangeEvent } from 'react'; -import { useFormContext } from 'react-hook-form'; import { useNavigate, useLocation, useParams } from 'react-router-dom'; -import { ROUTES } from '@router/constant/routes'; import { arrayMove } from '@dnd-kit/sortable'; +import useToast from '@hooks/use-toast'; +import { ROUTES } from '@router/constant/routes'; import { useFoodTruckImage, useUploadImage, @@ -11,15 +11,14 @@ import { } from '@pages/@owner/upload-food-truck-images/hooks/use-food-truck-image'; import type { DisplayImage } from '@pages/@owner/upload-food-truck-images/types/food-truck-image-display'; -import type { FoodTruckFormData } from '@pages/@owner/food-truck-form/schemas/food-truck-form.schema'; import { imageFileSchema } from '@pages/@owner/upload-food-truck-images/schemas/upload-food-truck-images.schema'; export const useUploadImages = () => { + const toast = useToast(); const navigate = useNavigate(); const location = useLocation(); const { foodTruckId } = useParams<{ foodTruckId: string }>(); - - const { setValue, getValues } = useFormContext(); + const formData = location.state.formData; const [initialImageUrls, setInitialImageUrls] = useState([]); const [images, setImages] = useState([]); @@ -30,8 +29,13 @@ export const useUploadImages = () => { const { mutateAsync: uploadToS3 } = useUploadImage(); const { mutateAsync: deleteFromS3 } = useDeleteImage(); + if (!formData) { + toast.error('잘못된 접근입니다.'); + navigate(ROUTES.FOOD_TRUCK_MANAGEMENT); + } + useEffect(() => { - const existingUrls: string[] = getValues('photoUrls') || []; + const existingUrls: string[] = formData.photoUrls || []; setInitialImageUrls(existingUrls); const displayImages = existingUrls.map(url => ({ @@ -40,7 +44,7 @@ export const useUploadImages = () => { url: url, })); setImages(displayImages); - }, [getValues]); + }, [formData]); useEffect(() => { const previews = images.map(image => { @@ -143,12 +147,13 @@ export const useUploadImages = () => { }) .filter((url): url is string => !!url); - setValue('photoUrls', finalOrderedUrls, { shouldValidate: true }); - navigate(ROUTES.FOOD_TRUCK_FORM(foodTruckId), { state: { - formData: getValues(), from: 'upload-food-truck-images', + formData: { + ...formData, + photoUrls: finalOrderedUrls, + }, }, }); } catch (error) { @@ -161,7 +166,7 @@ export const useUploadImages = () => { const handleLeftClick = () => { if (!foodTruckId) return; navigate(ROUTES.FOOD_TRUCK_FORM(foodTruckId), { - state: { formData: getValues(), from: location.pathname }, + state: { from: location.pathname, formData: formData }, }); }; diff --git a/src/pages/food-truck-detail/hooks/use-food-truck-detail.ts b/src/pages/food-truck-detail/hooks/use-food-truck-detail.ts index 92198b9d..04e59d70 100644 --- a/src/pages/food-truck-detail/hooks/use-food-truck-detail.ts +++ b/src/pages/food-truck-detail/hooks/use-food-truck-detail.ts @@ -10,6 +10,7 @@ const foodTruckDetailQuery = (foodTruckId: number) => ({ queryKey: FOOD_TRUCKS_QUERY_KEY.DETAIL(foodTruckId), queryFn: () => getFoodTruckDetail(foodTruckId), staleTime: 5000, + enabled: !!foodTruckId, }); export default function useFoodTruckDetail(foodTruckId: number) { @@ -19,7 +20,9 @@ export default function useFoodTruckDetail(foodTruckId: number) { data: foodTruckDetailData, isPending: isPendingFoodTruckDetail, isError: isErrorFoodTruckDetail, - } = useQuery(foodTruckDetailQuery(foodTruckId)); + } = useQuery( + foodTruckDetailQuery(foodTruckId) + ); const { mutate: updateSaveStatus } = useUpdateFoodTruckSaveStatus(foodTruckId); diff --git a/src/shared/components/region/components/DepthSection.tsx b/src/shared/components/region/components/DepthSection.tsx index 0fd7976e..25ab7773 100644 --- a/src/shared/components/region/components/DepthSection.tsx +++ b/src/shared/components/region/components/DepthSection.tsx @@ -65,6 +65,7 @@ export default function DepthSection({ handleSelectDepth3={() => handleToggleRegion({ name: fullName, + id: item.id, code: item.code, }) } diff --git a/src/shared/components/region/hooks/use-region.ts b/src/shared/components/region/hooks/use-region.ts index df7678d8..0d40fc51 100644 --- a/src/shared/components/region/hooks/use-region.ts +++ b/src/shared/components/region/hooks/use-region.ts @@ -19,12 +19,18 @@ export default function useRegion({ const toast = useToast(); const handleSelectRegion = (region: RegionResponse) => { + const alreadySelected = selectedRegions.some(r => r.code === region.code); + if (alreadySelected) { + setSelectedRegions(prev => prev.filter(r => r.code !== region.code)); + return; + } if (selectedRegions.length >= MAX_SELECTED) { toast.error('최대 선택 개수를 초과했습니다.'); return; } setSelectedRegions(prev => [...prev, region]); }; + const handleDeleteRegion = (region: RegionResponse) => { setSelectedRegions(prev => prev.filter(r => r.code !== region.code)); }; diff --git a/src/shared/constant/need-electricity.ts b/src/shared/constant/need-electricity.ts index e137f036..88fa9f5b 100644 --- a/src/shared/constant/need-electricity.ts +++ b/src/shared/constant/need-electricity.ts @@ -1,6 +1,6 @@ export const NEED_ELECTRICITY = { REQUIRED: '필요', - NOT_REQUIRED: '필요 없음', + NOT_REQUIRED: '불필요', NEED_DISCUSSION: '논의 필요', } as const; diff --git a/src/shared/constant/payment-method.ts b/src/shared/constant/payment-method.ts index f23d2927..e73f4bfc 100644 --- a/src/shared/constant/payment-method.ts +++ b/src/shared/constant/payment-method.ts @@ -1,7 +1,7 @@ export const PAYMENT_METHOD = { CARD: '카드', - BANK_TRANSFER: '계좌 이체', - ANY: '아무거나', + BANK_TRANSFER: '계좌이체', + ANY: '무관', } as const; export type PaymentMethodKey = keyof typeof PAYMENT_METHOD; diff --git a/src/shared/hooks/use-toast.ts b/src/shared/hooks/use-toast.ts index 9a78b403..48b4f71b 100644 --- a/src/shared/hooks/use-toast.ts +++ b/src/shared/hooks/use-toast.ts @@ -1,18 +1,35 @@ import { useSetAtom } from 'jotai'; import { ToastAtom } from '@utils/toast'; import { TOAST_TYPE } from '@constant/toast'; +import { useCallback, useMemo } from 'react'; const useToast = () => { const addToast = useSetAtom(ToastAtom); - return { - success: (message: string) => - addToast({ type: TOAST_TYPE.SUCCESS, message }), - error: (message: string) => addToast({ type: TOAST_TYPE.ERROR, message }), - warning: (message: string) => - addToast({ type: TOAST_TYPE.WARNING, message }), - info: (message: string) => addToast({ type: TOAST_TYPE.INFO, message }), - }; + const success = useCallback( + (message: string) => addToast({ type: TOAST_TYPE.SUCCESS, message }), + [addToast] + ); + + const error = useCallback( + (message: string) => addToast({ type: TOAST_TYPE.ERROR, message }), + [addToast] + ); + + const warning = useCallback( + (message: string) => addToast({ type: TOAST_TYPE.WARNING, message }), + [addToast] + ); + + const info = useCallback( + (message: string) => addToast({ type: TOAST_TYPE.INFO, message }), + [addToast] + ); + + return useMemo( + () => ({ success, error, warning, info }), + [success, error, warning, info] + ); }; export default useToast; diff --git a/src/shared/utils/date/date-formatter.ts b/src/shared/utils/date/date-formatter.ts index 163f858b..4aaae1f6 100644 --- a/src/shared/utils/date/date-formatter.ts +++ b/src/shared/utils/date/date-formatter.ts @@ -66,17 +66,19 @@ export const formatSelectedDateToSchedules = ( }; /** - * 기존 견적서의 날짜를 AvailableDates 형식으로 포맷하는 함수 + * string[] 형식의 날짜를 AvailableDate[] 형식으로 포맷하는 함수 * ["2025.09.20 ~ 2025.09.20", "2025.09.25 ~ 2025.09.25"] * -> AvailableDate[] */ -export const formatEstimateDatesToAvailableDates = ( - estimateDates: string[] +export const formatStringDatesToAvailableDates = ( + dates: string[] ): AvailableDate[] => { - return estimateDates + return dates .map((raw, index) => { + // "YYYY-MM-DD ~ YYYY-MM-DD" 형태로 들어왔을 때 변환. 나의 푸드트럭 등록/수정에서 사용 + const normalized = raw.replaceAll('-', '.'); // "YYYY.MM.DD ~ YYYY.MM.DD" 또는 "YYYY.MM.DD" 둘 다 대응 - const [startRaw, endRaw] = raw.split('~').map(s => s.trim()); + const [startRaw, endRaw] = normalized.split('~').map(s => s.trim()); const startDate = startRaw ?? ''; const endDate = endRaw ?? ''; @@ -100,3 +102,19 @@ export const formatEstimateDatesToAvailableDates = ( }) .filter((v): v is AvailableDate => v !== null); }; + +/** + * AvailableDate[] 형식의 날짜를 string[] 형식으로 포맷하는 함수 + */ +export const formatAvailableDatesToString = ( + dates: AvailableDate[] +): string[] => { + return dates + .filter(date => date.startDate) + .map(date => { + if (date.endDate) { + return `${date.startDate} ~ ${date.endDate}`; + } + return `${date.startDate} ~ ${date.startDate}`; + }); +}; diff --git a/src/shared/utils/normalize-enum-value.ts b/src/shared/utils/normalize-enum-value.ts new file mode 100644 index 00000000..3d2e3087 --- /dev/null +++ b/src/shared/utils/normalize-enum-value.ts @@ -0,0 +1,11 @@ +export const normalizeEnumValue = >( + enumObj: T, + value?: string +): T[keyof T] | undefined => { + if (!value) return undefined; + + const values = Object.values(enumObj) as Array; + return values.includes(value as T[keyof T]) + ? (value as T[keyof T]) + : undefined; +};