-
Notifications
You must be signed in to change notification settings - Fork 1
✨ Feat: 상품 등록 페이지 퍼블리싱, 기능 개발 및 API 연동 #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
a9f1052
1c3160f
f60d1a3
d075e86
901f296
c0ee6cf
0824477
2d3522f
c5f3778
6bfd150
146ecb2
76e8017
a3fc380
729821a
61d93db
ab9ffb0
5d4d924
ff54771
7524b49
80df1f2
5bbd414
3e41f20
dfa7b19
f2489b8
30c9f76
bc31c81
efca2ee
091e904
4abdbe6
cd5fcc1
2ab8270
10e58a2
61645f6
0911886
c8c06f8
ddc10fd
b1453dc
59ca13a
6629f99
a8f5ed6
9fbcf38
9321bd1
8cf0346
4a3de1c
4968568
c279306
a4b293a
6bd7989
c82faa7
1774202
a5668ae
eba3473
8dc16a9
5fadb8e
3dc8753
45fc7db
18a3684
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { DeliveryCompany } from './product-delivery-company' | ||
| export { DeliveryTerms } from './product-delivery-terms' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { DISCLOSURE_FIELDS, RADIO_OPTIONS } from './product-disclosure.constant' | ||
| export type { ProductInfoNoticeKey } from './product-disclosure.constant' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { | ||
| DeliveryFormInput, | ||
| ProductDisclosureFormInput, | ||
| ProductFormInput, | ||
| ProductOptionFormInput, | ||
| ThumbnailFormInput, | ||
| } from './create-indivisual-form.type' | ||
|
|
||
| export type CreateFormType = ProductFormInput & | ||
| DeliveryFormInput & { | ||
| options: ProductOptionFormInput[] // Feature의 스키마 대신 Entity의 순수 타입을 사용 | ||
| } & ProductDisclosureFormInput & | ||
| ThumbnailFormInput | ||
|
Comment on lines
+9
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 교집합 타입 시 키 충돌 주의 (Optional) 여러 폼 타입을 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ export type ProductFormInput = { | |
| productionTime: string | ||
| price: number | null | ||
| discountAmount: number | null | ||
| discountType: 'won' | 'percentage' | ||
| discountType: 'AMOUNT' | 'RATE' | ||
| } | ||
|
|
||
| export type DeliveryFormInput = { | ||
|
|
@@ -16,8 +16,13 @@ export type DeliveryFormInput = { | |
| deliveryMinFee: number | null | ||
| } | ||
|
|
||
| export type ThumbnailFormInput = { | ||
| mainImage: File | null | ||
| extraImages: File[] | ||
| } | ||
|
|
||
| export type ProductOptionFormInput = { | ||
| mainCategory: string | ||
| mainCategory: 'bread' | 'snack' | '' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Optional: 미선택 상태를 |
||
| subCategory: string | ||
| optionName: string | ||
| ingredientCategories: ('glutenFree' | 'vegan')[] | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||
| import { client } from '@/shared/utils/axios' | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { ApiResponse, StoreInfo } from './create.type' | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // storeId 조회 | ||||||||||||||||||||||||||
| export const getMyStore = async () => { | ||||||||||||||||||||||||||
| const response = await client.get<ApiResponse<{ store: StoreInfo }>>( | ||||||||||||||||||||||||||
| '/api/v1/seller/stores', | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| return response.data.result.store | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 상품 등록 | ||||||||||||||||||||||||||
| export const createProduct = async (formData: FormData) => { | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 반환 타입 응답 스키마( |
||||||||||||||||||||||||||
| const response = await client.post('/api/v1/seller/boards', formData, { | ||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||
| 'Content-Type': 'multipart/form-data', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| return response.data | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+14
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, axios automatically sets the Content-Type: multipart/form-data header with the correct boundary when posting a FormData object, by detecting the FormData payload and either letting the browser/network adapter handle it or using the FormData instance's headers (like getHeaders in Node.js with form-data polyfill). This has been the standard behavior since early versions, with improvements in v0.27+ for better Node.js support via form-data dependency. If the Content-Type header is manually set to multipart/form-data without a boundary parameter (e.g., { headers: { 'Content-Type': 'multipart/form-data' } }), it can cause problems: - In browser XMLHttpRequest adapter (default): Axios historically avoided overriding the header to let the browser set the boundary; manual setting without boundary may lead to missing boundary, causing servers to reject with "Multipart: Boundary not found". - In Node.js http adapter: Uses FormData's getHeaders which includes boundary, but manual override without boundary breaks parsing. - In fetch adapter (recent fix in 2026 commit 62f6281): Axios now detects and removes manual Content-Type: multipart/form-data without boundary when data is FormData, allowing native fetch to set the correct header with boundary. Recommendation: Do not manually set Content-Type when using FormData—let axios auto-detect and set it correctly. Manual setting is only needed for the automatic object-to-FormData serialization feature (v0.27+), but even then, omit the boundary. Citations:
FormData 전송 시 수동으로 Content-Type 헤더 설정하지 말 것 axios가 FormData를 감지하여 boundary를 포함한 헤더를 자동 설정하므로, 수동으로 제안 수정 export const createProduct = async (formData: FormData) => {
- const response = await client.post('/api/v1/seller/boards', formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- })
+ const response = await client.post('/api/v1/seller/boards', formData)
return response.data
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { queryOptions } from '@tanstack/react-query' | ||
|
|
||
| import { getMyStore } from './create.api' | ||
|
|
||
| export const productKeys = { | ||
| all: ['products'] as const, | ||
| myStore: () => [...productKeys.all, 'myStore'] as const, | ||
| } | ||
|
|
||
| export const productQueries = { | ||
| myStore: () => | ||
| queryOptions({ | ||
| queryKey: productKeys.myStore(), | ||
| queryFn: getMyStore, | ||
| }), | ||
| } | ||
|
Comment on lines
+1
to
+16
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
export const productQueries = {
all: () => ['products'] as const,
myStore: () =>
queryOptions({
queryKey: [...productQueries.all(), 'myStore'],
queryFn: getMyStore,
}),
}
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // API 요청 타입 | ||
| export interface CreateProductRequest { | ||
| storeId: number | ||
| title: string | ||
| isFresh: boolean | ||
| productionStartAt: string | ||
| price: number | ||
| discountType: 'AMOUNT' | 'RATE' | ||
| discountValue: number | ||
| deliveryCondition: string | ||
| deliveryCompany: string | ||
| deliveryFee: number | ||
| freeShippingConditions: number | ||
| content: string | ||
| products: ProductOptionRequest[] | ||
| productInfoNoticeRequest: Record<string, string> | ||
|
Comment on lines
+6
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP '\b(productionStartAt|productionStartTime|boardDetailRequest\.content)\b' -C2Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 1599 🏁 Script executed: # 백엔드 API 스키마나 DTO 정의 찾기
find . -type f \( -name "*.ts" -o -name "*.java" -o -name "*.dto.ts" \) | head -20Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 1062 🏁 Script executed: # 매퍼 전체 컨텍스트 확인 - multipartData.append 패턴 분석
rg -A1 -B1 "multipartData\.append" apps/seller/src/features/products/create/create-form/create-form-mapper.ts | head -50Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 1426 🏁 Script executed: # 백엔드 타입 정의가 있는지 확인
find . -path "*/backend/*" -o -path "*/api/*" | grep -E "(dto|entity|type|schema)" | head -20Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 70 🏁 Script executed: # create.type.ts 전체 파일 확인하여 필드명 정확히 파악
cat apps/seller/src/entity/products/create/create-form/create.type.tsRepository: eco-dessert-platform/dessert-front-dashboard Length of output: 1394 타입 정의가 실제 API 필드명과 불일치 - 매퍼와 타입 계약이 어긋남
|
||
| } | ||
|
|
||
| export interface ProductOptionRequest { | ||
| title: string | ||
| category: string | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Optional 정리
|
||
| plusPriceWithBoardPrice: number | ||
| stock: number | ||
| dietaryTags: { | ||
| glutenFreeTag: boolean | ||
| highProteinTag: boolean | ||
| sugarFreeTag: boolean | ||
| veganTag: boolean | ||
| ketogenicTag: boolean | ||
| } | ||
| availability: { | ||
| monday: boolean | ||
| tuesday: boolean | ||
| wednesday: boolean | ||
| thursday: boolean | ||
| friday: boolean | ||
| saturday: boolean | ||
| sunday: boolean | ||
| } | ||
| nutritionInfo: { | ||
| totalWeight: number | ||
| servingSize: number | ||
| carbohydrates: number | ||
| sugars: number | ||
| protein: number | ||
| fat: number | ||
| calories: number | ||
| } | null | ||
|
Comment on lines
+40
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| // API 응답 타입 | ||
| export interface StoreInfo { | ||
| storeId: number | ||
| name: string | ||
| introduce: string | ||
| profile: string | ||
| phoneNumber: string | ||
| email: string | ||
| } | ||
|
|
||
| export interface ApiResponse<T> { | ||
| success: boolean | ||
| code: number | ||
| message: string | ||
| result: T | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export * from './create-form.types' | ||
| export * from './create-indivisual-form.type' | ||
| export { getMyStore, createProduct } from './create.api' | ||
| export { productQueries, productKeys } from './create.query' | ||
| export type { | ||
| CreateProductRequest, | ||
| ProductOptionRequest, | ||
| StoreInfo, | ||
| ApiResponse, | ||
| } from './create.type' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { CategoryOptions } from './category-options.constants' | ||
| export { EssentialOptions } from './essential-options.constants' | ||
| export type { OptionTags } from './options-tag.type' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { ProductDiscountType } from './product-discount-type.constants' | ||
| export { productionTimes } from './production-time.constants' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| export const ProductDiscountType = [ | ||
| { | ||
| label: '원', | ||
| value: 'won', | ||
| value: 'AMOUNT', | ||
| }, | ||
| { | ||
| label: '%', | ||
| value: 'percentage', | ||
| value: 'RATE', | ||
| }, | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,29 @@ | ||
| // export const productionTimes = [ | ||
| // { | ||
| // label: '03:00~04:00', | ||
| // value: '03:00~04:00', | ||
| // }, | ||
| // { | ||
| // label: '04:00~05:00', | ||
| // value: '04:00~05:00', | ||
| // }, | ||
| // { | ||
| // label: '05:00~06:00', | ||
| // value: '05:00~06:00', | ||
| // }, | ||
| // { | ||
| // label: '06:00~07:00', | ||
| // value: '06:00~07:00', | ||
| // }, | ||
| // { | ||
| // label: '07:00~08:00', | ||
| // value: '07:00~08:00', | ||
| // }, | ||
| // ] | ||
| export const productionTimes = [ | ||
| { | ||
| label: '03:00~04:00', | ||
| value: '03:00~04:00', | ||
| }, | ||
| { | ||
| label: '04:00~05:00', | ||
| value: '04:00~05:00', | ||
| }, | ||
| { | ||
| label: '05:00~06:00', | ||
| value: '05:00~06:00', | ||
| }, | ||
| { | ||
| label: '06:00~07:00', | ||
| value: '06:00~07:00', | ||
| }, | ||
| { | ||
| label: '07:00~08:00', | ||
| value: '07:00~08:00', | ||
| }, | ||
| { label: '03:00~04:00', value: '03:00' }, | ||
| { label: '04:00~05:00', value: '04:00' }, | ||
| { label: '05:00~06:00', value: '05:00' }, | ||
| { label: '06:00~07:00', value: '06:00' }, | ||
| { label: '07:00~08:00', value: '07:00' }, | ||
| ] | ||
|
Comment on lines
+1
to
29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 주석 처리된 이전 코드 제거 권장 + value 의미 확인 원인: 22줄의 주석 블록은 dead code이고, |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export { NUTRITION_FIELDS } from './product-nutritions.constant' | ||
| export type { NutritionFieldKey } from './product-nutritions.constant' | ||
| export { | ||
| MAIN_CATEGORY_OPTIONS, | ||
| SUB_CATEGORY_MAP, | ||
| } from './product-options.constant' | ||
| export { SHIPPING_DAYS } from './product-shipping-days.constant' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| export type ProductFileType = Record<string, boolean> | ||
|
|
||
| export interface NutritionData { | ||
| sugar: number | null | ||
| protein: number | null | ||
| fat: number | null | ||
| ingredientCategories: string[] | ||
| } | ||
|
|
||
| export interface ActiveTags { | ||
| [key: string]: boolean | ||
| } | ||
|
|
||
| export interface CreateFormHeaderType { | ||
| productFields: ProductFileType | ||
| currentStep: number | ||
| headerHeight: number | ||
|
|
||
| setProductFields: React.Dispatch<React.SetStateAction<ProductFileType>> | ||
| setCurrentStep: React.Dispatch<React.SetStateAction<number>> | ||
| setHeaderHeight: React.Dispatch<React.SetStateAction<number>> | ||
| scrollToStep: (index: number) => void | ||
|
|
||
| isScrollingToStep: React.MutableRefObject<boolean> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain🌐 Web query:
💡 Result: No, React.MutableRefObject is not deprecated in React 19. Instead, in the Citations:
🏁 Script executed: cat -n apps/seller/src/entity/products/create/create-store/create-store-header.type.ts | head -40Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 1209 🏁 Script executed: rg "isScrollingToStep" apps/seller/src/entity/products/create --type ts --type tsx -B 2 -A 2Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 116 🏁 Script executed: rg "isScrollingToStep" apps/seller/src/entity/products/create -B 2 -A 2Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 620 🏁 Script executed: rg "isScrollingToStep" apps/seller/src --B 3 -A 3Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 494 🏁 Script executed: rg "isScrollingToStep" apps/seller/src -B 3 -A 3Repository: eco-dessert-platform/dessert-front-dashboard Length of output: 3534 React 19에서 React 19의 |
||
|
|
||
| nutritionDataList: NutritionData[] | ||
| setNutritionData: (index: number, data: NutritionData) => void | ||
| activeTags: ActiveTags | ||
|
|
||
| productPrice: number | null | ||
| setProductPrice: React.Dispatch<React.SetStateAction<number | null>> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './create-store-header.type' |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,24 @@ | ||
| import { useEffect, useState } from 'react' | ||
|
|
||
| export function useNumberInput( | ||
| value: number | null, | ||
| value: number | null | undefined, // undefined 추가 | ||
| onChange: (value: number | null) => void, | ||
| options?: { allowNegative?: boolean }, | ||
| ) { | ||
| const { allowNegative = false } = options ?? {} | ||
|
|
||
| // 💡 undefined 체크 추가하여 에러 방지 | ||
| const [displayValue, setDisplayValue] = useState( | ||
| value !== null ? value.toLocaleString('ko-KR') : '', | ||
| value !== null && value !== undefined ? value.toLocaleString('ko-KR') : '', | ||
| ) | ||
|
|
||
| useEffect(() => { | ||
| setDisplayValue(value !== null ? value.toLocaleString('ko-KR') : '') | ||
| // 💡 undefined 체크 추가하여 에러 방지 | ||
| setDisplayValue( | ||
| value !== null && value !== undefined | ||
| ? value.toLocaleString('ko-KR') | ||
| : '', | ||
| ) | ||
| }, [value]) | ||
|
Comment on lines
11
to
22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial LGTM (Optional 1건)
|
||
|
|
||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
|
|
@@ -22,7 +28,6 @@ export function useNumberInput( | |
| ? raw.replace(/[^0-9-]/g, '').replace(/(?!^)-/g, '') | ||
| : raw.replace(/[^0-9]/g, '') | ||
|
|
||
| // '-' 만 입력된 중간 상태는 표시만 유지, onChange는 호출 안 함 | ||
| if (cleaned === '-') { | ||
| setDisplayValue('-') | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { useFloatInput } from './create-form-float-input.hook' | ||
| export { useNumberInput } from './create-form-number-input.hook' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
주석 처리된 코드 제거 권장
원인:
storageGuide항목이 주석으로 남아 의도가 불명확합니다.영향: 향후 유지보수 시 혼란, dead code 누적.
대안: 영구 제외라면 주석 라인 삭제, 일시적이라면 이유를 짧게 명시하세요.