maplestory/frontend/src/features/admin/components/ImagePicker.jsx

146 lines
5.6 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { api } from '../../../api/client'
const PAGE_SIZE = 24
/**
* 업로드된 이미지 하나를 선택하는 모달 피커
*/
export default function ImagePicker({ open, onClose, onSelect, currentImageId }) {
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
useEffect(() => {
const t = setTimeout(() => {
setDebouncedSearch(search)
setPage(1)
}, 300)
return () => clearTimeout(t)
}, [search])
useEffect(() => {
if (!open) {
setSearch('')
setDebouncedSearch('')
setPage(1)
}
}, [open])
const { data, isLoading } = useQuery({
queryKey: ['admin', 'images', { page, search: debouncedSearch }],
queryFn: () => {
const params = new URLSearchParams({
page,
limit: PAGE_SIZE,
...(debouncedSearch && { search: debouncedSearch }),
})
return api(`/api/admin/images?${params}`)
},
enabled: open,
placeholderData: (prev) => prev,
})
const images = data?.items || []
const totalPages = data?.total_pages || 1
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className="w-full max-w-3xl rounded-2xl bg-gray-900 border border-white/10 shadow-2xl max-h-[90vh] flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="px-6 py-4 border-b border-white/5 flex items-center justify-between shrink-0">
<h3 className="font-semibold">이미지 선택</h3>
<button onClick={onClose} className="text-gray-500 hover:text-white transition text-xl leading-none">×</button>
</div>
{/* 검색 */}
<div className="px-6 pt-4 shrink-0">
<div className="relative">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="이미지 이름으로 검색..."
className="w-full rounded-lg border border-white/10 bg-gray-950 pl-10 pr-4 py-2.5 text-sm outline-none focus:border-emerald-500/50 transition"
/>
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">🔍</span>
</div>
</div>
{/* 이미지 그리드 */}
<div className="px-6 py-4 overflow-y-auto flex-1">
{isLoading ? (
<div className="grid gap-3 grid-cols-3 sm:grid-cols-4 lg:grid-cols-6">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="aspect-square rounded-lg bg-white/[0.02] animate-pulse" />
))}
</div>
) : images.length === 0 ? (
<div className="py-12 text-center text-gray-500 text-sm">
{debouncedSearch ? '검색 결과가 없습니다' : '업로드된 이미지가 없습니다'}
</div>
) : (
<div className="grid gap-3 grid-cols-3 sm:grid-cols-4 lg:grid-cols-6">
{images.map((image) => (
<button
key={image.id}
type="button"
onClick={() => { onSelect(image); onClose() }}
className={`group rounded-lg border overflow-hidden transition ${
currentImageId === image.id
? 'border-emerald-500/60 ring-2 ring-emerald-500/30'
: 'border-white/5 hover:border-white/20'
}`}
title={image.name}
>
<div className="aspect-square bg-gradient-to-br from-gray-900 to-gray-950 flex items-center justify-center p-3">
<img src={image.url} alt={image.name} className="max-w-full max-h-full object-contain" />
</div>
<div className="px-2 py-1.5 border-t border-white/5 bg-gray-950/50">
<div className="text-xs truncate">{image.name}</div>
</div>
</button>
))}
</div>
)}
</div>
{/* 페이지네이션 + 액션 */}
<div className="px-6 py-4 border-t border-white/5 flex items-center justify-between shrink-0 gap-3">
{totalPages > 1 ? (
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="w-8 h-8 rounded border border-white/10 hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center text-sm"
>
</button>
<span className="text-xs text-gray-400 px-2">{page} / {totalPages}</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="w-8 h-8 rounded border border-white/10 hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center text-sm"
>
</button>
</div>
) : <div />}
{currentImageId && (
<button
type="button"
onClick={() => { onSelect(null); onClose() }}
className="text-sm text-red-400 hover:text-red-300 transition"
>
이미지 제거
</button>
)}
</div>
</div>
</div>
)
}