feat(web): apply KxDataTable to common-code admin (4th screen, per-column control demo)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2265789bdf
commit
d19b74207b
@ -4,11 +4,12 @@
|
||||
* DELETE /api/admin/codes/groups/{grp} · DELETE /api/admin/codes/{grp}/{code} (ADMIN).
|
||||
* UI: 좌 그룹 목록(선택·추가), 우 코드 값 테이블(추가/수정/삭제 모달). 저장은 upsert(코드 존재 시 갱신).
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { KxDataTable, type KxColumn } from '../../components/KxDataTable';
|
||||
import { IconPlus } from '../../components/ui/icons';
|
||||
import { errMessage, useToast } from '../work/workShared';
|
||||
import { codeApi, type Code, type CodeGroup } from './adminCrudApi';
|
||||
@ -77,6 +78,44 @@ export function CommonCodeAdminPage() {
|
||||
|
||||
const codes = codesQ.data ?? [];
|
||||
|
||||
// 코드 값 테이블 컬럼 — 검색·정렬·페이지·CSV 는 KxDataTable 담당(관리 열은 정렬/검색/CSV 제외).
|
||||
const codeColumns = useMemo<KxColumn<Code>[]>(
|
||||
() => [
|
||||
{ key: 'code', header: t('code.code'), value: (c) => c.code,
|
||||
render: (c) => <span className="kx-adm-mono">{c.code}</span> },
|
||||
{ key: 'name', header: t('code.name'), value: (c) => c.codeName,
|
||||
render: (c) => <span className="kx-adm-tbl__strong">{c.codeName}</span> },
|
||||
{ key: 'value', header: t('code.value'), value: (c) => c.codeValue ?? '',
|
||||
render: (c) => <span className="kx-adm-muted">{c.codeValue ?? '—'}</span> },
|
||||
{ key: 'sort', header: t('code.sort'), numeric: true, value: (c) => c.sortOrder },
|
||||
{ key: 'use', header: t('code.use'), value: (c) => (c.useYn === 'N' ? t('code.unused') : t('code.used')),
|
||||
render: (c) => (
|
||||
<span className={`kx-adm-pill kx-adm-pill--${c.useYn === 'N' ? 'neutral' : 'success'}`}>
|
||||
{c.useYn === 'N' ? t('code.unused') : t('code.used')}
|
||||
</span>
|
||||
) },
|
||||
{ key: 'manage', header: t('code.manage'), csv: false, sortable: false, searchable: false,
|
||||
thClassName: 'kx-adm-col-manage', tdClassName: 'kx-adm-col-manage',
|
||||
render: (c) => (
|
||||
<div className="kx-adm-tbl__actions">
|
||||
<Button variant="ghost" onClick={() => setCodeModal(c)}>{t('code.edit')}</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="kx-adm-del"
|
||||
disabled={delCodeM.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(t('code.deleteCodeConfirm', { name: c.codeName })))
|
||||
delCodeM.mutate({ grpCode: c.grpCode, code: c.code });
|
||||
}}
|
||||
>
|
||||
{t('code.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
) },
|
||||
],
|
||||
[t, delCodeM.isPending, delCodeM],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="kx-page kx-adm">
|
||||
<header className="kx-adm__head">
|
||||
@ -153,57 +192,17 @@ export function CommonCodeAdminPage() {
|
||||
</div>
|
||||
)}
|
||||
{!codesQ.isLoading && !codesQ.isError && (
|
||||
codes.length === 0 ? (
|
||||
<div style={{ padding: 16 }}>
|
||||
<EmptyState title={t('code.noCodesTitle')} description={t('code.noCodesDesc')} />
|
||||
<div className="kx-adm-codes__tbl">
|
||||
<KxDataTable
|
||||
columns={codeColumns}
|
||||
rows={codes}
|
||||
rowKey={(c) => c.code}
|
||||
csvFileName={`codes_${selected ?? 'group'}`}
|
||||
ariaLabel={t('code.codeValues')}
|
||||
emptyTitle={t('code.noCodesTitle')}
|
||||
emptyDescription={t('code.noCodesDesc')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="kx-table-scroll">
|
||||
<table className="kx-table kx-table--zebra kx-adm-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('code.code')}</th>
|
||||
<th>{t('code.name')}</th>
|
||||
<th>{t('code.value')}</th>
|
||||
<th className="kx-num">{t('code.sort')}</th>
|
||||
<th>{t('code.use')}</th>
|
||||
<th style={{ textAlign: 'right' }}>{t('code.manage')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{codes.map((c) => (
|
||||
<tr key={c.code}>
|
||||
<td className="kx-adm-mono">{c.code}</td>
|
||||
<td className="kx-adm-tbl__strong">{c.codeName}</td>
|
||||
<td className="kx-adm-muted">{c.codeValue ?? '—'}</td>
|
||||
<td className="kx-num tnum">{c.sortOrder}</td>
|
||||
<td>
|
||||
<span className={`kx-adm-pill kx-adm-pill--${c.useYn === 'N' ? 'neutral' : 'success'}`}>
|
||||
{c.useYn === 'N' ? t('code.unused') : t('code.used')}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="kx-adm-tbl__actions">
|
||||
<Button variant="ghost" onClick={() => setCodeModal(c)}>{t('code.edit')}</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="kx-adm-del"
|
||||
disabled={delCodeM.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(t('code.deleteCodeConfirm', { name: c.codeName })))
|
||||
delCodeM.mutate({ grpCode: c.grpCode, code: c.code });
|
||||
}}
|
||||
>
|
||||
{t('code.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -551,6 +551,9 @@
|
||||
border-bottom: var(--border-card);
|
||||
}
|
||||
.kx-adm-codes__head h2 { font-size: var(--fs-h3); font-weight: var(--fw-bold); color: var(--color-neutral-900); }
|
||||
/* 코드 값 KxDataTable 래퍼 — 카드 padding:0 을 상쇄(툴바·표·페이저 여백) */
|
||||
.kx-adm-codes__tbl { padding: var(--space-3) var(--space-4) var(--space-4); }
|
||||
.kx-adm-col-manage { text-align: right; }
|
||||
.kx-adm-glist { list-style: none; margin: 0; padding: var(--space-2); display: flex; flex-direction: column; gap: 4px; }
|
||||
.kx-adm-gitem {
|
||||
width: 100%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user