import { useState, useRef, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../../../api/client'
import Select from '../../../components/Select'
import ConfirmDialog from '../../../components/ConfirmDialog'
const TYPE_OPTIONS = [
{ value: '아케인', label: '아케인' },
{ value: '어센틱', label: '어센틱' },
{ value: '그랜드 어센틱', label: '그랜드 어센틱' },
]
const inputCls = '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 formatMesoKorean(n) {
if (!n || n <= 0) return ''
const eok = Math.floor(n / 100_000_000)
const man = Math.floor((n % 100_000_000) / 10_000)
const parts = []
if (eok) parts.push(`${eok}억`)
if (man) parts.push(`${man.toLocaleString()}만`)
if (!parts.length) return `${n.toLocaleString()}`
return parts.join(' ')
}
function MesoInput({ value, onChange, ...rest }) {
const display = value === '' || value == null ? '' : Number(String(value).replace(/[^\d]/g, '')).toLocaleString()
const korean = formatMesoKorean(Number(String(value).replace(/[^\d]/g, '')) || 0)
return (
)
}
function Field({ label, hint, error, required, children }) {
return (
{hint && {hint}}
{children}
{error &&
{error}
}
)
}
export default function SymbolForm() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const { id } = useParams()
const isEdit = !!id
const fileInputRef = useRef(null)
const [type, setType] = useState('아케인')
const [region, setRegion] = useState('')
const [maxLevel, setMaxLevel] = useState('')
const [dailyDefault, setDailyDefault] = useState('')
const [weeklyDefault, setWeeklyDefault] = useState('')
const [imageFile, setImageFile] = useState(null)
const [imagePreview, setImagePreview] = useState(null)
const [existingImageUrl, setExistingImageUrl] = useState(null)
const [levels, setLevels] = useState([])
const [confirmDelete, setConfirmDelete] = useState(false)
const [error, setError] = useState('')
// 편집 시 데이터 로드
const { data: symbolData } = useQuery({
queryKey: ['admin', 'symbol', 'symbols', id],
queryFn: () => api(`/api/admin/symbol/symbols/${id}`),
enabled: isEdit,
})
useEffect(() => {
if (!symbolData) return
setType(symbolData.type)
setRegion(symbolData.region)
setMaxLevel(String(symbolData.max_level))
setDailyDefault(String(symbolData.daily_default ?? ''))
setWeeklyDefault(String(symbolData.weekly_default ?? ''))
setExistingImageUrl(symbolData.image_url)
const rows = Array.from({ length: symbolData.max_level - 1 }, (_, i) => {
const level = i + 1
const existing = symbolData.levels.find((l) => l.level === level)
return {
level,
required_count: existing?.required_count ?? '',
meso_cost: existing?.meso_cost ?? '',
}
})
setLevels(rows)
}, [symbolData])
const handleFile = (e) => {
const file = e.target.files?.[0]
if (!file) return
setImageFile(file)
setImagePreview(URL.createObjectURL(file))
}
const updateLevel = (idx, field, val) => {
setLevels((prev) => prev.map((l, i) => (i === idx ? { ...l, [field]: val } : l)))
}
const adjustLevelRows = (newMax) => {
const n = Number(newMax)
if (!n || n < 2) return
setLevels((prev) => {
const rows = Array.from({ length: n - 1 }, (_, i) => {
const level = i + 1
return prev.find((l) => l.level === level) || { level, required_count: '', meso_cost: '' }
})
return rows
})
}
const saveMutation = useMutation({
mutationFn: async () => {
const formData = new FormData()
formData.append('type', type)
formData.append('region', region.trim())
formData.append('max_level', String(maxLevel))
formData.append('daily_default', String(Number(dailyDefault) || 0))
formData.append('weekly_default', String(Number(weeklyDefault) || 0))
formData.append('levels', JSON.stringify(
levels
.filter((l) => l.required_count !== '' || l.meso_cost !== '')
.map((l) => ({
level: l.level,
required_count: Number(l.required_count) || 0,
meso_cost: Number(l.meso_cost) || 0,
}))
))
if (imageFile) formData.append('image', imageFile)
const adminKey = localStorage.getItem('maple-admin-key')
const url = isEdit ? `/api/admin/symbol/symbols/${id}` : '/api/admin/symbol/symbols'
const res = await fetch(url, {
method: isEdit ? 'PATCH' : 'POST',
headers: { 'x-admin-key': adminKey },
body: formData,
})
const json = await res.json()
if (!res.ok) throw new Error(json.error || '저장 실패')
return json
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'symbol', 'symbols'] })
queryClient.invalidateQueries({ queryKey: ['symbol', 'symbols'] })
navigate('..')
},
onError: (err) => setError(err.message),
})
const deleteMutation = useMutation({
mutationFn: () => api(`/api/admin/symbol/symbols/${id}`, { method: 'DELETE' }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'symbol', 'symbols'] })
queryClient.invalidateQueries({ queryKey: ['symbol', 'symbols'] })
navigate('..')
},
onError: (err) => alert(err.message),
})
const handleSubmit = () => {
setError('')
if (!type) return setError('심볼 종류를 선택해주세요')
if (!region.trim()) return setError('지역 이름을 입력해주세요')
if (!maxLevel || Number(maxLevel) < 2) return setError('만렙을 입력해주세요')
if (!isEdit && !imageFile) return setError('심볼 이미지를 업로드해주세요')
saveMutation.mutate()
}
const displayImage = imagePreview || existingImageUrl
return (
{isEdit ? '심볼 편집' : '심볼 추가'}
심볼 정보와 레벨별 필요 개수/메소를 입력합니다
{/* 기본 정보 */}
기본 정보
{/* 레벨별 설정 */}
레벨별 필요 개수 · 메소
레벨 N → N+1 업그레이드 기준 (만렙-1행)
{/* 하단 버튼 */}
{isEdit && (
)}
{error && (
{error}
)}
setConfirmDelete(false)}
onConfirm={() => { setConfirmDelete(false); deleteMutation.mutate() }}
title="심볼 삭제"
description={'이 심볼을 삭제하시겠습니까?\n레벨별 데이터도 함께 삭제됩니다.'}
confirmText="삭제"
destructive
/>
)
}