Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7872e07
feat: 기존 데이터가 있다면 폼 데이터 초기화
holdn2 Dec 23, 2025
1698113
feat: 메뉴 목록 조회 api를 이용하여 있다면 폼 데이터의 menu가 true 가 되도록 함
holdn2 Dec 23, 2025
35eb0e1
refactor: 다른 페이지 이동 시 getNavigateState 사용하도록 다시 롤백
holdn2 Dec 23, 2025
1d3d9f2
Merge branch 'develop' into feature/#199/food-truck-form-api
holdn2 Jan 3, 2026
535d2a3
code review: zod 스키마에서 수량, 전기, 결제 관련 유효성 검증 기본 string에서 enum으로 수정
holdn2 Jan 3, 2026
93eeb76
code review: normalizeEnumValue의 undefined 반환 처리
holdn2 Jan 3, 2026
017fe67
refactor: 활동가능지역 관련 로직 수정 및 오류 해결 / 가독성 및 유지보수를 위해 resetFoodTruckForm…
holdn2 Jan 3, 2026
ac68c43
code review: 코드 리뷰 반영
holdn2 Jan 3, 2026
102fed4
refactor: 푸드트럭 사진 관련 로직 수정
holdn2 Jan 3, 2026
a7613d7
code review: 코드리뷰 반영. 에러 방지 코드 추가
holdn2 Jan 3, 2026
9e6d4d0
chore: 스웨거 관련 명령어 수정 및 스웨거 업데이트
holdn2 Jan 4, 2026
75372e4
code review: UploadFoodTruckImages에서 훅 호출 전 foodTruckId 유효성 검증
holdn2 Jan 4, 2026
549542b
feat: 나의 푸드트럭 등록 및 수정 api 함수 구현 및 관련 훅 구현
holdn2 Jan 4, 2026
048c595
fix: 메뉴 등록 페이지로 갔다 돌아올 때도 state를 적절히 추가해서 에러가 생기지 않도록 수정함
holdn2 Jan 5, 2026
2f282ed
fix: 메뉴 등록 페이지로 갔다 돌아올 때도 state를 적절히 추가해서 에러가 생기지 않도록 수정함
holdn2 Jan 5, 2026
812888c
feat: 나의푸드트럭 등록/수정 api 연동 완료. 서버와의 오류 해결 필요
holdn2 Jan 5, 2026
a981621
refactor: 서버에서 받는 날짜 형식이 달라도 동일한 반환값을 내도록 함수 수정
holdn2 Jan 6, 2026
328eea5
fix: 서버에서 요청한데로 지역코드가 아닌 id로 보내도록 수정
holdn2 Jan 6, 2026
527735e
code review: formdata가 없을 때의 예외처리 추가
holdn2 Jan 6, 2026
cb80f29
feat: 닉네임 중복 검증 추가
holdn2 Jan 6, 2026
f8016bf
refactor: 이름 관련 유효성 검증 알맞게 수정
holdn2 Jan 10, 2026
695b746
refactor: 운영정보 및 기타에 대해서 null이 오면 빈 string으로 변환하도록 수정
holdn2 Jan 10, 2026
7d3aafc
code review: early return 추가 및 객체 안전성을 위한 스키마 수정(z.nativeEnum 사용)
holdn2 Jan 10, 2026
e2cb925
fix: 타입 단언으로 빌드오류 수정
holdn2 Jan 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apis/data-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apis/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class HttpClient<SecurityDataType = unknown> {
}: ApiConfig<SecurityDataType> = {}) {
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;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/@owner/estimate/hooks/use-estimate-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,7 +67,7 @@ export const useEstimateForm = (
{
location: estimateData.address ?? '',
detailLocation: estimateData.detailAddress ?? '',
availableDates: formatEstimateDatesToAvailableDates(
availableDates: formatStringDatesToAvailableDates(
estimateData.reservationDates ?? []
),
activeTime: estimateData.operationHour ?? undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -29,7 +29,8 @@ export default function RegionSection({ foodTruckId }: RegionSectionProps) {
state: getNavigateState(formData),
});
};
const { regionCodes } = useRegion(foodTruckId);

const regionCodes = formData.regionCodes ?? [];

return (
<FormLayout isRequired={true} title='활동 가능 지역'>
Expand Down
24 changes: 13 additions & 11 deletions src/pages/@owner/food-truck-form/FoodTruckForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,7 +15,6 @@ import {
FoodTruckOption,
FoodTruckPhoto,
} from '@pages/@owner/food-truck-form/@section/basic-info-section/index';

import {
AvailableQuantity,
NeedElectricity,
Expand All @@ -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() {
Expand All @@ -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 } =
useFoodTruckForm(foodTruckIdNumber);

const {
formActiveTime,
Expand All @@ -58,25 +59,26 @@ export default function FoodTruckForm() {
handleActiveDateSetValue,
handleActiveDateError,
} = useFoodTruckFormDate(methods);
// 서버에서 활동 가능 지역은 지역코드로 받아야함
const { foodTruckDetailData } = useFoodTruckDetail(foodTruckIdNumber);

useEffect(() => {
if (location.state?.formData && location.state?.from) {
reset(location.state.formData);
}
}, [location.state, reset]);

if (!foodTruckId || isNaN(foodTruckIdNumber)) {
toast.error('잘못된 접근입니다.');
navigate(ROUTES.FOOD_TRUCK_MANAGEMENT);
}

const handleNavigateBack = () => {
navigate(ROUTES.FOOD_TRUCK_MANAGEMENT);
};

return (
<FormProvider {...methods}>
<Navigation
centerContent={
foodTruckDetailData ? '나의 푸드트럭 수정' : '나의 푸드트럭 등록'
}
centerContent={isEdit ? '나의 푸드트럭 수정' : '나의 푸드트럭 등록'}
leftIcon={<Icon name='ic_back' />}
handleLeftClick={handleNavigateBack}
/>
Expand All @@ -91,7 +93,7 @@ export default function FoodTruckForm() {
handleActiveTimeSetValue={handleActiveTimeSetValue}
handleTimeDiscussRequiredSetValue={handleTimeDiscussRequiredSetValue}
/>
<RegionSection />
<RegionSection foodTruckId={foodTruckId} />
<MenuCategory />
<AvailableQuantity />
<NeedElectricity />
Expand Down
17 changes: 17 additions & 0 deletions src/pages/@owner/food-truck-form/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {
BaseResponseFoodTruckIdResponse,
UpdateFoodTruckInfoRequest,
} from 'apis/data-contracts';
import { apiRequest } from '@api/apiRequest';

export const updateMyFoodTruckInfoApi = async (
foodTruckId: number,
data: UpdateFoodTruckInfoRequest
) => {
const response = await apiRequest<BaseResponseFoodTruckIdResponse>({
endPoint: `/food-trucks/${foodTruckId}`,
method: 'PUT',
data,
});
return response.data;
};
2 changes: 1 addition & 1 deletion src/pages/@owner/food-truck-form/constants/food-truck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const FOOD_TRUCK_MAX_LENGTH = {
},
availableDates: {
min: 1,
max: 2,
max: 4,
},
photoUrls: {
min: 1,
Expand Down
40 changes: 35 additions & 5 deletions src/pages/@owner/food-truck-form/hooks/use-food-truck-form.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } 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';

const initialData = {
name: '',
Expand All @@ -25,10 +32,11 @@ const initialData = {
menus: false,
};

export const useFoodTruckForm = (prevData?: FoodTruckFormData) => {
export const useFoodTruckForm = (foodTruckIdNumber: number) => {
const toast = useToast();
const methods = useForm<FoodTruckFormData>({
resolver: zodResolver(foodTruckSchema),
defaultValues: prevData ?? initialData,
defaultValues: initialData,
mode: 'onChange',
});

Expand All @@ -39,6 +47,27 @@ export const useFoodTruckForm = (prevData?: FoodTruckFormData) => {
setError,
} = methods;

// 기존 등록 푸드트럭 데이터 조회
const { foodTruckDetailData } = useFoodTruckDetail(foodTruckIdNumber);

// 메뉴 등록 여부를 위한 조회
const { data: menuData } = useMenusQuery(foodTruckIdNumber, '최신순');

const isEdit = !!foodTruckDetailData;

useEffect(() => {
if (!foodTruckDetailData) return;

const menus = menuData?.pages.flatMap(page => page?.content || []);
const result = resetFoodTruckFormValue(foodTruckDetailData, menus);

if (result.isError) {
toast.error('잘못된 정보입니다. 다시 시도해주세요.');
return;
}
reset(result.values);
}, [foodTruckDetailData, menuData, reset, toast]);

const onSubmit = async (formData: FoodTruckFormData) => {
if (!formData.nameDuplicate) {
setError('name', {
Expand All @@ -47,13 +76,14 @@ export const useFoodTruckForm = (prevData?: FoodTruckFormData) => {
return;
}
if (isValid && formData) {
//TODO: 계좌 등록 제출
// TODO: 푸드트럭 등록 api 호출
alert('푸드트럭 등록 제출');
}
};

return {
// Form methods
isEdit,
methods,
handleSubmit: handleSubmit(onSubmit),
reset,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useNavigate } from 'react-router-dom';
import type { UpdateFoodTruckInfoRequest } from 'apis/data-contracts';
import { useMutation, useQueryClient } from '@tanstack/react-query';

import useToast from '@hooks/use-toast';
import { updateMyFoodTruckInfoApi } 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: UpdateFoodTruckInfoRequest;
}) => 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}`);
},
Comment on lines +30 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 메시지 노출을 제한하세요.

error.message를 직접 사용자에게 노출하면 기술적 세부사항이나 민감한 정보가 유출될 수 있습니다. 사용자 친화적인 일반 메시지를 표시하거나, 특정 에러 케이스를 처리하는 것이 좋습니다.

🔎 안전한 에러 처리 제안
     onError: error => {
-      toast.error(`업데이트 실패 : ${error.message}`);
+      const errorMessage = 
+        error instanceof ApiRequestError 
+          ? '푸드트럭 정보 업데이트에 실패했습니다. 다시 시도해주세요.'
+          : '네트워크 오류가 발생했습니다.';
+      toast.error(errorMessage);
+      console.error('Food truck update error:', error);
     },

또는 에러 타입별로 다른 메시지를 제공할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/@owner/food-truck-form/hooks/use-mutation-food-truck-form.ts around
lines 28-30, the onError handler currently exposes error.message directly to the
user; replace that with a generic, user-friendly toast (e.g., "업데이트 중 문제가
발생했습니다. 잠시 후 다시 시도해주세요.") and map known error types/codes to specific friendly
messages if needed; send the original error details to internal logging
(console.error or Sentry) for debugging instead of displaying them in the toast
so technical details/sensitive info are not exposed to users.

});

return {
updateFoodTruckInfo,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ 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 { NEED_ELECTRICITY } from '@constant/need-electricity';
import { PAYMENT_METHOD } from '@constant/payment-method';
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';
import { NEED_ELECTRICITY } from '@constant/need-electricity';
import { PAYMENT_METHOD } from '@constant/payment-method';
import { AVAILABLE_QUANTITY } from '@constant/available-quantity';

export const foodTruckSchema = z.object({
name: z
Expand All @@ -34,10 +34,12 @@ export const foodTruckSchema = z.object({
FOOD_TRUCK_ERROR_MESSAGE.phoneNumber.required
),
regionCodes: z.array(z.custom<RegionResponse>()),
availableQuantity: z.nativeEnum(AVAILABLE_QUANTITY),
needElectricity: z.nativeEnum(NEED_ELECTRICITY),
paymentMethod: z.nativeEnum(PAYMENT_METHOD),
menuCategories: z.array(z.nativeEnum(FOOD_CATEGORIES)),
availableQuantity: z.enum(
Object.values(AVAILABLE_QUANTITY).map(item => item)
),
needElectricity: z.enum(Object.values(NEED_ELECTRICITY).map(item => item)),
paymentMethod: z.enum(Object.values(PAYMENT_METHOD).map(item => item)),
menuCategories: z.array(z.string()),
photoUrls: z.array(z.string()).min(1, {
message: FOOD_TRUCK_ERROR_MESSAGE.photoUrls.required,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
FoodTruckDetailResponse,
MyFoodTruckMenuResponse,
} from 'apis/data-contracts';

import { normalizeEnumValue } from '@utils/normalize-enum-value';
import { AVAILABLE_QUANTITY } from '@constant/available-quantity';
import { NEED_ELECTRICITY } from '@constant/need-electricity';
import { PAYMENT_METHOD } from '@constant/payment-method';
import { formatStringDatesToAvailableDates } from '@utils/date';

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,
nameDuplicate: true,
description: foodTruckDetailData.description,
phoneNumber: foodTruckDetailData.phoneNumber,
regionCodes: foodTruckDetailData.regionCodes,
availableQuantity: availableQuantity,
needElectricity: needElectricity,
paymentMethod: payment,
menuCategories: foodTruckDetailData.menuCategories,
photoUrls: foodTruckDetailData.photoUrl,
operatingInfo: foodTruckDetailData.operatingInfo,
option: foodTruckDetailData.option,
availableDates: formatStringDatesToAvailableDates(
foodTruckDetailData.availableDates ?? []
),
activeTime: foodTruckDetailData.activeTime,
timeDiscussRequired: foodTruckDetailData.timeDiscussRequired,
menus: menus && menus.length > 0,
},
};
};
Loading