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:
zio 2026-07-13 05:38:57 +09:00
parent 2265789bdf
commit d19b74207b
2 changed files with 54 additions and 52 deletions

View File

@ -4,11 +4,12 @@
* DELETE /api/admin/codes/groups/{grp} · DELETE /api/admin/codes/{grp}/{code} (ADMIN). * DELETE /api/admin/codes/groups/{grp} · DELETE /api/admin/codes/{grp}/{code} (ADMIN).
* UI: (·), (// ). upsert( ). * UI: (·), (// ). upsert( ).
*/ */
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States'; import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { Button } from '../../components/ui/Button'; import { Button } from '../../components/ui/Button';
import { KxDataTable, type KxColumn } from '../../components/KxDataTable';
import { IconPlus } from '../../components/ui/icons'; import { IconPlus } from '../../components/ui/icons';
import { errMessage, useToast } from '../work/workShared'; import { errMessage, useToast } from '../work/workShared';
import { codeApi, type Code, type CodeGroup } from './adminCrudApi'; import { codeApi, type Code, type CodeGroup } from './adminCrudApi';
@ -77,6 +78,44 @@ export function CommonCodeAdminPage() {
const codes = codesQ.data ?? []; 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 ( return (
<div className="kx-page kx-adm"> <div className="kx-page kx-adm">
<header className="kx-adm__head"> <header className="kx-adm__head">
@ -153,57 +192,17 @@ export function CommonCodeAdminPage() {
</div> </div>
)} )}
{!codesQ.isLoading && !codesQ.isError && ( {!codesQ.isLoading && !codesQ.isError && (
codes.length === 0 ? ( <div className="kx-adm-codes__tbl">
<div style={{ padding: 16 }}> <KxDataTable
<EmptyState title={t('code.noCodesTitle')} description={t('code.noCodesDesc')} /> columns={codeColumns}
</div> rows={codes}
) : ( rowKey={(c) => c.code}
<div className="kx-table-scroll"> csvFileName={`codes_${selected ?? 'group'}`}
<table className="kx-table kx-table--zebra kx-adm-tbl"> ariaLabel={t('code.codeValues')}
<thead> emptyTitle={t('code.noCodesTitle')}
<tr> emptyDescription={t('code.noCodesDesc')}
<th>{t('code.code')}</th> />
<th>{t('code.name')}</th> </div>
<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> </section>
</div> </div>

View File

@ -551,6 +551,9 @@
border-bottom: var(--border-card); 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); } .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-glist { list-style: none; margin: 0; padding: var(--space-2); display: flex; flex-direction: column; gap: 4px; }
.kx-adm-gitem { .kx-adm-gitem {
width: 100%; width: 100%;