import { useState, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '../../api/client' import ImagePicker from './components/ImagePicker' import ConfirmDialog from '../../components/ConfirmDialog' function Field({ label, hint, error, required, children }) { return (
{hint && {hint}}
{children} {error &&
{error}
}
) } 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' export default function AdminMenuForm() { const navigate = useNavigate() const queryClient = useQueryClient() const { id } = useParams() const isEdit = !!id const [pickerOpen, setPickerOpen] = useState(false) const [confirmDelete, setConfirmDelete] = useState(false) const [form, setForm] = useState({ title: '', description: '', slug: '', // 사용자 입력 (앞 / 제외) image_id: null, image: null, // 미리보기용 캐시 }) const [errors, setErrors] = useState({}) // 편집 모드일 때 기존 데이터 로드 const { data: menuData } = useQuery({ queryKey: ['admin', 'menus', id], queryFn: () => api(`/api/admin/menus/${id}`), enabled: isEdit, }) // id 변경 또는 데이터 로드 시 폼 동기화 useEffect(() => { if (!isEdit) { setForm({ title: '', description: '', slug: '', image_id: null, image: null }) return } if (menuData) { setForm({ title: menuData.title || '', description: menuData.description || '', slug: (menuData.url || '').replace(/^\/+/, ''), image_id: menuData.image_id, image: menuData.image, }) } }, [isEdit, id, menuData]) const update = (patch) => setForm((prev) => ({ ...prev, ...patch })) // slug에서 / 자동 제거 (붙여넣기 등 대비) const handleSlugChange = (value) => { update({ slug: value.replace(/^\/+/, '') }) } const fullUrl = `/${form.slug.trim()}` const validate = () => { const errs = {} if (!form.title.trim()) errs.title = '제목을 입력해주세요' if (!form.slug.trim()) errs.slug = '경로를 입력해주세요' else if (!/^[a-zA-Z0-9\-/]+$/.test(form.slug.trim())) errs.slug = '영문, 숫자, 하이픈(-), 슬래시(/)만 사용할 수 있습니다' setErrors(errs) return Object.keys(errs).length === 0 } const saveMutation = useMutation({ mutationFn: async () => { const payload = { title: form.title.trim(), description: form.description.trim(), url: fullUrl, image_id: form.image_id, } if (isEdit) { return api(`/api/admin/menus/${id}`, { method: 'PATCH', body: payload }) } return api('/api/admin/menus', { method: 'POST', body: payload }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'menus'] }) queryClient.invalidateQueries({ queryKey: ['menus'] }) navigate('/admin') }, onError: (err) => alert(err.message), }) const handleSubmit = (e) => { e.preventDefault() if (!validate()) return saveMutation.mutate() } const deleteMutation = useMutation({ mutationFn: () => api(`/api/admin/menus/${id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'menus'] }) queryClient.invalidateQueries({ queryKey: ['menus'] }) navigate('/admin') }, onError: (err) => alert(err.message), }) return (

{isEdit ? '메뉴 항목 편집' : '메뉴 항목 추가'}

홈 화면에 표시되는 카드의 정보를 설정합니다

{/* 미리보기 */}
미리보기

{form.title || '제목 없음'}

{form.description || '설명 없음'}

update({ title: e.target.value })} placeholder="예: 주간 보스 수익 계산기" className={inputCls} /> update({ description: e.target.value })} placeholder="예: 캐릭터별 보스 결정석 수익을 계산합니다" className={inputCls} />
/ handleSlugChange(e.target.value)} placeholder="boss-crystal" className="flex-1 min-w-0 bg-transparent px-3 py-2 text-sm outline-none" />
{form.slug.trim() && !errors.slug && (
전체 URL: https://maple.caadiq.co.kr{fullUrl}
)}
{form.image ? ( <>
{form.image.name}
) : (
이미지 선택
)}
{isEdit && ( )}
setConfirmDelete(false)} onConfirm={() => deleteMutation.mutate()} title="메뉴 삭제" description={`"${form.title}" 메뉴를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`} confirmText="삭제" destructive loading={deleteMutation.isPending} /> setPickerOpen(false)} currentImageId={form.image_id} onSelect={(img) => update({ image_id: img?.id || null, image: img })} />
) }