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).
|
* 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>
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user