zioinfo-esn/frontend/src/pages/uiws/system/CodeManagement.tsx

159 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import { codeList, codeGroupGet, codeCreate, codeUpdate, codeDelete } from '../../../api/uiws'
import { PageHeader, SearchBar, Panel, Button, Modal, FormField, Input, Spinner, Pagination, YnBadge, DataGrid, type Column } from '../../../components/uiws/ui'
import AdminGate from './AdminGate'
/**
* 2.7 코드(공통코드) 관리 — /api/system/codes. 그룹 목록(PageResponse) + 상세(그룹+코드값 목록) 편집.
* 그룹 단위 저장(values 포함). RBAC 변경 ADMIN/MANAGER(403 + AdminGate). 색상 토큰만.
*/
type CodeGrp = { grpCd: string; grpNm: string; useYn: string }
type CodeValue = { grpCd: string; codeVal: string; codeNm: string; sortOrd: number | null; useYn: string }
type CodeGrpDetail = CodeGrp & { values: CodeValue[] }
const PSIZE = 20
function friendlyError(e: any): string {
if (e?.response?.status === 403) return '권한이 없습니다 (관리자 전용).'
if (e?.response?.status === 409) return '이미 존재하는 코드 그룹입니다.'
return e?.response?.data?.message || '요청을 처리하지 못했습니다.'
}
export default function CodeManagement() {
const [rows, setRows] = useState<CodeGrp[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(0)
const [kw, setKw] = useState('')
const [loading, setLoading] = useState(false)
const [open, setOpen] = useState(false)
const [editing, setEditing] = useState(false)
const [form, setForm] = useState<CodeGrpDetail>({ grpCd: '', grpNm: '', useYn: 'Y', values: [] })
const [err, setErr] = useState('')
async function load(p = page, keyword = kw) {
setLoading(true)
try {
const res = await codeList({ keyword: keyword || undefined, page: p, size: PSIZE })
setRows(res.data?.data?.content || [])
setTotal(res.data?.data?.total || 0)
} catch { setRows([]); setTotal(0) } finally { setLoading(false) }
}
useEffect(() => { load(0, '') }, [])
function openCreate() {
setForm({ grpCd: '', grpNm: '', useYn: 'Y', values: [] }); setEditing(false); setErr(''); setOpen(true)
}
async function openEdit(g: CodeGrp) {
setErr('')
try {
const res = await codeGroupGet(g.grpCd)
const d = res.data?.data
setForm({ grpCd: d.grpCd, grpNm: d.grpNm, useYn: d.useYn, values: d.values || [] })
setEditing(true); setOpen(true)
} catch (e) { alert(friendlyError(e)) }
}
function addValue() {
setForm(f => ({ ...f, values: [...f.values, { grpCd: f.grpCd, codeVal: '', codeNm: '', sortOrd: f.values.length + 1, useYn: 'Y' }] }))
}
function setVal(i: number, patch: Partial<CodeValue>) {
setForm(f => ({ ...f, values: f.values.map((v, idx) => idx === i ? { ...v, ...patch } : v) }))
}
function removeValue(i: number) {
setForm(f => ({ ...f, values: f.values.filter((_, idx) => idx !== i) }))
}
async function save() {
setErr('')
if (!form.grpCd.trim() || !form.grpNm.trim()) { setErr('그룹 코드와 이름은 필수입니다.'); return }
const body = { ...form, values: form.values.map(v => ({ ...v, grpCd: form.grpCd })) }
try {
if (editing) await codeUpdate(form.grpCd, body)
else await codeCreate(body)
setOpen(false); load()
} catch (e) { setErr(friendlyError(e)) }
}
async function remove(grpCd: string) {
if (!confirm(`코드 그룹 '${grpCd}' 을(를) 삭제할까요?`)) return
try { await codeDelete(grpCd); load() } catch (e) { alert(friendlyError(e)) }
}
const totalPages = Math.max(1, Math.ceil(total / PSIZE))
const columns: Column<CodeGrp & Record<string, unknown>>[] = [
{ key: 'grpCd', header: '그룹 코드', render: r => <span className="font-mono">{r.grpCd}</span> },
{ key: 'grpNm', header: '그룹명' },
{ key: 'useYn', header: '사용', width: 80, render: r => <YnBadge yn={r.useYn} yes="사용" no="미사용" /> },
{ key: 'act', header: '', width: 64, align: 'right', render: r => (
<button onClick={e => { e.stopPropagation(); remove(r.grpCd) }} className="text-xs" style={{ color: 'var(--uiws-danger)' }}></button>
) },
]
return (
<AdminGate>
<div className="uiws-scope">
<PageHeader title="공통코드 관리" subtitle="코드 그룹 및 코드값 정의"
actions={<Button onClick={openCreate}> </Button>} />
<SearchBar>
<Input value={kw} onChange={setKw} placeholder="그룹코드/그룹명 검색" style={{ flex: 1, minWidth: 220 }} />
<Button onClick={() => { setPage(0); load(0, kw) }}></Button>
</SearchBar>
{loading ? <Spinner /> : (
<>
<DataGrid columns={columns} rows={rows as (CodeGrp & Record<string, unknown>)[]} rowKey={r => r.grpCd}
onRowClick={r => openEdit(r)} empty="등록된 코드 그룹이 없습니다." />
<Pagination page={page} totalPages={totalPages} onChange={p => { setPage(p); load(p) }} />
</>
)}
{open && (
<Modal title={editing ? '코드 그룹 수정' : '코드 그룹 추가'} onClose={() => setOpen(false)}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<FormField label="그룹 코드"><Input value={form.grpCd} onChange={v => setForm({ ...form, grpCd: v })} placeholder="예: ALARM_TYPE" style={{ width: '100%' }} /></FormField>
<FormField label="그룹명"><Input value={form.grpNm} onChange={v => setForm({ ...form, grpNm: v })} style={{ width: '100%' }} /></FormField>
</div>
<FormField label="사용 여부"><Input value={form.useYn} onChange={v => setForm({ ...form, useYn: v })} placeholder="Y / N" /></FormField>
<div style={{ marginTop: 8, marginBottom: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: 'var(--uiws-text-muted)' }}></span>
<Button variant="ghost" onClick={addValue}>+ </Button>
</div>
<div style={{ border: '1px solid var(--uiws-border)', borderRadius: 8, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
<thead>
<tr style={{ background: 'var(--uiws-surface-2)', color: 'var(--uiws-text-muted)' }}>
<th style={{ padding: '7px 8px', textAlign: 'left' }}></th>
<th style={{ padding: '7px 8px', textAlign: 'left' }}></th>
<th style={{ padding: '7px 8px', textAlign: 'left', width: 60 }}></th>
<th style={{ padding: '7px 8px', textAlign: 'left', width: 50 }}></th>
<th style={{ width: 36 }} />
</tr>
</thead>
<tbody>
{form.values.length === 0 ? (
<tr><td colSpan={5} style={{ padding: 14, textAlign: 'center', color: 'var(--uiws-text-faint)' }}> .</td></tr>
) : form.values.map((v, i) => (
<tr key={i} style={{ borderTop: '1px solid var(--uiws-border)' }}>
<td style={{ padding: 4 }}><Input value={v.codeVal} onChange={x => setVal(i, { codeVal: x })} style={{ width: '100%', padding: '6px 8px' }} /></td>
<td style={{ padding: 4 }}><Input value={v.codeNm} onChange={x => setVal(i, { codeNm: x })} style={{ width: '100%', padding: '6px 8px' }} /></td>
<td style={{ padding: 4 }}><Input type="number" value={String(v.sortOrd ?? '')} onChange={x => setVal(i, { sortOrd: x === '' ? null : Number(x) })} style={{ width: '100%', padding: '6px 8px' }} /></td>
<td style={{ padding: 4 }}><Input value={v.useYn} onChange={x => setVal(i, { useYn: x })} style={{ width: '100%', padding: '6px 8px' }} /></td>
<td style={{ textAlign: 'center' }}><button onClick={() => removeValue(i)} style={{ background: 'none', border: 'none', color: 'var(--uiws-danger)', cursor: 'pointer' }}>×</button></td>
</tr>
))}
</tbody>
</table>
</div>
{err && <p className="text-xs" style={{ color: 'var(--uiws-danger)', marginTop: 8 }}>{err}</p>}
<div className="flex justify-end gap-2 pt-3">
<Button variant="ghost" onClick={() => setOpen(false)}></Button>
<Button onClick={save}></Button>
</div>
</Modal>
)}
</div>
</AdminGate>
)
}