import { useCallback, useEffect, useRef, useState } from 'react'; import { FiRefreshCw, FiUploadCloud, FiX } from 'react-icons/fi'; // 어드민에서 이미지를 업로드하고 결과 URL 을 form 상태에 전달하는 공용 컴포넌트. // props // value: 현재 저장된 URL (비어있으면 빈 업로드 박스 노출) // onChange: (newUrl: string) => void — 업로드 성공 시 호출 // previewAlt: img alt 텍스트 // className: 외곽 래퍼 커스터마이징 function formatBytes(bytes) { if (bytes == null || Number.isNaN(bytes)) return null; if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${Math.round(bytes / 102.4) / 10}KB`; return `${Math.round(bytes / (1024 * 102.4)) / 10}MB`; } function ImageUploader({ value, onChange, previewAlt = 'Upload preview', className = '', preset, }) { const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); const [dragOver, setDragOver] = useState(false); // 해상도 · 용량 · MIME. 업로드 직후엔 서버 응답에서, prefill/기존 URL 에는 img.onload 로 해상도만 채움. const [meta, setMeta] = useState({ width: null, height: null, bytes: null, mime: null, }); // 방금 업로드한 URL 을 기억해둬서, value 변경이 "우리 업로드에서 온 것" 인지 // "외부에서 다른 URL 이 주입된 것" 인지 구분한다. 전자는 bytes/mime 을 유지, // 후자는 네 필드 모두 초기화해 이전 업로드의 메타가 다른 이미지에 붙어 보이지 않도록 한다. const lastUploadedUrlRef = useRef(null); useEffect(() => { if (value && value === lastUploadedUrlRef.current) { setMeta((prev) => ({ width: null, height: null, bytes: prev.bytes, mime: prev.mime, })); } else { setMeta({ width: null, height: null, bytes: null, mime: null }); } }, [value]); const handleImageLoad = useCallback((event) => { const img = event.currentTarget; setMeta((prev) => ({ ...prev, width: img.naturalWidth, height: img.naturalHeight, })); }, []); async function handleFiles(fileList) { if (!fileList || fileList.length === 0) return; setError(''); setUploading(true); try { const form = new FormData(); form.append('file', fileList[0]); const endpoint = preset ? `/api/admin/uploads?preset=${encodeURIComponent(preset)}` : '/api/admin/uploads'; const res = await fetch(endpoint, { method: 'POST', credentials: 'include', body: form, }); if (res.status === 401) { setError('로그인이 만료되었습니다. 다시 로그인 해주세요.'); return; } const payload = await res.json().catch(() => null); if (!res.ok) { setError(payload?.error?.message ?? `업로드 실패 (${res.status})`); return; } const data = payload?.data; if (!data?.url) { setError('서버가 URL 을 돌려주지 않았습니다.'); return; } setMeta({ width: null, height: null, bytes: data.bytes ?? null, mime: data.mime ?? null, }); lastUploadedUrlRef.current = data.url; onChange(data.url); } catch (err) { console.error('[ImageUploader] failed', err); setError('네트워크 오류로 업로드에 실패했습니다.'); } finally { setUploading(false); if (inputRef.current) inputRef.current.value = ''; } } function openPicker() { if (uploading) return; inputRef.current?.click(); } function handleDrop(event) { event.preventDefault(); setDragOver(false); handleFiles(event.dataTransfer?.files); } function handleDragOver(event) { event.preventDefault(); if (!dragOver) setDragOver(true); } function handleDragLeave() { setDragOver(false); } function clear() { if (uploading) return; setMeta({ width: null, height: null, bytes: null, mime: null }); onChange(''); } const metaLabel = [ meta.width && meta.height ? `${meta.width}×${meta.height}` : null, formatBytes(meta.bytes), meta.mime, ] .filter(Boolean) .join(' · '); const dropRingClass = dragOver ? 'ring-2 ring-indigo-400 dark:ring-indigo-500' : ''; return (
{value ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {previewAlt}
{metaLabel && (

{metaLabel}

)}

{value}

) : ( )} handleFiles(e.target.files)} /> {error && (

{error}

)}
); } export default ImageUploader;