From a9f1052671d37bbef511c28b1fb4b8e19d87b8de Mon Sep 17 00:00:00 2001 From: yeaseula Date: Mon, 20 Apr 2026 15:20:41 +0900 Subject: [PATCH 01/56] =?UTF-8?q?[seller]=20fix(173):sticky=20header=20?= =?UTF-8?q?=EC=95=84=EC=BD=94=EB=94=94=EC=96=B8=20arrow=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create/create-header/create-header.ui.tsx | 13 ++++++------- packages/ui/src/accordion/accordion.tsx | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/seller/src/features/products/create/create-header/create-header.ui.tsx b/apps/seller/src/features/products/create/create-header/create-header.ui.tsx index 62b35c51..7f2da6cb 100644 --- a/apps/seller/src/features/products/create/create-header/create-header.ui.tsx +++ b/apps/seller/src/features/products/create/create-header/create-header.ui.tsx @@ -5,13 +5,13 @@ import { AccordionTrigger, StageTab, } from '@dessert/ui' -import { ChevronDown } from 'lucide-react' +import { ChevronDownIcon } from 'lucide-react' import { CategoryOptions, EssentialOptions } from '@/entity/products' import { ProductHeaderTags } from './create-header-tags.ui' -import { useCreateFormSteps } from '../create-form/use-create-form-steps.hook' import { InfoTooltip } from '../create-form/info-tooltip.ui' +import { useCreateFormSteps } from '../create-form/use-create-form-steps.hook' const stagestep = [ '상품 정보', @@ -64,11 +64,10 @@ export const ProductHeader = () => { /> - -
- -
-
+ diff --git a/packages/ui/src/accordion/accordion.tsx b/packages/ui/src/accordion/accordion.tsx index d86c0c10..156435d6 100644 --- a/packages/ui/src/accordion/accordion.tsx +++ b/packages/ui/src/accordion/accordion.tsx @@ -33,11 +33,17 @@ function AccordionItem({ ) } +interface CustomTriggerProps + extends React.ComponentProps { + customIcon?: React.ReactNode +} + function AccordionTrigger({ className, children, + customIcon = false, ...props -}: React.ComponentProps) { +}: CustomTriggerProps) { return ( {children} From 1c3160f1c492fb6247ca117d215593b678ef909c Mon Sep 17 00:00:00 2001 From: yeaseula Date: Mon, 20 Apr 2026 17:08:04 +0900 Subject: [PATCH 02/56] =?UTF-8?q?[seller]=20feat(173):=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=ED=8F=BC=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20context=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create/create-form/product-form.type.ts | 5 + .../create-form-thumbnail-upload-area.ui.tsx | 172 ++++++++++++++++++ .../create-thumbnail-upload.schema.ts | 13 ++ .../create-form-thumbnail-upload/index.ts | 3 + .../use-product-thumnail-form.hook.ts | 30 +++ .../create-form/create-form-provider.ui.tsx | 1 + .../create-form/product-create.types.ts | 6 +- .../use-create-product-form.hook.ts | 4 + .../create/create-header/create-header.ui.tsx | 1 - .../src/features/products/create/index.ts | 1 + .../src/pages/products/create/create-page.tsx | 5 +- 11 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx create mode 100644 apps/seller/src/features/products/create/create-form-thumbnail-upload/create-thumbnail-upload.schema.ts create mode 100644 apps/seller/src/features/products/create/create-form-thumbnail-upload/index.ts create mode 100644 apps/seller/src/features/products/create/create-form-thumbnail-upload/use-product-thumnail-form.hook.ts diff --git a/apps/seller/src/entity/products/create/create-form/product-form.type.ts b/apps/seller/src/entity/products/create/create-form/product-form.type.ts index a7e102bb..ace66d6c 100644 --- a/apps/seller/src/entity/products/create/create-form/product-form.type.ts +++ b/apps/seller/src/entity/products/create/create-form/product-form.type.ts @@ -16,6 +16,11 @@ export type DeliveryFormInput = { deliveryMinFee: number | null } +export type ThumbnailFormInput = { + mainImage: File | null + extraImages: File[] +} + export type ProductOptionFormInput = { mainCategory: string subCategory: string diff --git a/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx new file mode 100644 index 00000000..f7dad5e1 --- /dev/null +++ b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useRef } from 'react' + +import { CameraIcon, XIcon } from 'lucide-react' // 아이콘 라이브러리 적절히 사용 +import { useFieldArray, useFormContext } from 'react-hook-form' + +import { cn } from '@/shared/libs/utils' + +import { useProductThumbnailForm } from './use-product-thumnail-form.hook' +import { useCreateFormSteps } from '../create-form/use-create-form-steps.hook' + +export function ThumbnailUploadArea() { + const { + form, + mainImage, + extraImages, + isFormField, + handleMainImageChange, + handleExtraImagesChange, + } = useProductThumbnailForm() + + const { setProductFields } = useCreateFormSteps() + + useEffect(() => { + setProductFields((prev) => ({ ...prev, productDelivery: isFormField })) + }, [isFormField, setProductFields]) + + // 이미지 삭제 모달 상태 관리 (커스텀 필요) + const handleDelete = (index: number | 'main') => { + // 프로젝트 공통 모달이 있다면 window.confirm 대신 교체 가능 + if (!window.confirm('이미지를 제거하시겠습니까?')) return + + if (index === 'main') { + handleMainImageChange(null) + } else { + const newImages = extraImages.filter((_, i) => i !== index) + handleExtraImagesChange(newImages) + } + } + + const handleFileChange = ( + e: React.ChangeEvent, + type: 'main' | 'extra', + ) => { + const files = e.target.files + if (!files) return + + if (type === 'main') { + handleMainImageChange(files[0]) + } else { + const remainingSlots = 9 - extraImages.length + const newFiles = Array.from(files).slice(0, remainingSlots) + handleExtraImagesChange([...extraImages, ...newFiles]) + } + // 동일 파일 다시 선택 가능하도록 초기화 + e.target.value = '' + } + + return ( +
+

썸네일 등록

+ + {/* 대표 이미지 영역 */} +
+ +
+ {mainImage ? ( + handleDelete('main')} + /> + ) : ( + handleFileChange(e, 'main')} + /> + )} +

+ 권장 크기 1000×1000, 최소 160×160 이상 (1:1 비율) · jpg, jpeg, png + 형식 · 10MB 이하 파일만 업로드 가능해요 +

+
+
+ + {/* 추가 이미지 영역 */} +
+ +
+ {/* 추가 이미지들은 업로드 버튼이 항상 앞에 오거나 뒤에 오도록 배치 */} + {extraImages.length < 9 && ( + handleFileChange(e, 'extra')} + /> + )} + {extraImages.map((file: File, idx: number) => ( + handleDelete(idx)} + /> + ))} +
+
+
+ ) +} + +interface UploadButtonProps { + id: string + count: number + max: number + multiple?: boolean + onChange: (e: React.ChangeEvent) => void +} + +function UploadButton({ + id, + count, + max, + multiple = false, + onChange, +}: UploadButtonProps) { + return ( + + ) +} + +// 프리뷰 아이템 컴포넌트 +function ImagePreviewItem({ + src, + onDelete, +}: { + src: string + onDelete: () => void +}) { + return ( +
+ preview + +
+ ) +} diff --git a/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-thumbnail-upload.schema.ts b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-thumbnail-upload.schema.ts new file mode 100644 index 00000000..0305fbe8 --- /dev/null +++ b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-thumbnail-upload.schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const thumbnailSchema = z.object({ + mainImage: z + .custom() + .refine((file) => file !== null, { + message: '대표 이미지는 필수 입력사항입니다.', + }), + extraImages: z + .array(z.custom()) + .max(9, { message: '추가 이미지는 최대 9개까지 등록 가능합니다.' }) + .default([]), +}) diff --git a/apps/seller/src/features/products/create/create-form-thumbnail-upload/index.ts b/apps/seller/src/features/products/create/create-form-thumbnail-upload/index.ts new file mode 100644 index 00000000..e0dd1a33 --- /dev/null +++ b/apps/seller/src/features/products/create/create-form-thumbnail-upload/index.ts @@ -0,0 +1,3 @@ +export { thumbnailSchema } from './create-thumbnail-upload.schema' +export { ThumbnailUploadArea } from './create-form-thumbnail-upload-area.ui' +export { useProductThumbnailForm } from './use-product-thumnail-form.hook' diff --git a/apps/seller/src/features/products/create/create-form-thumbnail-upload/use-product-thumnail-form.hook.ts b/apps/seller/src/features/products/create/create-form-thumbnail-upload/use-product-thumnail-form.hook.ts new file mode 100644 index 00000000..74f82638 --- /dev/null +++ b/apps/seller/src/features/products/create/create-form-thumbnail-upload/use-product-thumnail-form.hook.ts @@ -0,0 +1,30 @@ +import { useFormContext } from 'react-hook-form' + +import { CreateProductForm } from '../create-form/product-create.types' + +export function useProductThumbnailForm() { + const form = useFormContext() + + const mainImage = form.watch('mainImage') + const extraImages = form.watch('extraImages') || [] + + // 필수 입력 사항 판별: 대표 이미지가 있으면 true + const isFormField = mainImage !== null + + const handleMainImageChange = (file: File | null) => { + form.setValue('mainImage', file, { shouldValidate: true }) + } + + const handleExtraImagesChange = (files: File[]) => { + form.setValue('extraImages', files, { shouldValidate: true }) + } + + return { + form, + mainImage, + extraImages, + isFormField, + handleMainImageChange, + handleExtraImagesChange, + } +} diff --git a/apps/seller/src/features/products/create/create-form/create-form-provider.ui.tsx b/apps/seller/src/features/products/create/create-form/create-form-provider.ui.tsx index a0633785..54bf88ee 100644 --- a/apps/seller/src/features/products/create/create-form/create-form-provider.ui.tsx +++ b/apps/seller/src/features/products/create/create-form/create-form-provider.ui.tsx @@ -10,6 +10,7 @@ export const FormStepsProvider = ({ const [productFields, setProductFields] = useState({ productInfo: false, productDelivery: false, + productThumbnail: false, productOptions: false, }) diff --git a/apps/seller/src/features/products/create/create-form/product-create.types.ts b/apps/seller/src/features/products/create/create-form/product-create.types.ts index 8ba751c6..280efd48 100644 --- a/apps/seller/src/features/products/create/create-form/product-create.types.ts +++ b/apps/seller/src/features/products/create/create-form/product-create.types.ts @@ -3,19 +3,23 @@ import { ProductDisclosureFormInput, ProductFormInput, ProductOptionFormInput, + ThumbnailFormInput, } from '@/entity/products' import { deliverySchema } from '../create-form-delivery/create-delivery.schema' import { disclosureSchema } from '../create-form-disclosure/create-disclosure.schema' import { productSchema } from '../create-form-info/create-info.schema' import { productOptionSchema } from '../create-form-options/create-options.schema' +import { thumbnailSchema } from '../create-form-thumbnail-upload' export type CreateProductForm = ProductFormInput & DeliveryFormInput & ProductOptionFormInput & - ProductDisclosureFormInput + ProductDisclosureFormInput & + ThumbnailFormInput export const createProductSchema = productSchema .and(deliverySchema) .and(productOptionSchema) .and(disclosureSchema) + .and(thumbnailSchema) diff --git a/apps/seller/src/features/products/create/create-form/use-create-product-form.hook.ts b/apps/seller/src/features/products/create/create-form/use-create-product-form.hook.ts index c2637e15..3ef9f179 100644 --- a/apps/seller/src/features/products/create/create-form/use-create-product-form.hook.ts +++ b/apps/seller/src/features/products/create/create-form/use-create-product-form.hook.ts @@ -2,6 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Resolver, useForm } from 'react-hook-form' import { DISCLOSURE_FIELDS } from '@/entity/products' + import { CreateProductForm, createProductSchema } from './product-create.types' export const useCreateProductForm = () => { @@ -20,6 +21,9 @@ export const useCreateProductForm = () => { deliveryFee: null, deliveryMinFee: null, + mainImage: null, + extraImages: [], + mainCategory: '', subCategory: '', optionName: '', diff --git a/apps/seller/src/features/products/create/create-header/create-header.ui.tsx b/apps/seller/src/features/products/create/create-header/create-header.ui.tsx index 7f2da6cb..698a554a 100644 --- a/apps/seller/src/features/products/create/create-header/create-header.ui.tsx +++ b/apps/seller/src/features/products/create/create-header/create-header.ui.tsx @@ -5,7 +5,6 @@ import { AccordionTrigger, StageTab, } from '@dessert/ui' -import { ChevronDownIcon } from 'lucide-react' import { CategoryOptions, EssentialOptions } from '@/entity/products' diff --git a/apps/seller/src/features/products/create/index.ts b/apps/seller/src/features/products/create/index.ts index 202e6b01..263cd594 100644 --- a/apps/seller/src/features/products/create/index.ts +++ b/apps/seller/src/features/products/create/index.ts @@ -5,3 +5,4 @@ export * from './create-form-disclosure' export * from './create-form-info' export * from './create-form-options' export * from './create-header' +export * from './create-form-thumbnail-upload' diff --git a/apps/seller/src/pages/products/create/create-page.tsx b/apps/seller/src/pages/products/create/create-page.tsx index e2e08057..59b09483 100644 --- a/apps/seller/src/pages/products/create/create-page.tsx +++ b/apps/seller/src/pages/products/create/create-page.tsx @@ -10,6 +10,7 @@ import { ProductHeader, ProductInfoArea, ProductOptionsArea, + ThumbnailUploadArea, useCreateProductForm, } from '@/features/products/create' @@ -35,7 +36,9 @@ function CreatePageInner() { - + + + From f60d1a3b977bd262d5d6b5fff98bb9ee6019529c Mon Sep 17 00:00:00 2001 From: yeaseula Date: Tue, 21 Apr 2026 16:36:23 +0900 Subject: [PATCH 03/56] =?UTF-8?q?[seller]=20feat(173):=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20css?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create-form-thumbnail-upload-area.ui.tsx | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx index f7dad5e1..319b0772 100644 --- a/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx +++ b/apps/seller/src/features/products/create/create-form-thumbnail-upload/create-form-thumbnail-upload-area.ui.tsx @@ -1,16 +1,14 @@ import React, { useEffect, useRef } from 'react' -import { CameraIcon, XIcon } from 'lucide-react' // 아이콘 라이브러리 적절히 사용 -import { useFieldArray, useFormContext } from 'react-hook-form' - -import { cn } from '@/shared/libs/utils' +import { Label } from '@dessert/ui' +import { Camera, XIcon } from 'lucide-react' import { useProductThumbnailForm } from './use-product-thumnail-form.hook' +import { InfoTooltip } from '../create-form/info-tooltip.ui' import { useCreateFormSteps } from '../create-form/use-create-form-steps.hook' export function ThumbnailUploadArea() { const { - form, mainImage, extraImages, isFormField, @@ -56,15 +54,21 @@ export function ThumbnailUploadArea() { } return ( -
-

썸네일 등록

- + <> +
+
{/* 대표 이미지 영역 */} -
- -
+
+