163 lines
7.2 KiB
TypeScript
163 lines
7.2 KiB
TypeScript
import { useState } from 'react'
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||
import { Plus, Edit, Trash2, X } from 'lucide-react'
|
||
import { getTemplates, createTemplate, updateTemplate, deleteTemplate } from '../api/client'
|
||
|
||
export default function TemplateList() {
|
||
const qc = useQueryClient()
|
||
const [modal, setModal] = useState<any>(null)
|
||
|
||
const { data: templates = [], isLoading } = useQuery({
|
||
queryKey: ['templates'],
|
||
queryFn: () => getTemplates(),
|
||
})
|
||
|
||
const saveMut = useMutation({
|
||
mutationFn: (d: any) => d.id ? updateTemplate(d.id, d) : createTemplate(d),
|
||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['templates'] }); setModal(null) },
|
||
})
|
||
|
||
const deleteMut = useMutation({
|
||
mutationFn: deleteTemplate,
|
||
onSuccess: () => qc.invalidateQueries({ queryKey: ['templates'] }),
|
||
})
|
||
|
||
if (isLoading) return <div className="text-gray-400 p-8">로딩 중...</div>
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-semibold text-white">ESL 템플릿</h1>
|
||
<button onClick={() => setModal({})}
|
||
className="flex items-center gap-1.5 bg-brand hover:bg-brand2 text-white px-3 py-1.5 rounded text-sm">
|
||
<Plus size={14} /> 템플릿 추가
|
||
</button>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{(templates as any[]).map((t: any) => (
|
||
<div key={t.id} className="bg-card border border-edge rounded-lg p-4">
|
||
<div className="flex justify-between items-start mb-2">
|
||
<div>
|
||
<p className="text-sm font-medium text-white">{t.templateName}</p>
|
||
<p className="text-xs text-brand mt-0.5">{t.tenantCode} · {t.templateType}</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button onClick={() => setModal(t)} className="text-brand hover:text-blue-300"><Edit size={14} /></button>
|
||
<button onClick={() => { if(confirm('삭제?')) deleteMut.mutate(t.id) }}
|
||
className="text-red-400 hover:text-red-300"><Trash2 size={14} /></button>
|
||
</div>
|
||
</div>
|
||
<div className="bg-panel border border-edge rounded p-3 flex items-center justify-center"
|
||
style={{ height: Math.min((t.height || 76) / 2 + 40, 120) }}>
|
||
<div className="text-center">
|
||
<p className="text-xs text-gray-400">{t.width}×{t.height}px</p>
|
||
<p className="text-xs text-gray-500 mt-1">{t.description}</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-gray-500 mt-2">코드: {t.templateCode}</p>
|
||
</div>
|
||
))}
|
||
{(templates as any[]).length === 0 && (
|
||
<div className="col-span-3 text-center text-gray-500 py-8">템플릿이 없습니다</div>
|
||
)}
|
||
</div>
|
||
|
||
{modal && (
|
||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||
<div className="bg-card border border-edge rounded-lg p-6 w-full max-w-md">
|
||
<div className="flex justify-between mb-4">
|
||
<h2 className="font-semibold">{modal.id ? '템플릿 수정' : '템플릿 추가'}</h2>
|
||
<button onClick={() => setModal(null)}><X size={16} /></button>
|
||
</div>
|
||
<TemplateForm initial={modal} onSave={(d: any) => saveMut.mutate(d)} />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function TemplateForm({ initial, onSave }: { initial: any; onSave: (d: any) => void }) {
|
||
const isEdit = !!initial.id
|
||
const [f, setF] = useState({
|
||
tenantCode: initial.tenantCode || 'EMART',
|
||
templateCode: initial.templateCode || '',
|
||
templateName: initial.templateName || '',
|
||
templateType: initial.templateType || 'PRICE',
|
||
width: initial.width ?? 250,
|
||
height: initial.height ?? 122,
|
||
description: initial.description || '',
|
||
layoutJson: initial.layoutJson || '',
|
||
active: initial.active !== false,
|
||
id: initial.id,
|
||
})
|
||
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) =>
|
||
setF(p => ({ ...p, [k]: e.target.value }))
|
||
|
||
const submit = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
onSave({
|
||
...f,
|
||
width: f.width === ('' as any) ? null : Number(f.width),
|
||
height: f.height === ('' as any) ? null : Number(f.height),
|
||
active: f.active === true || (f as any).active === 'true',
|
||
})
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={submit} className="space-y-3">
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">테넌트</label>
|
||
<select value={f.tenantCode} onChange={set('tenantCode')}
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
|
||
{['TENANT_A','TENANT_B','EMART','ZIOINFO'].map(o => <option key={o} value={o}>{o}</option>)}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">템플릿 코드</label>
|
||
<input value={f.templateCode} onChange={set('templateCode')} disabled={isEdit} required
|
||
placeholder="예: EM-PRICE-STD"
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white disabled:opacity-50" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">템플릿명</label>
|
||
<input value={f.templateName} onChange={set('templateName')} required
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">유형</label>
|
||
<select value={f.templateType} onChange={set('templateType')}
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white">
|
||
{['PRICE','INFO','PROMO','CUSTOM'].map(o => <option key={o} value={o}>{o}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{[
|
||
{ label: '너비(px)', key: 'width' },
|
||
{ label: '높이(px)', key: 'height' },
|
||
].map(({ label, key }) => (
|
||
<div key={key}>
|
||
<label className="block text-xs text-gray-400 mb-1">{label}</label>
|
||
<input type="number" value={(f as any)[key]} onChange={set(key)}
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">설명</label>
|
||
<input value={f.description} onChange={set('description')}
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-400 mb-1">레이아웃 JSON (선택)</label>
|
||
<textarea value={f.layoutJson} onChange={set('layoutJson')} rows={2}
|
||
placeholder='{"fields":[...]}'
|
||
className="w-full bg-panel border border-edge rounded px-3 py-2 text-sm text-white font-mono" />
|
||
</div>
|
||
<button type="submit" className="w-full bg-brand hover:bg-brand2 text-white py-2 rounded text-sm">
|
||
저장
|
||
</button>
|
||
</form>
|
||
)
|
||
}
|