import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { DndContext, DragOverlay, closestCenter, PointerSensor, KeyboardSensor, useSensor, useSensors, } from '@dnd-kit/core' import { SortableContext, sortableKeyboardCoordinates, useSortable, rectSortingStrategy, arrayMove, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { api } from '../../../api/client' const TYPE_COLOR = { '아케인': { text: 'text-violet-300', bg: 'bg-violet-500/15', border: 'border-violet-500/30' }, '어센틱': { text: 'text-sky-300', bg: 'bg-sky-500/15', border: 'border-sky-500/30' }, '그랜드 어센틱': { text: 'text-amber-300', bg: 'bg-amber-500/15', border: 'border-amber-500/30' }, } function SymbolCardContent({ symbol, dragging = false }) { const color = TYPE_COLOR[symbol.type] || TYPE_COLOR['아케인'] return (
{symbol.image_url ? ( ) : ( ? )}

{symbol.region}

{symbol.type}
만렙 {symbol.max_level} 일퀘 {symbol.daily_default} 주간퀘 {symbol.weekly_default}
) } function SortableSymbolCard({ symbol }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, setActivatorNodeRef } = useSortable({ id: symbol.id, transition: { duration: 200, easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)' }, }) const style = { transform: CSS.Transform.toString(transform), transition } return (
) } export default function SymbolList() { const queryClient = useQueryClient() const { data: symbols = [], isLoading } = useQuery({ queryKey: ['admin', 'symbol', 'symbols'], queryFn: () => api('/api/admin/symbol/symbols').catch(() => []), }) const [items, setItems] = useState([]) const [activeId, setActiveId] = useState(null) useEffect(() => { setItems(symbols) }, [symbols]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) const reorderMutation = useMutation({ mutationFn: (ids) => api('/api/admin/symbol/symbols/reorder', { method: 'POST', body: { ids }, }), onError: (err) => { alert(err.message) queryClient.invalidateQueries({ queryKey: ['admin', 'symbol', 'symbols'] }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['symbol', 'symbols'] }) }, }) const handleDragEnd = (event) => { const { active, over } = event setActiveId(null) if (!over || active.id === over.id) return const oldIdx = items.findIndex((s) => s.id === active.id) const newIdx = items.findIndex((s) => s.id === over.id) const next = arrayMove(items, oldIdx, newIdx) setItems(next) reorderMutation.mutate(next.map((s) => s.id)) } const activeSymbol = items.find((s) => s.id === activeId) return (

심볼 관리

심볼 정보 및 레벨별 필요 개수/메소를 관리합니다

+ 심볼 추가
{isLoading ? (
{Array.from({ length: 4 }).map((_, i) => (
))}
) : items.length === 0 ? (
🔮

등록된 심볼이 없습니다

첫 심볼 추가하기 →
) : ( setActiveId(e.active.id)} onDragCancel={() => setActiveId(null)} onDragEnd={handleDragEnd} > s.id)} strategy={rectSortingStrategy}>
{items.map((s) => ( ))}
{activeSymbol ? : null}
)}
) }