import { useState, useEffect } from 'react'
import { api } from '../../api/client'
/* ── 공용 모달 ── */
function Modal({ open, onClose, title, children, maxWidth = 'max-w-md' }) {
if (!open) return null
return (
selectMode && onToggle(image.id)}
className={`group relative rounded-xl border overflow-hidden transition ${
selected
? 'border-emerald-500/60 bg-emerald-500/5 ring-2 ring-emerald-500/30'
: 'border-white/5 bg-gray-900/40 hover:border-white/15'
} ${selectMode ? 'cursor-pointer' : ''}`}
>
{/* 체크박스 (선택모드) */}
{selectMode && (
{selected && ✓}
)}

{!selectMode && (
)}
)
}
/* ── 메인 ── */
export default function AdminImages() {
const [images, setImages] = useState([])
const [loading, setLoading] = useState(true)
const [uploadOpen, setUploadOpen] = useState(false)
const [uploading, setUploading] = useState(false)
const [search, setSearch] = useState('')
const [selectMode, setSelectMode] = useState(false)
const [selectedIds, setSelectedIds] = useState(new Set())
const [confirmDelete, setConfirmDelete] = useState(null) // {ids, names}
const [deleting, setDeleting] = useState(false)
const [copiedId, setCopiedId] = useState(null)
const fetchImages = async () => {
setLoading(true)
try {
const data = await api('/api/admin/images')
setImages(data)
} catch {
setImages([])
} finally {
setLoading(false)
}
}
useEffect(() => { fetchImages() }, [])
const handleUpload = async (items) => {
setUploading(true)
try {
const formData = new FormData()
items.forEach((it) => {
formData.append('files', it.file)
formData.append('names', it.name.trim())
})
const adminKey = localStorage.getItem('maple-admin-key')
const res = await fetch('/api/admin/images', {
method: 'POST',
headers: { 'x-admin-key': adminKey },
body: formData,
})
const result = await res.json()
if (!res.ok) throw new Error(result.error || '업로드 실패')
if (result.errors?.length > 0) {
alert(`일부 업로드 실패:\n${result.errors.map((e) => `- ${e.name}: ${e.error}`).join('\n')}`)
}
setUploadOpen(false)
await fetchImages()
} catch (err) {
alert(err.message)
} finally {
setUploading(false)
}
}
const toggleSelect = (id) => {
setSelectedIds((prev) => {
const next = new Set(prev)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
}
const toggleSelectMode = () => {
setSelectMode((prev) => !prev)
setSelectedIds(new Set())
}
const filtered = images.filter((img) =>
img.name.toLowerCase().includes(search.toLowerCase())
)
const selectAll = () => {
if (selectedIds.size === filtered.length) {
setSelectedIds(new Set())
} else {
setSelectedIds(new Set(filtered.map((img) => img.id)))
}
}
const requestDelete = () => {
const items = images.filter((img) => selectedIds.has(img.id))
setConfirmDelete({
ids: items.map((i) => i.id),
names: items.map((i) => i.name),
})
}
const handleDeleteConfirm = async () => {
setDeleting(true)
try {
await api('/api/admin/images/delete', {
method: 'POST',
body: { ids: confirmDelete.ids },
})
setConfirmDelete(null)
setSelectedIds(new Set())
setSelectMode(false)
await fetchImages()
} catch (err) {
alert(err.message)
} finally {
setDeleting(false)
}
}
const copyUrl = (image) => {
navigator.clipboard.writeText(image.url)
setCopiedId(image.id)
setTimeout(() => setCopiedId(null), 1500)
}
return (
이미지 관리
공용 이미지를 업로드하고 관리합니다
{selectMode ? (
<>
{selectedIds.size}개 선택
>
) : (
<>
{images.length > 0 && (
)}
>
)}
{/* 검색 */}
{images.length > 0 && (
setSearch(e.target.value)}
placeholder="이미지 이름으로 검색..."
className="w-full rounded-lg border border-white/10 bg-gray-900/50 pl-10 pr-4 py-2.5 text-sm outline-none focus:border-emerald-500/50 transition"
/>
🔍
)}
{/* 이미지 그리드 */}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : filtered.length === 0 ? (
🖼️
{images.length === 0 ? '업로드된 이미지가 없습니다' : '검색 결과가 없습니다'}
{images.length === 0 && (
)}
) : (
{filtered.map((image) => (
))}
)}
setUploadOpen(false)}
onUpload={handleUpload}
uploading={uploading}
existingNames={new Set(images.map((img) => img.name))}
/>
setConfirmDelete(null)}
onConfirm={handleDeleteConfirm}
title="이미지 삭제"
description={confirmDelete ? `${confirmDelete.ids.length}개의 이미지를 삭제하시겠습니까?\n\n${confirmDelete.names.slice(0, 5).map((n) => `· ${n}`).join('\n')}${confirmDelete.names.length > 5 ? `\n· 외 ${confirmDelete.names.length - 5}개` : ''}\n\n이 작업은 되돌릴 수 없습니다.` : ''}
confirmText="삭제"
destructive
loading={deleting}
/>
)
}