diff --git a/src/frontend/src/screens/admin/HolidayAdminPage.tsx b/src/frontend/src/screens/admin/HolidayAdminPage.tsx index 6b3aeca..9adbae6 100644 --- a/src/frontend/src/screens/admin/HolidayAdminPage.tsx +++ b/src/frontend/src/screens/admin/HolidayAdminPage.tsx @@ -7,11 +7,12 @@ import { 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 { 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 { holidayApi, type HolidayCreateBody } from './holidayApi'; +import { holidayApi, type Holiday, type HolidayCreateBody } from './holidayApi'; import './admin-modules.css'; /** 데이터 미존재 시 폴백 연도 목록(V41 시드 범위). */ @@ -72,6 +73,37 @@ export function HolidayAdminPage() { const count = useMemo(() => list.filter((h) => h.isHoliday).length, [list]); + const columns = useMemo[]>( + () => [ + { key: 'ymd', header: t('holiday.date'), value: (h) => h.ymd, + render: (h) => {h.ymd} }, + { key: 'weekday', header: t('holiday.weekday'), sortable: false, value: (h) => t(`common.weekday.${weekdayIndex(h.ymd)}`), + render: (h) => {t(`common.weekday.${weekdayIndex(h.ymd)}`)} }, + { key: 'name', header: t('holiday.name'), value: (h) => h.name, + render: (h) => {h.name} }, + { key: 'type', header: t('holiday.type'), value: (h) => (h.isSubstitute ? t('holiday.substitute') : t('holiday.legal')), + render: (h) => h.isSubstitute + ? {t('holiday.substitute')} + : {t('holiday.legal')} }, + { key: 'status', header: t('holiday.status'), value: (h) => (h.isHoliday ? t('holiday.on') : t('holiday.off')), + render: (h) => {h.isHoliday ? t('holiday.on') : t('holiday.off')} }, + { key: 'manage', header: t('holiday.manage'), csv: false, sortable: false, searchable: false, + thClassName: 'kx-adm-col-manage', tdClassName: 'kx-adm-col-manage', + render: (h) => ( +
+ + +
+ ) }, + ], + [t, toggleM, delM], + ); + return (
@@ -106,83 +138,23 @@ export function HolidayAdminPage() { )} {!listQ.isLoading && !listQ.isError && ( - list.length === 0 ? ( - - ) : ( -
-
-

{t('holiday.listOf', { y: year })}

-

{t('holiday.count', { n: count })}

-
-
- - - - - - - - - - - - - {list.map((h) => { - const dow = weekdayIndex(h.ymd); - const isSun = dow === 0; - return ( - - - - - - - - - ); - })} - -
{t('holiday.date')}{t('holiday.weekday')}{t('holiday.name')}{t('holiday.type')}{t('holiday.status')}{t('holiday.manage')}
- {h.ymd} - - {t(`common.weekday.${dow}`)} - - {h.name} - - {h.isSubstitute ? ( - {t('holiday.substitute')} - ) : ( - {t('holiday.legal')} - )} - - - {h.isHoliday ? t('holiday.on') : t('holiday.off')} - - -
- - -
-
-
-
- ) +
+
+

{t('holiday.listOf', { y: year })}

+

{t('holiday.count', { n: count })}

+
+
+ h.ymd} + csvFileName={`holidays_${year}`} + ariaLabel={t('holiday.listOf', { y: year })} + emptyTitle={t('holiday.emptyTitle')} + emptyDescription={t('holiday.emptyDesc')} + /> +
+
)} {adding && ( diff --git a/src/frontend/src/screens/admin/ProgramAdminPage.tsx b/src/frontend/src/screens/admin/ProgramAdminPage.tsx index b1a3396..f8411f9 100644 --- a/src/frontend/src/screens/admin/ProgramAdminPage.tsx +++ b/src/frontend/src/screens/admin/ProgramAdminPage.tsx @@ -2,11 +2,12 @@ * W12 시스템관리 — 프로그램(화면) 등록부. * 정본: GET/POST /api/admin/programs · DELETE /{id} (ProgramController). 라우트/화면 마스터(메뉴와 연결). */ -import { useState } from 'react'; +import { 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 { ErrorState, Skeleton } from '../../components/ui/States'; import { Button } from '../../components/ui/Button'; +import { KxDataTable, type KxColumn } from '../../components/KxDataTable'; import { IconPlus, IconSearch } from '../../components/ui/icons'; import { errMessage, useToast } from '../work/workShared'; import { programApi, PROGRAM_TYPES, type Program, type ProgramSaveBody } from './systemBasicsApi'; @@ -38,6 +39,31 @@ export function ProgramAdminPage() { onError: (e) => show(errMessage(e)), }); + const columns = useMemo[]>( + () => [ + { key: 'name', header: t('program.colName'), value: (r) => r.programName }, + { key: 'type', header: t('program.colType'), value: (r) => r.programType, + render: (r) => {r.programType} }, + { key: 'url', header: t('program.colUrl'), value: (r) => r.programUrl ?? '', + render: (r) => {r.programUrl ?? '-'} }, + { key: 'category', header: t('program.colCategory'), value: (r) => r.category ?? '', render: (r) => r.category ?? '-' }, + { key: 'use', header: t('program.colUse'), value: (r) => (r.useYn === 'N' ? t('sysadmin.noUse') : t('sysadmin.use')), + render: (r) => (r.useYn === 'N' ? t('sysadmin.noUse') : t('sysadmin.use')) }, + { key: 'manage', header: t('sysadmin.actions'), csv: false, sortable: false, searchable: false, + thClassName: 'kx-adm-col-manage', tdClassName: 'kx-adm-col-manage', + render: (r) => ( +
+ + +
+ ) }, + ], + [t, delM], + ); + return (
@@ -66,42 +92,16 @@ export function ProgramAdminPage() { {!q.isLoading && !q.isError && (
- {rows.length === 0 ? ( - - ) : ( -
- - - - - - - - - - - - {rows.map((r) => ( - - - - - - - - - ))} - -
{t('program.colName')}{t('program.colType')}{t('program.colUrl')}{t('program.colCategory')}{t('program.colUse')} -
{r.programName}{r.programType}{r.programUrl ?? '-'}{r.category ?? '-'}{r.useYn === 'N' ? t('sysadmin.noUse') : t('sysadmin.use')} - - -
-
- )} + r.id} + searchable={false} + csvFileName="programs" + ariaLabel={t('program.title')} + emptyTitle={t('program.emptyTitle')} + emptyDescription={t('program.emptyDesc')} + />
)} diff --git a/src/frontend/src/screens/admin/RulesetVersionsPage.tsx b/src/frontend/src/screens/admin/RulesetVersionsPage.tsx index a7431e5..40e6dcc 100644 --- a/src/frontend/src/screens/admin/RulesetVersionsPage.tsx +++ b/src/frontend/src/screens/admin/RulesetVersionsPage.tsx @@ -3,11 +3,12 @@ * 데이터 원천: 실 API(읽기 전용) — GET /api/admin/rulesets(요약 목록) · GET /api/admin/rulesets/{name}(규칙 전문). * 백엔드는 룰셋을 classpath 리소스(resources/rulesets/*.json)로 버전 관리하며 편집 엔드포인트는 없다(규정 개정=리소스 교체). */ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States'; +import { KxDataTable, type KxColumn } from '../../components/KxDataTable'; import { rulesetApi, type RulesetRule } from './adminModulesApi'; import './admin-modules.css'; @@ -41,6 +42,29 @@ export function RulesetVersionsPage() { const rules: RulesetRule[] = detailQ.data?.rules ?? []; + const ruleColumns = useMemo[]>( + () => [ + { key: 'name', header: t('ruleset.colRuleName'), value: (r) => `${r.label ?? r.code} ${r.code}`, + render: (r) => ( + <> + {r.label ?? r.code} + {r.code} + + ) }, + { key: 'value', header: t('ruleset.colValue'), value: (r) => formatValue(t, r), + render: (r) => {formatValue(t, r)} }, + { key: 'logic', header: t('ruleset.colLogic'), value: (r) => formatLogic(t, r), + render: (r) => {formatLogic(t, r)} }, + { key: 'severity', header: t('ruleset.colSeverity'), value: (r) => (r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')), + render: (r) => ( + + {r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')} + + ) }, + ], + [t], + ); + return (
@@ -121,36 +145,14 @@ export function RulesetVersionsPage() { )} {rules.length > 0 && ( -
- - - - - - - - - - - {rules.map((r) => ( - - - - - - - ))} - -
{t('ruleset.colRuleName')}{t('ruleset.colValue')}{t('ruleset.colLogic')}{t('ruleset.colSeverity')}
- {r.label ?? r.code} - {r.code} - {formatValue(t, r)}{formatLogic(t, r)} - - {r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')} - -
+
+ r.code} + csvFileName={`ruleset_${activeName ?? 'rules'}`} + ariaLabel={t('ruleset.ruleList')} + />
)}