// 제네릭 CRUD 페이지 — 목록/검색/추가/수정/삭제 공통. 각 도메인 페이지에서 columns + form 스펙만 정의. import { useState, type ReactNode } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { PageHeader, Toolbar, Table, Modal, Button, Field, TextInput, Select, type Column } from './ui' export interface FormFieldSpec { key: string label: string type?: 'text' | 'number' | 'select' | 'textarea' | 'checkbox' | 'date' | 'time' | 'datetime-local' opts?: string[] | { value: string; label: string }[] placeholder?: string required?: boolean } interface CrudProps> { title: string subtitle?: string addLabel?: string queryKey: any[] fetcher: () => Promise onCreate?: (d: any) => Promise onUpdate?: (id: number, d: any) => Promise onDelete?: (id: number) => Promise columns: Column[] form?: FormFieldSpec[] defaults?: Record toolbarExtra?: ReactNode rowActions?: (row: T) => ReactNode readOnly?: boolean } export function CrudPage>(p: CrudProps) { const qc = useQueryClient() const [modal, setModal] = useState(null) const { data: rows = [], isLoading } = useQuery({ queryKey: p.queryKey, queryFn: p.fetcher }) const saveMut = useMutation({ mutationFn: (d: any) => (d.id && p.onUpdate ? p.onUpdate(d.id, d) : p.onCreate!(d)), onSuccess: () => { qc.invalidateQueries({ queryKey: [p.queryKey[0]] }); setModal(null) }, }) const delMut = useMutation({ mutationFn: (id: number) => p.onDelete!(id), onSuccess: () => qc.invalidateQueries({ queryKey: [p.queryKey[0]] }), }) const columns: Column[] = [...p.columns] if (!p.readOnly && (p.form || p.rowActions)) { columns.push({ key: '__act', header: '', align: 'right', render: (row: any) => (
{p.rowActions?.(row)} {p.form && p.onUpdate && ( )} {p.onDelete && ( )}
), }) } return (
setModal({ ...(p.defaults || {}) })}>{p.addLabel || '추가'} )} /> {p.toolbarExtra && {p.toolbarExtra}} {isLoading ?
로딩 중...
: r.id} />} {modal && p.form && ( setModal(null)}> saveMut.mutate(d)} saving={saveMut.isPending} /> )} ) } export function CrudForm({ spec, initial, onSave, saving }: { spec: FormFieldSpec[]; initial: any; onSave: (d: any) => void; saving?: boolean }) { const init: any = { id: initial.id } spec.forEach(f => { init[f.key] = initial[f.key] ?? (f.type === 'checkbox' ? true : f.type === 'number' ? '' : '') }) const [f, setF] = useState(init) const set = (k: string, v: any) => setF((p: any) => ({ ...p, [k]: v })) return (
{ e.preventDefault(); onSave(f) }}> {spec.map(fs => ( {fs.type === 'select' ? ( ) : fs.type === 'textarea' ? (