diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index b19011b..1e53ee1 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -1,8 +1,16 @@ export async function api(url, options = {}) { + const headers = { 'Content-Type': 'application/json', ...options.headers } + + // 관리자 API에는 인증 헤더 자동 추가 + if (url.startsWith('/api/admin')) { + const adminKey = localStorage.getItem('maple-admin-key') + if (adminKey) headers['x-admin-key'] = adminKey + } + const res = await fetch(url, { credentials: 'include', - headers: { 'Content-Type': 'application/json', ...options.headers }, ...options, + headers, body: options.body ? JSON.stringify(options.body) : undefined, }) diff --git a/frontend/src/features/admin/AdminImages.jsx b/frontend/src/features/admin/AdminImages.jsx index e5ed85c..cbc3b66 100644 --- a/frontend/src/features/admin/AdminImages.jsx +++ b/frontend/src/features/admin/AdminImages.jsx @@ -1,13 +1,282 @@ -export default function AdminImages() { +import { useState, useEffect, useRef } from 'react' +import { api } from '../../api/client' + +function UploadModal({ open, onClose, onUpload, uploading }) { + const [file, setFile] = useState(null) + const [name, setName] = useState('') + const [preview, setPreview] = useState(null) + const fileInputRef = useRef(null) + const [dragOver, setDragOver] = useState(false) + + useEffect(() => { + if (!open) { + setFile(null) + setName('') + setPreview(null) + } + }, [open]) + + const handleFile = (f) => { + if (!f || !f.type.startsWith('image/')) return + setFile(f) + setName(f.name.replace(/\.[^.]+$/, '')) + const reader = new FileReader() + reader.onload = (e) => setPreview(e.target.result) + reader.readAsDataURL(f) + } + + const handleSubmit = async (e) => { + e.preventDefault() + if (!file || !name.trim()) return + await onUpload({ file, name: name.trim() }) + } + + if (!open) return null + return ( -
-
-

이미지 관리

-

공용 이미지를 업로드하고 관리합니다

-
-
- 준비 중 +
+
e.stopPropagation()}> +
+

이미지 업로드

+ +
+ +
+ {/* 파일 업로드 영역 */} + + + {/* 이름 */} +
+ + setName(e.target.value)} + placeholder="예: 강렬한 힘의 결정" + className="w-full rounded-lg border border-white/10 bg-gray-950 px-3 py-2 text-sm outline-none focus:border-emerald-500/50 transition" + /> +
+ + {/* 버튼 */} +
+ + +
+
) } + +function ImageCard({ image, onDelete }) { + const [showMenu, setShowMenu] = useState(false) + const [copied, setCopied] = useState(false) + + const copyUrl = () => { + navigator.clipboard.writeText(image.url) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( +
+ {/* 이미지 영역 */} +
+ {image.name} + + {/* 액션 버튼 */} +
+ + +
+
+ + {/* 정보 */} +
+
{image.name}
+
{image.url}
+
+
+ ) +} + +export default function AdminImages() { + const [images, setImages] = useState([]) + const [loading, setLoading] = useState(true) + const [modalOpen, setModalOpen] = useState(false) + const [uploading, setUploading] = useState(false) + const [search, setSearch] = useState('') + + 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 ({ file, name }) => { + setUploading(true) + try { + const formData = new FormData() + formData.append('file', file) + formData.append('name', name) + + const adminKey = localStorage.getItem('maple-admin-key') + const res = await fetch('/api/admin/images', { + method: 'POST', + headers: { 'x-admin-key': adminKey }, + body: formData, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({})) + throw new Error(err.error || '업로드 실패') + } + setModalOpen(false) + await fetchImages() + } catch (err) { + alert(err.message) + } finally { + setUploading(false) + } + } + + const handleDelete = async (image) => { + if (!confirm(`"${image.name}" 이미지를 삭제하시겠습니까?`)) return + try { + await api(`/api/admin/images/${image.id}`, { method: 'DELETE' }) + await fetchImages() + } catch (err) { + alert(err.message) + } + } + + const filtered = images.filter((img) => + img.name.toLowerCase().includes(search.toLowerCase()) + ) + + return ( +
+
+
+

이미지 관리

+

공용 이미지를 업로드하고 관리합니다

+
+ +
+ + {/* 검색 */} + {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) => ( + + ))} +
+ )} + + setModalOpen(false)} + onUpload={handleUpload} + uploading={uploading} + /> +
+ ) +}