'use client'; import { useState } from 'react'; import { FoxFileUpload } from '@fox/core/components/fox-file-upload'; import { FoxHelperText } from '@fox/core/components/fox-helper-text'; import { FoxSortableList } from '@fox/core/components/fox-sortable-list'; import { FoxTextButton } from '@fox/core/components/fox-text-button'; import { FILE_MODULE, fileModuleAccept } from '@/lib/domain/file-module'; import { MAX_EXTRA_IMAGES, type MerchantImageSlot, } from '@/lib/domain/merchant-detail'; import styles from './merchant-image-field.module.scss'; interface MerchantImageFieldProps { defaultThumbnailUrl: string | null; retainedThumbnailId: string | null; defaultImageUrls: readonly string[]; retainedImageFileId: string | null; thumbnailError?: string; imagesError?: string; } const ACCEPT = fileModuleAccept(FILE_MODULE.merchantImage); const IMAGE_HINT = 'JPG · PNG / 5MB 이하'; /** * 기획 BO-MAL-002 ⑤ — 대표 1장 필수 + 추가 최대 11장, 추가 이미지는 타일을 끌어 순서를 바꾼다. * 타일 아래에는 「삭제」 글자만 두고, 「추가 이미지」 캡션은 맨 끝 [+ 추가] 타일에만 붙는다 * (개정 시안 2805:1597). * * 추가 이미지는 칸마다 `imageSlot`(`saved:` | `new`)과 `imageFile`을 같은 순서로 * 싣는다. 액션은 이 둘을 자리로 짝지어 묶음을 다시 만든다 — 저장된 장은 백엔드에서 다시 읽고 * 새 장은 그대로 올린다. 그래서 순서·삭제·추가 어느 것이든 폼 한 번으로 끝난다. * * 비제어 `FoxFileUpload`를 칸마다 두고 React key로 마운트를 관리한다(4컷만화와 같은 방식). */ export function MerchantImageField({ defaultThumbnailUrl, retainedThumbnailId, defaultImageUrls, retainedImageFileId, thumbnailError, imagesError, }: MerchantImageFieldProps) { const [slots, setSlots] = useState(() => defaultImageUrls.map((url, index) => ({ key: `saved-${index + 1}`, previewUrl: url, fileSn: index + 1, })) ); const [nextKey, setNextKey] = useState(0); function addSlot() { if (slots.length >= MAX_EXTRA_IMAGES) return; setSlots((current) => [ ...current, { key: `new-${nextKey}`, previewUrl: null, fileSn: null }, ]); setNextKey((current) => current + 1); } function removeSlot(key: string) { setSlots((current) => current.filter((slot) => slot.key !== key)); } return (
대표이미지 * {thumbnailError && }
slot.key} onReorder={setSlots} ariaLabel="추가 이미지 순서" renderItem={(slot, index) => (
removeSlot(slot.key)} />
)} /> {slots.length < MAX_EXTRA_IMAGES && (