Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
bdc5066
code review: 코드래빗 리뷰 반영.
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",
Copy link
Collaborator

Choose a reason for hiding this comment

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

직접 수정을 해보니 package.json에서 환경변수를 써서 처리하는 건 여러 방식 중 하나로 하면 가능한데 생성된 http-client.ts 같은 파일에서는 여전히 노출되는 문제가 있습니다.
그래서 제안하는 방식은, swagger typescript api 스크립트를 별도 파일로 분리 + 요청 완료시 ip주소 문자열을, 환경 변수 사용으로 덮어씌우도록 하는 코드를 추가하는 건 어떨까요?
.then에서 replace( "${apiBaseUrl}", import.meta.env.VITE_API_SERVER_URL );처럼

"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
27 changes: 7 additions & 20 deletions src/pages/@owner/estimate/utils/format-estimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 형식으로 포맷
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FormLayout isRequired={true} title='푸드트럭 이름'>
<Input
Expand All @@ -23,19 +30,22 @@ export default function FoodTruckNameInput() {
maxLength={FOOD_TRUCK_MAX_LENGTH.name.max}
error={!!nameError}
value={name}
handleRightClick={checkNameDuplicated}
onChange={e => updateName(e.target.value)}
rightComponent={
<Button
buttonStyle={canCheckNameDuplicate ? 'active' : 'disabled'}
variant='verify'
disabled={!canCheckNameDuplicate}
>
중복확인
</Button>
}
handleRightClick={
canCheckNameDuplicate ? checkNameDuplicated : undefined
}
/>
{nameError && <ErrorText text={nameError} />}
{nameDuplicate && (
{isNameChecked && !isNameDuplicated && (
<div className='flex w-full items-center gap-[0.2rem]'>
<Icon name='ic_check' />
<p className='caption-m-12 text-green-500'>사용 가능한 이름입니다.</p>
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
27 changes: 15 additions & 12 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, previousName } =
useFoodTruckForm(foodTruckIdNumber);
Copy link
Collaborator

Choose a reason for hiding this comment

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

프론트 측 문제는 아니지만, url을 바꾸면 푸드트럭 주인이 아니더라도 그 내용이 조회가 가능한 문제가 있습니다.

서버 측에서 적절한 사용자인지 확인을 하는 로직이 추가로 있기 + 임의 문자열로 보이게 하는 처리가 병행되면 좋을 것 같습니다.


const {
formActiveTime,
Expand All @@ -58,30 +59,32 @@ 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);
return null;
}

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

return (
<FormProvider {...methods}>
<Navigation
centerContent={
foodTruckDetailData ? '나의 푸드트럭 수정' : '나의 푸드트럭 등록'
}
centerContent={isEdit ? '나의 푸드트럭 수정' : '나의 푸드트럭 등록'}
leftIcon={<Icon name='ic_back' />}
handleLeftClick={handleNavigateBack}
/>
<div className='flex flex-col px-[2rem] pb-[12rem]'>
<FoodTruckName />
<FoodTruckName previousName={previousName} />
<FoodTruckDescription />
<FoodTruckPhoneNumber />
<ActiveTime
Expand All @@ -91,7 +94,7 @@ export default function FoodTruckForm() {
handleActiveTimeSetValue={handleActiveTimeSetValue}
handleTimeDiscussRequiredSetValue={handleTimeDiscussRequiredSetValue}
/>
<RegionSection />
<RegionSection foodTruckId={foodTruckId} />
<MenuCategory />
<AvailableQuantity />
<NeedElectricity />
Expand Down
30 changes: 30 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,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<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
48 changes: 34 additions & 14 deletions src/pages/@owner/food-truck-form/hooks/use-basic-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FoodTruckFormData>();

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에서 토스트 처리됨
}
};

Expand Down Expand Up @@ -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,
Expand Down
Loading