File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'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:<fileSn>` | `new`)과 `imageFile`을 같은 순서로
* 싣는다. 액션은 이 둘을 자리로 짝지어 묶음을 다시 만든다 — 저장된 장은 백엔드에서 다시 읽고
* 새 장은 그대로 올린다. 그래서 순서·삭제·추가 어느 것이든 폼 한 번으로 끝난다.
*
* 비제어 `FoxFileUpload`를 칸마다 두고 React key로 마운트를 관리한다(4컷만화와 같은 방식).
*/
export function MerchantImageField({
defaultThumbnailUrl,
retainedThumbnailId,
defaultImageUrls,
retainedImageFileId,
thumbnailError,
imagesError,
}: MerchantImageFieldProps) {
const [slots, setSlots] = useState<MerchantImageSlot[]>(() =>
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 (
<div className={styles.root}>
<div className={styles.tiles}>
<div className={styles.thumbnail}>
<FoxFileUpload
mode="image"
name="thumbnailFile"
accept={ACCEPT}
thumbnailPicker
selectLabel="대표이미지 등록"
description={IMAGE_HINT}
thumbnailWidth={220}
thumbnailHeight={160}
defaultFiles={
defaultThumbnailUrl
? [{ id: 'thumbnail', name: '대표이미지', previewUrl: defaultThumbnailUrl }]
: undefined
}
/>
<input type="hidden" name="thumbnailFileId" value={retainedThumbnailId ?? ''} />
<span className={styles.caption}>
대표이미지 <span className={styles.required}>*</span>
</span>
{thumbnailError && <FoxHelperText type="danger" message={thumbnailError} />}
</div>
<input type="hidden" name="imageFileId" value={retainedImageFileId ?? ''} />
<FoxSortableList
items={slots}
getKey={(slot) => slot.key}
onReorder={setSlots}
ariaLabel="추가 이미지 순서"
renderItem={(slot, index) => (
<div className={styles.slot}>
<FoxFileUpload
mode="image"
name="imageFile"
accept={ACCEPT}
thumbnailPicker
selectLabel={`추가 이미지 ${index + 1} 선택`}
thumbnailWidth={160}
thumbnailHeight={160}
defaultFiles={
slot.previewUrl
? [{ id: slot.key, name: `추가 이미지 ${index + 1}`, previewUrl: slot.previewUrl }]
: undefined
}
/>
<input
type="hidden"
name="imageSlot"
value={slot.fileSn === null ? 'new' : `saved:${slot.fileSn}`}
/>
<FoxTextButton
size="xsm"
type="secondary"
label="삭제"
onAction={() => removeSlot(slot.key)}
/>
</div>
)}
/>
{slots.length < MAX_EXTRA_IMAGES && (
<div className={styles.slot}>
<button type="button" className={styles.addTile} onClick={addSlot}>
<i className="fox-ico fox-ico-Plus" aria-hidden="true" />
<span>추가</span>
</button>
<span className={styles.captionSubtle}>추가 이미지</span>
</div>
)}
</div>
{imagesError && <FoxHelperText type="danger" message={imagesError} />}
</div>
);
}