@withwiz/gallery 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +484 -0
- package/README.md +484 -0
- package/dist/chunk-B3MQL6ZR.mjs +79 -0
- package/dist/chunk-B3MQL6ZR.mjs.map +1 -0
- package/dist/chunk-BC3TPJQY.mjs +122 -0
- package/dist/chunk-BC3TPJQY.mjs.map +1 -0
- package/dist/chunk-BTLELFKC.mjs +11 -0
- package/dist/chunk-BTLELFKC.mjs.map +1 -0
- package/dist/chunk-GDUGKVDQ.mjs +50 -0
- package/dist/chunk-GDUGKVDQ.mjs.map +1 -0
- package/dist/chunk-GFOWZGJX.js +50 -0
- package/dist/chunk-GFOWZGJX.js.map +1 -0
- package/dist/chunk-ILSZPLME.js +47 -0
- package/dist/chunk-ILSZPLME.js.map +1 -0
- package/dist/chunk-M7CCTDUG.js +122 -0
- package/dist/chunk-M7CCTDUG.js.map +1 -0
- package/dist/chunk-WVRTM6E7.js +19 -0
- package/dist/chunk-WVRTM6E7.js.map +1 -0
- package/dist/chunk-XLPMNJJV.js +11 -0
- package/dist/chunk-XLPMNJJV.js.map +1 -0
- package/dist/chunk-XSPWM4DD.js +79 -0
- package/dist/chunk-XSPWM4DD.js.map +1 -0
- package/dist/chunk-YUMB3GBN.mjs +47 -0
- package/dist/chunk-YUMB3GBN.mjs.map +1 -0
- package/dist/chunk-ZEZCUJJD.mjs +19 -0
- package/dist/chunk-ZEZCUJJD.mjs.map +1 -0
- package/dist/components/gallery.css +1074 -0
- package/dist/components/index.d.mts +156 -0
- package/dist/components/index.d.ts +156 -0
- package/dist/components/index.js +1215 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +1215 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/errors-B9gzUsX-.d.ts +39 -0
- package/dist/errors-DYrlDCzP.d.mts +39 -0
- package/dist/gallery.css +1074 -0
- package/dist/hooks/index.d.mts +65 -0
- package/dist/hooks/index.d.ts +65 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +14 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +37 -0
- package/dist/index.mjs.map +1 -0
- package/dist/presets/ballet.d.mts +64 -0
- package/dist/presets/ballet.d.ts +64 -0
- package/dist/presets/ballet.js +162 -0
- package/dist/presets/ballet.js.map +1 -0
- package/dist/presets/ballet.mjs +162 -0
- package/dist/presets/ballet.mjs.map +1 -0
- package/dist/server/index.d.mts +157 -0
- package/dist/server/index.d.ts +157 -0
- package/dist/server/index.js +619 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +619 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +137 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.mts +22 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.ts +22 -0
- package/dist/validators/index.d.mts +68 -0
- package/dist/validators/index.d.ts +68 -0
- package/dist/validators/index.js +7 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/index.mjs +7 -0
- package/dist/validators/index.mjs.map +1 -0
- package/package.json +85 -0
- package/prisma/gallery.schema.prisma +62 -0
- package/prisma/migrations/2026-05-24-enum-to-table.sql +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/components/index.js","../../src/components/ToggleSwitch.tsx","../../src/components/ImageDropZone.tsx","../../src/components/GalleryManagerLayout.tsx","../../src/components/GalleryEditForm.tsx","../../src/components/GalleryHomePreview.tsx","../../src/components/GalleryAdminManager.tsx","../../src/components/CategoryAdminManager.tsx"],"names":["jsx","jsxs","useRef","t","useCallback","useEffect","useMemo","useState"],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/components/index.js","sourcesContent":[null,"import { type JSX, type KeyboardEvent } from \"react\";\nimport { cn } from \"../utils/cn\";\n\nexport interface ToggleSwitchProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n disabled?: boolean;\n /** aria-label 및 시각적 라벨로 모두 사용 (지정 시 옆에 텍스트 노출). */\n label?: string;\n /** size 클래스 — gallery.css 의 `.gallery-toggle--sm/--md/--lg`. 기본 \"md\". */\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n}\n\n/** 접근성 - 키보드 정책:\n * - role=\"switch\" + aria-checked\n * - Space / Enter 키로 toggle\n * - disabled 시 모든 인터랙션 차단 + aria-disabled\n *\n * CSS:\n * - `.gallery-toggle` 기본 컨테이너\n * - `.gallery-toggle--checked` / `--disabled` modifier\n * - `.gallery-toggle--sm/--md/--lg` size modifier\n * - `.gallery-toggle__track` / `__thumb` element classes\n * - `.gallery-toggle__label` element class (label 텍스트)\n */\nexport function ToggleSwitch(props: ToggleSwitchProps): JSX.Element {\n const { checked, onChange, disabled = false, label, size = \"md\", className } = props;\n\n const handleToggle = () => {\n if (disabled) return;\n onChange(!checked);\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n if (disabled) return;\n if (e.key === \" \" || e.key === \"Enter\") {\n e.preventDefault();\n onChange(!checked);\n }\n };\n\n return (\n <span className={cn(\"gallery-toggle\", `gallery-toggle--${size}`, className)}>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-disabled={disabled || undefined}\n aria-label={label}\n disabled={disabled}\n onClick={handleToggle}\n onKeyDown={handleKeyDown}\n className={cn(\n \"gallery-toggle__track\",\n checked && \"gallery-toggle--checked\",\n disabled && \"gallery-toggle--disabled\",\n )}\n >\n <span className=\"gallery-toggle__thumb\" aria-hidden=\"true\" />\n </button>\n {label ? <span className=\"gallery-toggle__label\">{label}</span> : null}\n </span>\n );\n}\n","import { useRef, type JSX } from \"react\";\nimport { useImageDropZone } from \"../hooks/useImageDropZone\";\nimport { cn } from \"../utils/cn\";\n\nexport interface ImageDropZoneProps {\n onFiles: (files: File[]) => void | Promise<void>;\n accept?: string[];\n maxSize?: number;\n multiple?: boolean;\n disabled?: boolean;\n /** 현재 표시할 미리보기 이미지 URL (선택). null/undefined 면 placeholder 표시. */\n previewUrl?: string | null;\n /** i18n 키 미주입 시 fallback 텍스트. host 의 i18n 시스템과 무관. */\n i18n?: {\n dropPrompt?: string;\n replacePrompt?: string;\n rejected?: string;\n };\n className?: string;\n}\n\nconst DEFAULT_TEXT = {\n dropPrompt: \"Drop image here or click to browse\",\n replacePrompt: \"Drop a new image to replace\",\n rejected: \"Some files were rejected\",\n};\n\n/** primitive UI 드롭존. useImageDropZone hook 위에 얹은 thin component.\n *\n * - 클릭 시 hidden `<input type=\"file\">` 트리거\n * - previewUrl 있으면 이미지 표시 + replace 안내\n * - isDragging 상태에서 `.gallery-dropzone--dragging` 클래스\n * - rejectedReasons 가 있으면 `.gallery-dropzone__rejected` 영역 표시\n */\nexport function ImageDropZone(props: ImageDropZoneProps): JSX.Element {\n const {\n onFiles,\n accept,\n maxSize,\n multiple,\n disabled = false,\n previewUrl,\n i18n,\n className,\n } = props;\n\n const inputRef = useRef<HTMLInputElement>(null);\n\n const { isDragging, dropProps, inputProps, rejectedReasons } = useImageDropZone({\n onFiles,\n accept,\n maxSize,\n multiple,\n disabled,\n });\n\n const text = {\n dropPrompt: i18n?.dropPrompt ?? DEFAULT_TEXT.dropPrompt,\n replacePrompt: i18n?.replacePrompt ?? DEFAULT_TEXT.replacePrompt,\n rejected: i18n?.rejected ?? DEFAULT_TEXT.rejected,\n };\n\n const openPicker = () => {\n if (disabled) return;\n inputRef.current?.click();\n };\n\n const containerClass = cn(\n \"gallery-dropzone\",\n isDragging && \"gallery-dropzone--dragging\",\n disabled && \"gallery-dropzone--disabled\",\n previewUrl && \"gallery-dropzone--has-preview\",\n className,\n );\n\n return (\n <div className=\"gallery-dropzone-wrap\">\n <input\n ref={inputRef}\n {...inputProps}\n disabled={disabled}\n className=\"gallery-dropzone__input\"\n style={{ position: \"absolute\", width: 1, height: 1, opacity: 0, pointerEvents: \"none\" }}\n aria-hidden=\"true\"\n tabIndex={-1}\n />\n <div\n className={containerClass}\n onClick={openPicker}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n openPicker();\n }\n }}\n {...dropProps}\n >\n {previewUrl ? (\n <>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={previewUrl}\n alt=\"\"\n className=\"gallery-dropzone__preview\"\n />\n <div className=\"gallery-dropzone__prompt gallery-dropzone__prompt--overlay\">\n {text.replacePrompt}\n </div>\n </>\n ) : (\n <div className=\"gallery-dropzone__prompt\">{text.dropPrompt}</div>\n )}\n </div>\n {rejectedReasons.length > 0 ? (\n <ul className=\"gallery-dropzone__rejected\" role=\"alert\">\n <li className=\"gallery-dropzone__rejected-title\">{text.rejected}</li>\n {rejectedReasons.map((reason, i) => (\n <li key={i} className=\"gallery-dropzone__rejected-item\">\n {reason}\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n","import { type JSX, type ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\nimport type { GalleryI18nKey, GallerySlotName } from \"../types\";\n\nexport interface GalleryManagerLayoutProps {\n /** 좌측 페인 — 호스트가 렌더하는 갤러리 목록. */\n listRoot: ReactNode;\n /** 좌측 페인 상단 필터/검색 컨트롤 (선택). */\n filterControls?: ReactNode;\n /** 좌측 페인 위의 툴바 — bulk actions 등 (선택). */\n toolbar?: ReactNode;\n /** 중앙 페인 — 편집 폼. null 이면 emptyState 사용. */\n editForm: ReactNode | null;\n /** editForm 이 null 일 때 표시할 컨텐츠 (미주입 시 i18n fallback). */\n emptyState?: ReactNode;\n /** 우측 페인 — 홈 프리뷰 (선택). 미주입 시 우측 영역 자체가 렌더되지 않음. */\n homePreview?: ReactNode;\n className?: string;\n /** i18n key fallback (영문). */\n i18n?: Partial<Record<GalleryI18nKey, string>>;\n /** 호스트 customization 지점. slots 가 같은 키의 ReactNode prop 보다 우선. */\n ui?: {\n classNames?: Partial<Record<GallerySlotName, string>>;\n slots?: Partial<Record<GallerySlotName, ReactNode>>;\n };\n}\n\nconst DEFAULT_TEXT: Partial<Record<GalleryI18nKey, string>> = {\n \"admin.emptyState\": \"Select an item to edit, or create a new one.\",\n};\n\nfunction t(\n i18n: Partial<Record<GalleryI18nKey, string>> | undefined,\n key: GalleryI18nKey,\n): string {\n return i18n?.[key] ?? DEFAULT_TEXT[key] ?? key;\n}\n\n/** Gallery admin 의 3-페인 레이아웃. AdminManagerBase 의존 없는 self-contained.\n *\n * - 좌측: toolbar + filterControls + listRoot\n * - 중앙: editForm (없으면 emptyState / i18n fallback)\n * - 우측: homePreview (미주입 시 영역 자체 없음 → grid 2-cols)\n *\n * slot 우선순위:\n * ui.slots[name] > 같은 의미의 props (toolbar/filterControls/listRoot 등) > fallback\n * className 병합:\n * ui.classNames[name] 이 해당 slot wrapper 의 className 에 추가됨.\n */\nexport function GalleryManagerLayout(props: GalleryManagerLayoutProps): JSX.Element {\n const {\n listRoot,\n filterControls,\n toolbar,\n editForm,\n emptyState,\n homePreview,\n className,\n i18n,\n ui,\n } = props;\n\n const slots = ui?.slots ?? {};\n const classes = ui?.classNames ?? {};\n\n const toolbarContent = slots.managerToolbar ?? toolbar;\n const listContent = slots.listRoot ?? listRoot;\n const editContent = slots.editFormRoot ?? editForm;\n const previewContent = slots.homePreviewRoot ?? homePreview;\n const emptyContent =\n slots.emptyState ?? emptyState ?? <span>{t(i18n, \"admin.emptyState\")}</span>;\n\n const hasPreview = previewContent !== undefined && previewContent !== null;\n\n return (\n <div\n className={cn(\n \"gallery-manager\",\n hasPreview ? \"gallery-manager--with-preview\" : \"gallery-manager--no-preview\",\n classes.managerRoot,\n className,\n )}\n >\n <aside className={cn(\"gallery-manager__list\", classes.listRoot)}>\n {toolbarContent ? (\n <div className={cn(\"gallery-manager__toolbar\", classes.managerToolbar)}>\n {toolbarContent}\n </div>\n ) : null}\n {filterControls ? (\n <div className=\"gallery-manager__filter\">{filterControls}</div>\n ) : null}\n <div className=\"gallery-manager__list-body\">{listContent}</div>\n </aside>\n\n <section className={cn(\"gallery-manager__form\", classes.editFormRoot)}>\n {editContent !== null && editContent !== undefined ? (\n editContent\n ) : (\n <div className={cn(\"gallery-manager__empty\", classes.emptyState)}>{emptyContent}</div>\n )}\n </section>\n\n {hasPreview ? (\n <aside className={cn(\"gallery-manager__preview\", classes.homePreviewRoot)}>\n {previewContent}\n </aside>\n ) : null}\n </div>\n );\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ChangeEvent,\n type JSX,\n} from \"react\";\nimport { cn } from \"../utils/cn\";\nimport { ToggleSwitch } from \"./ToggleSwitch\";\nimport { ImageDropZone } from \"./ImageDropZone\";\nimport type {\n CreateGalleryInput,\n GalleryCategoryItem,\n GalleryI18nKey,\n GalleryListItem,\n UpdateGalleryInput,\n} from \"../types\";\n\nexport interface GalleryEditFormProps {\n /** 편집 대상. null 이면 새로 생성 모드. */\n value: GalleryListItem | null;\n categories: GalleryCategoryItem[];\n onSubmit: (data: CreateGalleryInput | UpdateGalleryInput) => void | Promise<void>;\n onCancel?: () => void;\n onDelete?: () => void | Promise<void>;\n /** 다중 모드 (새 모드 + 여러 이미지). onSubmitMany 가 함께 주입되어야 동작. */\n multipleMode?: boolean;\n onSubmitMany?: (items: CreateGalleryInput[]) => void | Promise<void>;\n /** featured 토글 가능 여부. false 이고 현재 미선택 상태이면 토글 비활성. true 이면 자유. */\n canToggleFeatured?: boolean;\n saving?: boolean;\n /** 새 모드에서 이미지 파일을 host 가 업로드하고 그 결과 URL 을 반환. 미주입 시 ObjectURL preview 만 가능. */\n onImageSelect?: (file: File) => Promise<{ url: string; key?: string }>;\n i18n?: Partial<Record<GalleryI18nKey, string>>;\n className?: string;\n}\n\ninterface FormState {\n imageUrl: string;\n imageKey: string;\n caption: string;\n categoryId: string;\n sortOrder: number;\n featured: boolean;\n published: boolean;\n}\n\ninterface MultiImage {\n url: string;\n key?: string;\n}\n\nconst DEFAULT_TEXT: Partial<Record<GalleryI18nKey, string>> = {\n \"form.caption\": \"Caption\",\n \"form.captionPlaceholder\": \"Image description (optional)\",\n \"form.category\": \"Category\",\n \"form.sortOrder\": \"Sort order\",\n \"form.published\": \"Published\",\n \"form.featured\": \"Featured\",\n \"form.save\": \"Save\",\n \"form.cancel\": \"Cancel\",\n \"form.imageDropPrompt\": \"Drop image here or click to browse\",\n \"form.imageReplacePrompt\": \"Drop a new image to replace\",\n \"admin.featuredOverLimit\": \"Featured cap reached — uncheck others first\",\n};\n\nfunction t(\n i18n: Partial<Record<GalleryI18nKey, string>> | undefined,\n key: GalleryI18nKey,\n): string {\n return i18n?.[key] ?? DEFAULT_TEXT[key] ?? key;\n}\n\nfunction buildInitialForm(\n value: GalleryListItem | null,\n categories: GalleryCategoryItem[],\n): FormState {\n if (value) {\n return {\n imageUrl: value.imageUrl,\n imageKey: value.imageKey ?? \"\",\n caption: value.caption ?? \"\",\n categoryId: value.categoryId,\n sortOrder: value.sortOrder,\n featured: value.featured,\n published: value.published,\n };\n }\n return {\n imageUrl: \"\",\n imageKey: \"\",\n caption: \"\",\n categoryId: categories[0]?.id ?? \"\",\n sortOrder: 0,\n featured: false,\n published: false,\n };\n}\n\n/** Gallery admin edit form — self-contained.\n *\n * 단일 모드: 1 image + 메타. value.imageUrl 이 비어있으면 새 이미지를 onImageSelect 로 업로드.\n * 다중 모드 (multipleMode + value=null): 여러 이미지를 자동 caption suffix + sortOrder auto-increment 로 일괄 생성.\n */\nexport function GalleryEditForm(props: GalleryEditFormProps): JSX.Element {\n const {\n value,\n categories,\n onSubmit,\n onCancel,\n onDelete,\n multipleMode = false,\n onSubmitMany,\n canToggleFeatured = true,\n saving = false,\n onImageSelect,\n i18n,\n className,\n } = props;\n\n const isNew = value === null;\n const [form, setForm] = useState<FormState>(() => buildInitialForm(value, categories));\n const [multiImages, setMultiImages] = useState<MultiImage[]>([]);\n const [imageError, setImageError] = useState<string | null>(null);\n const valueIdRef = useRef<string | null>(value?.id ?? null);\n const dirtyRef = useRef(false);\n\n // value 가 null → non-null 또는 다른 id 로 변경되면 form 을 재초기화.\n // 단, 사용자가 입력 중인 경우 (dirty) 는 덮어쓰지 않음.\n useEffect(() => {\n const newId = value?.id ?? null;\n if (newId !== valueIdRef.current) {\n valueIdRef.current = newId;\n if (!dirtyRef.current) {\n setForm(buildInitialForm(value, categories));\n }\n } else if (value && !dirtyRef.current) {\n // 같은 id 이지만 value 내용이 바뀐 경우 (refresh 후 갱신).\n setForm(buildInitialForm(value, categories));\n }\n }, [value, categories]);\n\n // categories 가 늦게 로드된 경우, form.categoryId 가 비어있으면 첫 항목으로 채움.\n useEffect(() => {\n if (!form.categoryId && categories.length > 0) {\n setForm((f) => ({ ...f, categoryId: categories[0].id }));\n }\n }, [categories, form.categoryId]);\n\n const isMultiActive = isNew && multipleMode && multiImages.length > 1;\n\n // featured 토글의 disabled 결정.\n // canToggleFeatured=false && 현재 form.featured=false → 켜는 것은 차단. 끄는 것은 가능.\n const featuredDisabled = !canToggleFeatured && !form.featured;\n\n const updateField = useCallback(<K extends keyof FormState>(key: K, val: FormState[K]) => {\n dirtyRef.current = true;\n setForm((prev) => ({ ...prev, [key]: val }));\n }, []);\n\n const handleFiles = useCallback(\n async (files: File[]) => {\n if (files.length === 0) return;\n setImageError(null);\n if (!onImageSelect) {\n setImageError(\"onImageSelect prop missing — host must provide upload handler\");\n return;\n }\n try {\n if (isNew && multipleMode) {\n // 다중 모드 — 각 파일 업로드 후 multiImages 누적.\n const uploaded = await Promise.all(files.map((f) => onImageSelect(f)));\n setMultiImages((prev) => {\n const next = [...prev, ...uploaded];\n // 첫 번째 이미지를 form.imageUrl 로 반영 (단일 저장 fallback 용).\n setForm((f) => {\n if (!f.imageUrl && next.length > 0) {\n return { ...f, imageUrl: next[0].url, imageKey: next[0].key ?? \"\" };\n }\n return f;\n });\n return next;\n });\n } else {\n // 단일 모드 — 첫 파일만 사용.\n const r = await onImageSelect(files[0]);\n setForm((f) => ({ ...f, imageUrl: r.url, imageKey: r.key ?? \"\" }));\n }\n } catch (err) {\n setImageError(err instanceof Error ? err.message : \"Image upload failed\");\n }\n },\n [isNew, multipleMode, onImageSelect],\n );\n\n const handleSubmit = useCallback(async () => {\n if (isMultiActive && onSubmitMany) {\n const items: CreateGalleryInput[] = multiImages.map((img, i) => ({\n imageUrl: img.url,\n imageKey: img.key || undefined,\n caption: form.caption ? `${form.caption}_${i + 1}` : undefined,\n categoryId: form.categoryId,\n sortOrder: form.sortOrder + i,\n featured: form.featured,\n published: form.published,\n }));\n await onSubmitMany(items);\n return;\n }\n\n if (!form.imageUrl) {\n setImageError(\"Image is required\");\n return;\n }\n\n const payload: CreateGalleryInput | UpdateGalleryInput = {\n imageUrl: form.imageUrl,\n imageKey: form.imageKey || undefined,\n caption: form.caption || undefined,\n categoryId: form.categoryId,\n sortOrder: form.sortOrder,\n featured: form.featured,\n published: form.published,\n };\n await onSubmit(payload);\n }, [form, isMultiActive, multiImages, onSubmit, onSubmitMany]);\n\n const captionPreview = useMemo(() => {\n if (!isMultiActive || !form.caption) return null;\n return multiImages\n .slice(0, 3)\n .map((_, i) => `${form.caption}_${i + 1}`)\n .join(\", \") + (multiImages.length > 3 ? \", ...\" : \"\");\n }, [isMultiActive, multiImages, form.caption]);\n\n return (\n <div className={cn(\"gallery-edit-form\", className)}>\n <header className=\"gallery-edit-form__header\">\n <h2 className=\"gallery-edit-form__title\">\n {isNew\n ? isMultiActive\n ? `Batch (${multiImages.length})`\n : \"New image\"\n : \"Edit image\"}\n </h2>\n <div className=\"gallery-edit-form__actions\" data-toggle-group>\n <span data-toggle=\"featured\">\n <ToggleSwitch\n checked={form.featured}\n onChange={(v) => updateField(\"featured\", v)}\n label={t(i18n, \"form.featured\")}\n disabled={featuredDisabled}\n size=\"sm\"\n />\n </span>\n {featuredDisabled ? (\n <span className=\"gallery-edit-form__note\">{t(i18n, \"admin.featuredOverLimit\")}</span>\n ) : null}\n <span data-toggle=\"published\">\n <ToggleSwitch\n checked={form.published}\n onChange={(v) => updateField(\"published\", v)}\n label={t(i18n, \"form.published\")}\n size=\"sm\"\n />\n </span>\n {!isNew && onDelete ? (\n <button\n type=\"button\"\n className=\"gallery-edit-form__btn gallery-edit-form__btn--danger\"\n onClick={() => void onDelete()}\n >\n Delete\n </button>\n ) : null}\n {onCancel ? (\n <button\n type=\"button\"\n className=\"gallery-edit-form__btn\"\n onClick={onCancel}\n >\n {t(i18n, \"form.cancel\")}\n </button>\n ) : null}\n <button\n type=\"button\"\n className=\"gallery-edit-form__btn gallery-edit-form__btn--primary\"\n onClick={() => void handleSubmit()}\n disabled={saving}\n >\n {saving ? \"Saving...\" : isMultiActive ? `Save ${multiImages.length}` : t(i18n, \"form.save\")}\n </button>\n </div>\n </header>\n\n <div className=\"gallery-edit-form__section\">\n <div className=\"gallery-edit-form__section-title\">Image</div>\n <ImageDropZone\n onFiles={handleFiles}\n previewUrl={form.imageUrl || null}\n multiple={isNew && multipleMode}\n i18n={{\n dropPrompt: t(i18n, \"form.imageDropPrompt\"),\n replacePrompt: t(i18n, \"form.imageReplacePrompt\"),\n }}\n />\n {imageError ? (\n <div className=\"gallery-edit-form__error\" role=\"alert\">{imageError}</div>\n ) : null}\n {isMultiActive ? (\n <div className=\"gallery-edit-form__multi-grid\">\n {multiImages.map((img, i) => (\n <div key={img.key ?? `mi-${i}`} className=\"gallery-edit-form__multi-tile\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={img.url} alt=\"\" className=\"gallery-edit-form__multi-img\" />\n <button\n type=\"button\"\n className=\"gallery-edit-form__multi-remove\"\n onClick={() => {\n setMultiImages((prev) => {\n const next = prev.filter((_, idx) => idx !== i);\n setForm((f) => {\n if (next.length > 0) {\n return { ...f, imageUrl: next[0].url, imageKey: next[0].key ?? \"\" };\n }\n return { ...f, imageUrl: \"\", imageKey: \"\" };\n });\n return next;\n });\n }}\n aria-label=\"remove\"\n >\n ×\n </button>\n </div>\n ))}\n </div>\n ) : null}\n </div>\n\n <div className=\"gallery-edit-form__section\">\n <div className=\"gallery-edit-form__section-title\">{t(i18n, \"form.caption\")}</div>\n <input\n type=\"text\"\n name=\"caption\"\n className=\"gallery-edit-form__input\"\n value={form.caption}\n maxLength={200}\n placeholder={t(i18n, \"form.captionPlaceholder\")}\n onChange={(e: ChangeEvent<HTMLInputElement>) => updateField(\"caption\", e.target.value)}\n />\n {captionPreview ? (\n <div className=\"gallery-edit-form__hint\">Preview: {captionPreview}</div>\n ) : null}\n </div>\n\n <div className=\"gallery-edit-form__row\">\n <div className=\"gallery-edit-form__section\">\n <div className=\"gallery-edit-form__section-title\">{t(i18n, \"form.category\")}</div>\n <select\n name=\"categoryId\"\n className=\"gallery-edit-form__input\"\n value={form.categoryId}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => updateField(\"categoryId\", e.target.value)}\n >\n {categories.map((c) => (\n <option key={c.id} value={c.id}>{c.labelKo}</option>\n ))}\n </select>\n </div>\n <div className=\"gallery-edit-form__section\">\n <div className=\"gallery-edit-form__section-title\">{t(i18n, \"form.sortOrder\")}</div>\n <input\n type=\"number\"\n name=\"sortOrder\"\n className=\"gallery-edit-form__input\"\n value={form.sortOrder}\n min={0}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n const n = parseInt(e.target.value, 10);\n updateField(\"sortOrder\", Number.isNaN(n) ? 0 : n);\n }}\n />\n {isMultiActive ? (\n <div className=\"gallery-edit-form__hint\">\n {form.sortOrder} ~ {form.sortOrder + multiImages.length - 1}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n","import { useCallback, useRef, useState, type DragEvent, type JSX } from \"react\";\nimport { cn } from \"../utils/cn\";\nimport type { GalleryI18nKey, GalleryListItem } from \"../types\";\n\nexport interface GalleryHomePreviewProps {\n /** 표시할 항목들 — 호출자가 published & featured 필터 + 정렬을 마친 상태. */\n items: GalleryListItem[];\n /** 모자이크 셀 개수 (host config.limits.mosaicCount). */\n maxCount: number;\n /** drag-and-drop 으로 재정렬된 결과를 통보. orderedIds 의 길이 = 현재 보이는 items 수. */\n onReorder: (orderedIds: string[]) => void | Promise<void>;\n /** ★ 토글. next = 현재 featured 의 반대값. */\n onToggleFeatured: (id: string, next: boolean) => void | Promise<void>;\n i18n?: Partial<Record<GalleryI18nKey, string>>;\n className?: string;\n}\n\nconst DEFAULT_TEXT: Partial<Record<GalleryI18nKey, string>> = {\n \"preview.title\": \"Home preview\",\n \"preview.dragHint\": \"Drag tiles to reorder\",\n \"preview.emptyState\": \"Select images to feature on the home page\",\n};\n\nfunction t(\n i18n: Partial<Record<GalleryI18nKey, string>> | undefined,\n key: GalleryI18nKey,\n): string {\n return i18n?.[key] ?? DEFAULT_TEXT[key] ?? key;\n}\n\n/** 어드민 홈 프리뷰 — 모자이크 그리드 + HTML5 drag swap + 별 토글.\n *\n * drag 동작:\n * 1. dragStart 에서 dragIndex 기록\n * 2. dragOver 에서 dragOverIndex 기록 (visual highlight 용)\n * 3. drop 시 dragIndex 와 drop target index 가 다르면 swap 후 onReorder\n *\n * 키보드 접근성은 v0.2 — 현재는 mouse 만.\n */\nexport function GalleryHomePreview(props: GalleryHomePreviewProps): JSX.Element {\n const { items, maxCount, onReorder, onToggleFeatured, i18n, className } = props;\n\n const [dragIndex, setDragIndex] = useState<number | null>(null);\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);\n const reorderedRef = useRef<string[] | null>(null);\n\n const handleDragStart = useCallback(\n (e: DragEvent<HTMLDivElement>, index: number) => {\n setDragIndex(index);\n try {\n e.dataTransfer.effectAllowed = \"move\";\n } catch {}\n },\n [],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>, index: number) => {\n e.preventDefault();\n try {\n e.dataTransfer.dropEffect = \"move\";\n } catch {}\n setDragOverIndex(index);\n },\n [],\n );\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLDivElement>, dropIndex: number) => {\n e.preventDefault();\n if (dragIndex === null || dragIndex === dropIndex) {\n reorderedRef.current = null;\n return;\n }\n const reordered = [...items];\n const tmp = reordered[dragIndex];\n reordered[dragIndex] = reordered[dropIndex];\n reordered[dropIndex] = tmp;\n reorderedRef.current = reordered.map((it) => it.id);\n },\n [dragIndex, items],\n );\n\n const handleDragEnd = useCallback(() => {\n if (reorderedRef.current) {\n void onReorder(reorderedRef.current);\n }\n reorderedRef.current = null;\n setDragIndex(null);\n setDragOverIndex(null);\n }, [onReorder]);\n\n const handleStarClick = useCallback(\n (item: GalleryListItem) => {\n void onToggleFeatured(item.id, !item.featured);\n },\n [onToggleFeatured],\n );\n\n if (items.length === 0) {\n return (\n <div className={cn(\"gallery-home-preview\", \"gallery-home-preview--empty\", className)}>\n <div className=\"gallery-home-preview__header\">\n <h3 className=\"gallery-home-preview__title\">{t(i18n, \"preview.title\")}</h3>\n </div>\n <div className=\"gallery-home-preview__empty\">{t(i18n, \"preview.emptyState\")}</div>\n </div>\n );\n }\n\n return (\n <div className={cn(\"gallery-home-preview\", className)}>\n <div className=\"gallery-home-preview__header\">\n <h3 className=\"gallery-home-preview__title\">{t(i18n, \"preview.title\")}</h3>\n <span className=\"gallery-home-preview__count\">\n {items.length}/{maxCount}\n </span>\n </div>\n <div className=\"gallery-home-preview__hint\">{t(i18n, \"preview.dragHint\")}</div>\n <div\n className=\"gallery-home-preview__mosaic\"\n data-count={maxCount}\n >\n {Array.from({ length: maxCount }).map((_, slotIndex) => {\n const item = items[slotIndex];\n if (!item) {\n return (\n <div\n key={`empty-${slotIndex}`}\n className=\"gallery-home-preview__tile gallery-home-preview__tile--empty\"\n >\n <span className=\"gallery-home-preview__order\">{slotIndex + 1}</span>\n </div>\n );\n }\n return (\n <div\n key={item.id}\n className={cn(\n \"gallery-home-preview__tile\",\n dragOverIndex === slotIndex && \"gallery-home-preview__tile--drag-over\",\n dragIndex === slotIndex && \"gallery-home-preview__tile--dragging\",\n )}\n draggable\n onDragStart={(e) => handleDragStart(e, slotIndex)}\n onDragOver={(e) => handleDragOver(e, slotIndex)}\n onDrop={(e) => handleDrop(e, slotIndex)}\n onDragEnd={handleDragEnd}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={item.imageUrl}\n alt={item.caption ?? \"\"}\n className=\"gallery-home-preview__img\"\n />\n <button\n type=\"button\"\n className={cn(\n \"gallery-home-preview__star\",\n item.featured && \"gallery-home-preview__star--active\",\n )}\n onClick={() => handleStarClick(item)}\n aria-label={item.featured ? \"unfeature\" : \"feature\"}\n aria-pressed={item.featured}\n >\n {item.featured ? \"★\" : \"☆\"}\n </button>\n <span className=\"gallery-home-preview__order\">{slotIndex + 1}</span>\n {item.caption ? (\n <div className=\"gallery-home-preview__caption\">{item.caption}</div>\n ) : null}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { cn } from \"../utils/cn\";\nimport { getGalleryConfig } from \"../config\";\nimport { GalleryManagerLayout } from \"./GalleryManagerLayout\";\nimport { GalleryEditForm } from \"./GalleryEditForm\";\nimport { GalleryHomePreview } from \"./GalleryHomePreview\";\nimport type {\n CreateGalleryInput,\n GalleryCategoryItem,\n GalleryI18nKey,\n GalleryListItem,\n UpdateGalleryInput,\n} from \"../types\";\n\nexport interface GalleryAdminManagerProps {\n /** 초기 mode (라우트에서 결정). undefined = list view. */\n initialMode?: \"list\" | \"new\" | \"edit\";\n /** 편집 대상 ID (initialMode === \"edit\" 또는 URL 진입 시). */\n initialSelectedId?: string;\n className?: string;\n}\n\n// host 가 더 정교한 인증 fetcher 가 필요하면 v0.2 에 config.clientFetch 추가 검토.\nasync function clientFetch(url: string, init?: RequestInit): Promise<Response> {\n return fetch(url, {\n credentials: \"include\",\n headers: init?.body ? { \"Content-Type\": \"application/json\", ...(init?.headers as any) } : init?.headers,\n ...init,\n });\n}\n\nasync function jsonOrNull(res: Response): Promise<any> {\n try {\n return await res.json();\n } catch {\n return null;\n }\n}\n\nconst DEFAULT_TEXT: Partial<Record<GalleryI18nKey, string>> = {\n \"admin.title\": \"Gallery\",\n \"admin.newButton\": \"+ New\",\n \"admin.searchPlaceholder\": \"Search...\",\n \"admin.emptyState\": \"Select an item to edit\",\n};\n\nfunction t(\n i18n: Partial<Record<GalleryI18nKey, string>> | undefined,\n key: GalleryI18nKey,\n): string {\n return i18n?.[key] ?? DEFAULT_TEXT[key] ?? key;\n}\n\n/** 메인 마운트 포인트. host 의 페이지는 단순히 `<GalleryAdminManager initialSelectedId={id} />` 마운트. */\nexport function GalleryAdminManager(props: GalleryAdminManagerProps): JSX.Element {\n const { initialMode, initialSelectedId, className } = props;\n const config = getGalleryConfig();\n const i18n = (config.i18n ?? {}) as Partial<Record<GalleryI18nKey, string>>;\n const ui = config.ui;\n const maxFeatured = config.limits.maxFeatured;\n const mosaicCount = config.limits.mosaicCount;\n\n const [items, setItems] = useState<GalleryListItem[]>([]);\n const [categories, setCategories] = useState<GalleryCategoryItem[]>([]);\n const [selectedId, setSelectedId] = useState<string | null>(initialSelectedId ?? null);\n const [mode, setMode] = useState<\"list\" | \"new\" | \"edit\">(\n initialMode ?? (initialSelectedId ? \"edit\" : \"list\"),\n );\n const [saving, setSaving] = useState(false);\n const [search, setSearch] = useState(\"\");\n const [refreshKey, setRefreshKey] = useState(0);\n const mountedRef = useRef(true);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // 목록 로드\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const res = await clientFetch(\"/api/admin/galleries\");\n const json = await jsonOrNull(res);\n if (!cancelled && mountedRef.current) {\n const data = json?.data ?? json ?? [];\n setItems(Array.isArray(data) ? data : []);\n }\n } catch {}\n })();\n return () => {\n cancelled = true;\n };\n }, [refreshKey]);\n\n // 카테고리 로드\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const res = await clientFetch(\"/api/admin/gallery-categories\");\n const json = await jsonOrNull(res);\n if (!cancelled && mountedRef.current) {\n const data = json?.data ?? json ?? [];\n setCategories(Array.isArray(data) ? data : []);\n }\n } catch {}\n })();\n return () => {\n cancelled = true;\n };\n }, []);\n\n const selectedItem = useMemo(() => {\n if (!selectedId) return null;\n return items.find((i) => i.id === selectedId) ?? null;\n }, [selectedId, items]);\n\n const filteredItems = useMemo(() => {\n if (!search.trim()) return items;\n const q = search.trim().toLowerCase();\n return items.filter((g) => (g.caption ?? \"\").toLowerCase().includes(q));\n }, [items, search]);\n\n const featuredCount = useMemo(\n () => items.filter((i) => i.featured && i.published).length,\n [items],\n );\n const previewItems = useMemo(\n () =>\n items\n .filter((i) => i.featured && i.published)\n .sort((a, b) => a.sortOrder - b.sortOrder),\n [items],\n );\n\n const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);\n\n const handleSelect = useCallback((id: string) => {\n setSelectedId(id);\n setMode(\"edit\");\n }, []);\n\n const handleNewClick = useCallback(() => {\n setSelectedId(null);\n setMode(\"new\");\n }, []);\n\n const handleCancel = useCallback(() => {\n setSelectedId(null);\n setMode(\"list\");\n }, []);\n\n const handleSubmit = useCallback(\n async (data: CreateGalleryInput | UpdateGalleryInput) => {\n setSaving(true);\n try {\n if (mode === \"new\") {\n await clientFetch(\"/api/admin/galleries\", {\n method: \"POST\",\n body: JSON.stringify(data),\n });\n } else if (selectedId) {\n await clientFetch(`/api/admin/galleries/${selectedId}`, {\n method: \"PUT\",\n body: JSON.stringify(data),\n });\n }\n if (mountedRef.current) {\n refresh();\n setMode(\"list\");\n setSelectedId(null);\n }\n } finally {\n if (mountedRef.current) setSaving(false);\n }\n },\n [mode, selectedId, refresh],\n );\n\n const handleSubmitMany = useCallback(\n async (batch: CreateGalleryInput[]) => {\n setSaving(true);\n try {\n await clientFetch(\"/api/admin/galleries/bulk\", {\n method: \"POST\",\n body: JSON.stringify({ items: batch }),\n });\n if (mountedRef.current) {\n refresh();\n setMode(\"list\");\n }\n } finally {\n if (mountedRef.current) setSaving(false);\n }\n },\n [refresh],\n );\n\n const handleDelete = useCallback(async () => {\n if (!selectedId) return;\n if (typeof window !== \"undefined\" && !window.confirm(\"Delete this item?\")) return;\n await clientFetch(`/api/admin/galleries/${selectedId}`, { method: \"DELETE\" });\n if (mountedRef.current) {\n setSelectedId(null);\n setMode(\"list\");\n refresh();\n }\n }, [selectedId, refresh]);\n\n const handleToggleFeatured = useCallback(\n async (id: string, next: boolean) => {\n if (next && featuredCount >= maxFeatured) {\n if (typeof window !== \"undefined\") {\n window.alert(t(i18n, \"admin.featuredOverLimit\"));\n }\n return;\n }\n await clientFetch(`/api/admin/galleries/${id}`, {\n method: \"PUT\",\n body: JSON.stringify({ featured: next }),\n });\n if (mountedRef.current) refresh();\n },\n [featuredCount, maxFeatured, i18n, refresh],\n );\n\n const handleReorder = useCallback(\n async (orderedIds: string[]) => {\n await Promise.all(\n orderedIds.map((id, index) =>\n clientFetch(`/api/admin/galleries/${id}`, {\n method: \"PUT\",\n body: JSON.stringify({ sortOrder: index + 1 }),\n }),\n ),\n );\n if (mountedRef.current) refresh();\n },\n [refresh],\n );\n\n const isMultipleMode = mode === \"new\";\n const editForm =\n mode === \"list\"\n ? null\n : (\n <GalleryEditForm\n key={mode === \"new\" ? \"new\" : selectedId ?? \"edit\"}\n value={mode === \"edit\" ? selectedItem : null}\n categories={categories}\n saving={saving}\n multipleMode={isMultipleMode}\n canToggleFeatured={featuredCount < maxFeatured || (selectedItem?.featured ?? false)}\n i18n={i18n}\n onSubmit={handleSubmit}\n onSubmitMany={handleSubmitMany}\n onCancel={handleCancel}\n onDelete={mode === \"edit\" ? handleDelete : undefined}\n />\n );\n\n const filterControls = (\n <div className=\"gallery-manager__filter-controls\">\n <input\n type=\"search\"\n className=\"gallery-manager__search\"\n placeholder={t(i18n, \"admin.searchPlaceholder\")}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <span className=\"gallery-manager__count\">{filteredItems.length}</span>\n </div>\n );\n\n const toolbar = (\n <div className=\"gallery-manager__toolbar-row\">\n <h1 className=\"gallery-manager__title\">{t(i18n, \"admin.title\")}</h1>\n <button\n type=\"button\"\n className=\"gallery-manager__new-btn\"\n onClick={handleNewClick}\n >\n {t(i18n, \"admin.newButton\")}\n </button>\n </div>\n );\n\n const listRoot = (\n <ul className=\"gallery-manager__list-ul\">\n {filteredItems.map((item) => (\n <li\n key={item.id}\n className={cn(\"gallery-list-item\", selectedId === item.id && \"gallery-list-item--active\")}\n onClick={() => handleSelect(item.id)}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={item.imageUrl} alt=\"\" className=\"gallery-list-item__thumb\" />\n <div className=\"gallery-list-item__body\">\n <div className=\"gallery-list-item__caption\">\n {item.caption || \"(no caption)\"}\n </div>\n <div className=\"gallery-list-item__meta\">\n {item.featured ? \"★ \" : \"\"}\n {item.published ? \"공개\" : \"비공개\"} · 순서 {item.sortOrder}\n </div>\n </div>\n </li>\n ))}\n </ul>\n );\n\n const homePreview = (\n <GalleryHomePreview\n items={previewItems}\n maxCount={mosaicCount}\n onReorder={handleReorder}\n onToggleFeatured={handleToggleFeatured}\n i18n={i18n}\n />\n );\n\n return (\n <GalleryManagerLayout\n className={className}\n listRoot={listRoot}\n filterControls={filterControls}\n toolbar={toolbar}\n editForm={editForm}\n homePreview={homePreview}\n i18n={i18n}\n ui={ui}\n />\n );\n}\n","import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type JSX,\n} from \"react\";\nimport { cn } from \"../utils/cn\";\nimport { getGalleryConfig } from \"../config\";\nimport { ToggleSwitch } from \"./ToggleSwitch\";\nimport type { GalleryCategoryItem, GalleryI18nKey } from \"../types\";\n\nexport interface CategoryAdminManagerProps {\n className?: string;\n}\n\nconst DEFAULT_TEXT: Partial<Record<GalleryI18nKey, string>> = {\n \"category.title\": \"Gallery categories\",\n \"category.newButton\": \"+ New category\",\n \"category.slug\": \"Slug (uppercase)\",\n \"category.labelKo\": \"Label (Ko)\",\n \"category.labelEn\": \"Label (En)\",\n \"category.deleteConfirmInUse\": \"This category has galleries — remove or move them first\",\n};\n\nfunction t(\n i18n: Partial<Record<GalleryI18nKey, string>> | undefined,\n key: GalleryI18nKey,\n): string {\n return i18n?.[key] ?? DEFAULT_TEXT[key] ?? key;\n}\n\ninterface FormState {\n id: string | null;\n slug: string;\n labelKo: string;\n labelEn: string;\n sortOrder: number;\n isActive: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n id: null,\n slug: \"\",\n labelKo: \"\",\n labelEn: \"\",\n sortOrder: 0,\n isActive: true,\n};\n\nasync function clientFetch(url: string, init?: RequestInit): Promise<Response> {\n return fetch(url, {\n credentials: \"include\",\n headers: init?.body ? { \"Content-Type\": \"application/json\", ...(init?.headers as any) } : init?.headers,\n ...init,\n });\n}\n\nasync function jsonOrNull(res: Response): Promise<any> {\n try {\n return await res.json();\n } catch {\n return null;\n }\n}\n\n/** 카테고리 어드민 UI. 사용 중인 카테고리는 server 가 409 로 반환 → 안내 표시. */\nexport function CategoryAdminManager(props: CategoryAdminManagerProps): JSX.Element {\n const { className } = props;\n const config = getGalleryConfig();\n const i18n = (config.i18n ?? {}) as Partial<Record<GalleryI18nKey, string>>;\n\n const [items, setItems] = useState<GalleryCategoryItem[]>([]);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n const [refreshKey, setRefreshKey] = useState(0);\n const [saving, setSaving] = useState(false);\n const mountedRef = useRef(true);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const res = await clientFetch(\"/api/admin/gallery-categories\");\n const json = await jsonOrNull(res);\n if (!cancelled && mountedRef.current) {\n const data = json?.data ?? json ?? [];\n setItems(Array.isArray(data) ? data : []);\n }\n } catch {}\n })();\n return () => {\n cancelled = true;\n };\n }, [refreshKey]);\n\n const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);\n\n const handleEdit = useCallback((cat: GalleryCategoryItem) => {\n setError(null);\n setForm({\n id: cat.id,\n slug: cat.slug,\n labelKo: cat.labelKo,\n labelEn: cat.labelEn ?? \"\",\n sortOrder: cat.sortOrder,\n isActive: cat.isActive,\n });\n }, []);\n\n const handleNew = useCallback(() => {\n setError(null);\n setForm(EMPTY_FORM);\n }, []);\n\n const handleSave = useCallback(async () => {\n setSaving(true);\n setError(null);\n try {\n const body = JSON.stringify({\n slug: form.slug,\n labelKo: form.labelKo,\n labelEn: form.labelEn || undefined,\n sortOrder: form.sortOrder,\n isActive: form.isActive,\n });\n const url = form.id\n ? `/api/admin/gallery-categories/${form.id}`\n : \"/api/admin/gallery-categories\";\n const method = form.id ? \"PUT\" : \"POST\";\n const res = await clientFetch(url, { method, body });\n if (!res.ok) {\n const json = await jsonOrNull(res);\n setError(json?.error?.message ?? `Save failed (${res.status})`);\n return;\n }\n if (mountedRef.current) {\n setForm(EMPTY_FORM);\n refresh();\n }\n } finally {\n if (mountedRef.current) setSaving(false);\n }\n }, [form, refresh]);\n\n const handleDelete = useCallback(\n async (cat: GalleryCategoryItem) => {\n if (typeof window !== \"undefined\" && !window.confirm(`Delete \"${cat.labelKo}\"?`)) return;\n setError(null);\n const res = await clientFetch(`/api/admin/gallery-categories/${cat.id}`, {\n method: \"DELETE\",\n });\n if (res.status === 409) {\n setError(t(i18n, \"category.deleteConfirmInUse\"));\n return;\n }\n if (!res.ok) {\n const json = await jsonOrNull(res);\n setError(json?.error?.message ?? `Delete failed (${res.status})`);\n return;\n }\n if (mountedRef.current) refresh();\n },\n [i18n, refresh],\n );\n\n return (\n <div className={cn(\"gallery-category-admin\", className)}>\n <header className=\"gallery-category-admin__header\">\n <h2 className=\"gallery-category-admin__title\">{t(i18n, \"category.title\")}</h2>\n <button\n type=\"button\"\n className=\"gallery-category-admin__new\"\n onClick={handleNew}\n >\n {t(i18n, \"category.newButton\")}\n </button>\n </header>\n\n {error ? (\n <div className=\"gallery-category-admin__error\" role=\"alert\">{error}</div>\n ) : null}\n\n <ul className=\"gallery-category-admin__list\">\n {items.map((c) => (\n <li key={c.id} className=\"gallery-category-admin__item\">\n <button\n type=\"button\"\n className=\"gallery-category-admin__item-row\"\n onClick={() => handleEdit(c)}\n >\n <span className=\"gallery-category-admin__slug\">{c.slug}</span>\n <span className=\"gallery-category-admin__label\">{c.labelKo}</span>\n <span className=\"gallery-category-admin__order\">#{c.sortOrder}</span>\n <span className=\"gallery-category-admin__active\">\n {c.isActive ? \"✓\" : \"✗\"}\n </span>\n </button>\n <button\n type=\"button\"\n className=\"gallery-category-admin__delete\"\n onClick={() => void handleDelete(c)}\n aria-label=\"delete\"\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n\n <div className=\"gallery-category-admin__form\">\n <div className=\"gallery-category-admin__form-title\">\n {form.id ? \"Edit category\" : \"New category\"}\n </div>\n <div className=\"gallery-category-admin__field\">\n <label className=\"gallery-category-admin__field-label\">{t(i18n, \"category.slug\")}</label>\n <input\n name=\"slug\"\n className=\"gallery-category-admin__input\"\n value={form.slug}\n onChange={(e: ChangeEvent<HTMLInputElement>) => setForm((f) => ({ ...f, slug: e.target.value.toUpperCase() }))}\n placeholder=\"UPPER_SNAKE\"\n />\n </div>\n <div className=\"gallery-category-admin__field\">\n <label className=\"gallery-category-admin__field-label\">{t(i18n, \"category.labelKo\")}</label>\n <input\n name=\"labelKo\"\n className=\"gallery-category-admin__input\"\n value={form.labelKo}\n onChange={(e: ChangeEvent<HTMLInputElement>) => setForm((f) => ({ ...f, labelKo: e.target.value }))}\n />\n </div>\n <div className=\"gallery-category-admin__field\">\n <label className=\"gallery-category-admin__field-label\">{t(i18n, \"category.labelEn\")}</label>\n <input\n name=\"labelEn\"\n className=\"gallery-category-admin__input\"\n value={form.labelEn}\n onChange={(e: ChangeEvent<HTMLInputElement>) => setForm((f) => ({ ...f, labelEn: e.target.value }))}\n />\n </div>\n <div className=\"gallery-category-admin__field gallery-category-admin__field--row\">\n <label className=\"gallery-category-admin__field-label\">Sort order</label>\n <input\n type=\"number\"\n name=\"sortOrder\"\n className=\"gallery-category-admin__input\"\n value={form.sortOrder}\n min={0}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n const n = parseInt(e.target.value, 10);\n setForm((f) => ({ ...f, sortOrder: Number.isNaN(n) ? 0 : n }));\n }}\n />\n <ToggleSwitch\n checked={form.isActive}\n onChange={(v) => setForm((f) => ({ ...f, isActive: v }))}\n label=\"Active\"\n size=\"sm\"\n />\n </div>\n <div className=\"gallery-category-admin__form-actions\">\n {form.id ? (\n <button\n type=\"button\"\n className=\"gallery-category-admin__cancel\"\n onClick={() => setForm(EMPTY_FORM)}\n >\n Cancel\n </button>\n ) : null}\n <button\n type=\"button\"\n className=\"gallery-category-admin__save\"\n onClick={() => void handleSave()}\n disabled={saving || !form.slug || !form.labelKo}\n >\n {saving ? \"Saving...\" : form.id ? \"Update\" : \"Create\"}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"]}
|