Compare commits
No commits in common. "fe6757f00aaf39c8bf859c4202f94c93aeeedcec" and "a6098ef00203101caa40a1ac346ce6c2bb0b1de1" have entirely different histories.
fe6757f00a
...
a6098ef002
@ -81,12 +81,12 @@
|
|||||||
### 데이터 이슈 (코드 정상 · 담당=kintex-testdata-dev)
|
### 데이터 이슈 (코드 정상 · 담당=kintex-testdata-dev)
|
||||||
- sample-import 12개 화면(analytics·dashboard·ops·floorplan·gallery·marketing×2·schedule·visitor×3)은 **조건부 degraded 폴백**(NETWORK/NOT_FOUND/NOT_IMPLEMENTED 시에만 sample). 실 API가 primary. 빈 화면으로 보이면 코드가 아니라 **행사스코프 시드 미정렬** → testdata 담당.
|
- sample-import 12개 화면(analytics·dashboard·ops·floorplan·gallery·marketing×2·schedule·visitor×3)은 **조건부 degraded 폴백**(NETWORK/NOT_FOUND/NOT_IMPLEMENTED 시에만 sample). 실 API가 primary. 빈 화면으로 보이면 코드가 아니라 **행사스코프 시드 미정렬** → testdata 담당.
|
||||||
|
|
||||||
### 문서 위생 — STALE 주석 (기능 영향 없음 · 담당=frontend-dev 청소) — ✅해소(2026-07-13)
|
### 문서 위생 — STALE 주석 (기능 영향 없음 · 담당=frontend-dev 청소)
|
||||||
- ~~`screens/auction/sampleAuction.ts:3` "M15 역경매 엔진 미구현"~~ → 실구현 반영 주석 갱신
|
- `screens/auction/sampleAuction.ts:3` "M15 역경매 엔진 미구현" → 실구현됨
|
||||||
- ~~`screens/visitor/sampleVisitor.ts:3` "M10/M11 백엔드 미구현"~~ → 실구현 반영 주석 갱신
|
- `screens/visitor/sampleVisitor.ts:3` "M10/M11 백엔드 미구현" → 실구현됨
|
||||||
- ~~`screens/marketing/sampleMarketing.ts:3` "M12 백엔드 미구현"~~ → 실구현 반영 주석 갱신(G-04 실발송 후속 명시)
|
- `screens/marketing/sampleMarketing.ts:3` "M12 백엔드 미구현" → 실구현됨
|
||||||
- ~~`screens/cms/cms.css:4` "M17 백엔드 미구현 → 전 화면 로컬 샘플"~~ → cmsApi 실배선 반영 주석 갱신
|
- `screens/cms/cms.css:4` "M17 백엔드 미구현 → 전 화면 로컬 샘플" → cmsApi 실배선됨
|
||||||
→ 4건 모두 "실구현 완료 + 샘플은 강등 폴백" 문구로 정정. 재감사 오탐 방지.
|
→ 주석만 갱신(오해 유발). 재감사 오탐 방지 위해 정리 권장.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -101,7 +101,7 @@
|
|||||||
| 5 | G-06/G-07 | 회의 STT·통합검색 자연어 AI | kintex-ai-dev |
|
| 5 | G-06/G-07 | 회의 STT·통합검색 자연어 AI | kintex-ai-dev |
|
||||||
| 6 | G-05/G-09 | 서류 HWP/PDF 생성·렌더 이력 영속(파일/렌더 큐) | kintex-backend-dev |
|
| 6 | G-05/G-09 | 서류 HWP/PDF 생성·렌더 이력 영속(파일/렌더 큐) | kintex-backend-dev |
|
||||||
| 7 | G-08 | 마이페이지 프로필 PUT·알림설정 API | kintex-backend-dev + frontend-dev |
|
| 7 | G-08 | 마이페이지 프로필 PUT·알림설정 API | kintex-backend-dev + frontend-dev |
|
||||||
| 8 | ~~(위생)~~ ✅ | STALE "미구현" 주석 4건 정리 — 2026-07-13 해소 | kintex-frontend-dev |
|
| 8 | (위생) | STALE "미구현" 주석 4건 정리 | kintex-frontend-dev |
|
||||||
| — | (데이터) | 행사스코프 시드 정렬(빈 화면 폴백 해소) | kintex-testdata-dev |
|
| — | (데이터) | 행사스코프 시드 정렬(빈 화면 폴백 해소) | kintex-testdata-dev |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -7,12 +7,11 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { 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 { 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 { holidayApi, type Holiday, type HolidayCreateBody } from './holidayApi';
|
import { holidayApi, type HolidayCreateBody } from './holidayApi';
|
||||||
import './admin-modules.css';
|
import './admin-modules.css';
|
||||||
|
|
||||||
/** 데이터 미존재 시 폴백 연도 목록(V41 시드 범위). */
|
/** 데이터 미존재 시 폴백 연도 목록(V41 시드 범위). */
|
||||||
@ -73,37 +72,6 @@ export function HolidayAdminPage() {
|
|||||||
|
|
||||||
const count = useMemo(() => list.filter((h) => h.isHoliday).length, [list]);
|
const count = useMemo(() => list.filter((h) => h.isHoliday).length, [list]);
|
||||||
|
|
||||||
const columns = useMemo<KxColumn<Holiday>[]>(
|
|
||||||
() => [
|
|
||||||
{ key: 'ymd', header: t('holiday.date'), value: (h) => h.ymd,
|
|
||||||
render: (h) => <span className="kx-adm-mono" style={weekdayIndex(h.ymd) === 0 ? { color: 'var(--color-error)' } : undefined}>{h.ymd}</span> },
|
|
||||||
{ key: 'weekday', header: t('holiday.weekday'), sortable: false, value: (h) => t(`common.weekday.${weekdayIndex(h.ymd)}`),
|
|
||||||
render: (h) => <span style={weekdayIndex(h.ymd) === 0 ? { color: 'var(--color-error)' } : undefined}>{t(`common.weekday.${weekdayIndex(h.ymd)}`)}</span> },
|
|
||||||
{ key: 'name', header: t('holiday.name'), value: (h) => h.name,
|
|
||||||
render: (h) => <span className="kx-adm-tbl__strong" style={{ color: 'var(--color-error)' }}>{h.name}</span> },
|
|
||||||
{ key: 'type', header: t('holiday.type'), value: (h) => (h.isSubstitute ? t('holiday.substitute') : t('holiday.legal')),
|
|
||||||
render: (h) => h.isSubstitute
|
|
||||||
? <span className="kx-adm-pill kx-adm-pill--neutral">{t('holiday.substitute')}</span>
|
|
||||||
: <span className="kx-adm-muted">{t('holiday.legal')}</span> },
|
|
||||||
{ key: 'status', header: t('holiday.status'), value: (h) => (h.isHoliday ? t('holiday.on') : t('holiday.off')),
|
|
||||||
render: (h) => <span className={`kx-adm-pill kx-adm-pill--${h.isHoliday ? 'success' : 'neutral'}`}>{h.isHoliday ? t('holiday.on') : t('holiday.off')}</span> },
|
|
||||||
{ key: 'manage', header: t('holiday.manage'), csv: false, sortable: false, searchable: false,
|
|
||||||
thClassName: 'kx-adm-col-manage', tdClassName: 'kx-adm-col-manage',
|
|
||||||
render: (h) => (
|
|
||||||
<div className="kx-adm-tbl__actions">
|
|
||||||
<Button variant="ghost" disabled={toggleM.isPending} onClick={() => toggleM.mutate({ ymd: h.ymd, isHoliday: !h.isHoliday })}>
|
|
||||||
{h.isHoliday ? t('holiday.setOff') : t('holiday.setOn')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" className="kx-adm-del" disabled={delM.isPending}
|
|
||||||
onClick={() => { if (window.confirm(t('holiday.deleteConfirm', { name: h.name }))) delM.mutate(h.ymd); }}>
|
|
||||||
{t('holiday.delete')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) },
|
|
||||||
],
|
|
||||||
[t, toggleM, delM],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="kx-page kx-adm">
|
<div className="kx-page kx-adm">
|
||||||
<header className="kx-adm__head">
|
<header className="kx-adm__head">
|
||||||
@ -138,23 +106,83 @@ export function HolidayAdminPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!listQ.isLoading && !listQ.isError && (
|
{!listQ.isLoading && !listQ.isError && (
|
||||||
<section className="kx-card">
|
list.length === 0 ? (
|
||||||
<div className="kx-adm-codes__head">
|
<EmptyState title={t('holiday.emptyTitle')} description={t('holiday.emptyDesc')} />
|
||||||
<h2>{t('holiday.listOf', { y: year })}</h2>
|
) : (
|
||||||
<p className="kx-adm-muted">{t('holiday.count', { n: count })}</p>
|
<section className="kx-card">
|
||||||
</div>
|
<div className="kx-adm-codes__head">
|
||||||
<div className="kx-adm-codes__tbl">
|
<h2>{t('holiday.listOf', { y: year })}</h2>
|
||||||
<KxDataTable
|
<p className="kx-adm-muted">{t('holiday.count', { n: count })}</p>
|
||||||
columns={columns}
|
</div>
|
||||||
rows={list}
|
<div className="kx-table-scroll">
|
||||||
rowKey={(h) => h.ymd}
|
<table className="kx-table kx-table--zebra kx-adm-tbl">
|
||||||
csvFileName={`holidays_${year}`}
|
<thead>
|
||||||
ariaLabel={t('holiday.listOf', { y: year })}
|
<tr>
|
||||||
emptyTitle={t('holiday.emptyTitle')}
|
<th>{t('holiday.date')}</th>
|
||||||
emptyDescription={t('holiday.emptyDesc')}
|
<th>{t('holiday.weekday')}</th>
|
||||||
/>
|
<th>{t('holiday.name')}</th>
|
||||||
</div>
|
<th>{t('holiday.type')}</th>
|
||||||
</section>
|
<th>{t('holiday.status')}</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>{t('holiday.manage')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{list.map((h) => {
|
||||||
|
const dow = weekdayIndex(h.ymd);
|
||||||
|
const isSun = dow === 0;
|
||||||
|
return (
|
||||||
|
<tr key={h.ymd}>
|
||||||
|
<td className="kx-adm-mono" style={isSun ? { color: 'var(--color-error)' } : undefined}>
|
||||||
|
{h.ymd}
|
||||||
|
</td>
|
||||||
|
<td style={isSun ? { color: 'var(--color-error)' } : undefined}>
|
||||||
|
{t(`common.weekday.${dow}`)}
|
||||||
|
</td>
|
||||||
|
<td className="kx-adm-tbl__strong" style={{ color: 'var(--color-error)' }}>
|
||||||
|
{h.name}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{h.isSubstitute ? (
|
||||||
|
<span className="kx-adm-pill kx-adm-pill--neutral">{t('holiday.substitute')}</span>
|
||||||
|
) : (
|
||||||
|
<span className="kx-adm-muted">{t('holiday.legal')}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`kx-adm-pill kx-adm-pill--${h.isHoliday ? 'success' : 'neutral'}`}>
|
||||||
|
{h.isHoliday ? t('holiday.on') : t('holiday.off')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="kx-adm-tbl__actions">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
disabled={toggleM.isPending}
|
||||||
|
onClick={() => toggleM.mutate({ ymd: h.ymd, isHoliday: !h.isHoliday })}
|
||||||
|
>
|
||||||
|
{h.isHoliday ? t('holiday.setOff') : t('holiday.setOn')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="kx-adm-del"
|
||||||
|
disabled={delM.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(t('holiday.deleteConfirm', { name: h.name })))
|
||||||
|
delM.mutate(h.ymd);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('holiday.delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{adding && (
|
{adding && (
|
||||||
|
|||||||
@ -2,12 +2,11 @@
|
|||||||
* W12 시스템관리 — 프로그램(화면) 등록부.
|
* W12 시스템관리 — 프로그램(화면) 등록부.
|
||||||
* 정본: GET/POST /api/admin/programs · DELETE /{id} (ProgramController). 라우트/화면 마스터(메뉴와 연결).
|
* 정본: GET/POST /api/admin/programs · DELETE /{id} (ProgramController). 라우트/화면 마스터(메뉴와 연결).
|
||||||
*/
|
*/
|
||||||
import { useMemo, useState } from 'react';
|
import { 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 { 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, IconSearch } from '../../components/ui/icons';
|
import { IconPlus, IconSearch } from '../../components/ui/icons';
|
||||||
import { errMessage, useToast } from '../work/workShared';
|
import { errMessage, useToast } from '../work/workShared';
|
||||||
import { programApi, PROGRAM_TYPES, type Program, type ProgramSaveBody } from './systemBasicsApi';
|
import { programApi, PROGRAM_TYPES, type Program, type ProgramSaveBody } from './systemBasicsApi';
|
||||||
@ -39,31 +38,6 @@ export function ProgramAdminPage() {
|
|||||||
onError: (e) => show(errMessage(e)),
|
onError: (e) => show(errMessage(e)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = useMemo<KxColumn<Program>[]>(
|
|
||||||
() => [
|
|
||||||
{ key: 'name', header: t('program.colName'), value: (r) => r.programName },
|
|
||||||
{ key: 'type', header: t('program.colType'), value: (r) => r.programType,
|
|
||||||
render: (r) => <span className="kx-adm-pill kx-adm-pill--neutral">{r.programType}</span> },
|
|
||||||
{ key: 'url', header: t('program.colUrl'), value: (r) => r.programUrl ?? '',
|
|
||||||
render: (r) => <span className="kx-adm-mono">{r.programUrl ?? '-'}</span> },
|
|
||||||
{ 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) => (
|
|
||||||
<div className="kx-adm-tbl__actions">
|
|
||||||
<Button variant="ghost" onClick={() => setEditing(r)}>{t('sysadmin.edit')}</Button>
|
|
||||||
<Button variant="ghost" className="kx-adm-del" disabled={delM.isPending}
|
|
||||||
onClick={() => { if (window.confirm(t('program.deleteConfirm', { name: r.programName }))) delM.mutate(r.id); }}>
|
|
||||||
{t('sysadmin.delete')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) },
|
|
||||||
],
|
|
||||||
[t, delM],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="kx-page kx-adm">
|
<div className="kx-page kx-adm">
|
||||||
<header className="kx-adm__head">
|
<header className="kx-adm__head">
|
||||||
@ -92,16 +66,42 @@ export function ProgramAdminPage() {
|
|||||||
|
|
||||||
{!q.isLoading && !q.isError && (
|
{!q.isLoading && !q.isError && (
|
||||||
<section className="kx-card kx-adm-table-card">
|
<section className="kx-card kx-adm-table-card">
|
||||||
<KxDataTable
|
{rows.length === 0 ? (
|
||||||
columns={columns}
|
<EmptyState title={t('program.emptyTitle')} description={t('program.emptyDesc')} />
|
||||||
rows={rows}
|
) : (
|
||||||
rowKey={(r) => r.id}
|
<div className="kx-table-scroll">
|
||||||
searchable={false}
|
<table className="kx-table kx-table--zebra">
|
||||||
csvFileName="programs"
|
<thead>
|
||||||
ariaLabel={t('program.title')}
|
<tr>
|
||||||
emptyTitle={t('program.emptyTitle')}
|
<th>{t('program.colName')}</th>
|
||||||
emptyDescription={t('program.emptyDesc')}
|
<th>{t('program.colType')}</th>
|
||||||
/>
|
<th>{t('program.colUrl')}</th>
|
||||||
|
<th>{t('program.colCategory')}</th>
|
||||||
|
<th>{t('program.colUse')}</th>
|
||||||
|
<th aria-label={t('sysadmin.actions')} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((r) => (
|
||||||
|
<tr key={r.id}>
|
||||||
|
<td>{r.programName}</td>
|
||||||
|
<td><span className="kx-adm-pill kx-adm-pill--neutral">{r.programType}</span></td>
|
||||||
|
<td className="kx-adm-mono">{r.programUrl ?? '-'}</td>
|
||||||
|
<td>{r.category ?? '-'}</td>
|
||||||
|
<td>{r.useYn === 'N' ? t('sysadmin.noUse') : t('sysadmin.use')}</td>
|
||||||
|
<td className="kx-adm-tbl__actions">
|
||||||
|
<Button variant="ghost" onClick={() => setEditing(r)}>{t('sysadmin.edit')}</Button>
|
||||||
|
<Button variant="ghost" className="kx-adm-del" disabled={delM.isPending}
|
||||||
|
onClick={() => { if (window.confirm(t('program.deleteConfirm', { name: r.programName }))) delM.mutate(r.id); }}>
|
||||||
|
{t('sysadmin.delete')}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,11 @@
|
|||||||
* 데이터 원천: 실 API(읽기 전용) — GET /api/admin/rulesets(요약 목록) · GET /api/admin/rulesets/{name}(규칙 전문).
|
* 데이터 원천: 실 API(읽기 전용) — GET /api/admin/rulesets(요약 목록) · GET /api/admin/rulesets/{name}(규칙 전문).
|
||||||
* 백엔드는 룰셋을 classpath 리소스(resources/rulesets/*.json)로 버전 관리하며 편집 엔드포인트는 없다(규정 개정=리소스 교체).
|
* 백엔드는 룰셋을 classpath 리소스(resources/rulesets/*.json)로 버전 관리하며 편집 엔드포인트는 없다(규정 개정=리소스 교체).
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
|
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
|
||||||
import { KxDataTable, type KxColumn } from '../../components/KxDataTable';
|
|
||||||
import { rulesetApi, type RulesetRule } from './adminModulesApi';
|
import { rulesetApi, type RulesetRule } from './adminModulesApi';
|
||||||
import './admin-modules.css';
|
import './admin-modules.css';
|
||||||
|
|
||||||
@ -42,29 +41,6 @@ export function RulesetVersionsPage() {
|
|||||||
|
|
||||||
const rules: RulesetRule[] = detailQ.data?.rules ?? [];
|
const rules: RulesetRule[] = detailQ.data?.rules ?? [];
|
||||||
|
|
||||||
const ruleColumns = useMemo<KxColumn<RulesetRule>[]>(
|
|
||||||
() => [
|
|
||||||
{ key: 'name', header: t('ruleset.colRuleName'), value: (r) => `${r.label ?? r.code} ${r.code}`,
|
|
||||||
render: (r) => (
|
|
||||||
<>
|
|
||||||
<span className="kx-adm-rulename">{r.label ?? r.code}</span>
|
|
||||||
<span className="kx-adm-rulecode">{r.code}</span>
|
|
||||||
</>
|
|
||||||
) },
|
|
||||||
{ key: 'value', header: t('ruleset.colValue'), value: (r) => formatValue(t, r),
|
|
||||||
render: (r) => <span className="kx-adm-mono">{formatValue(t, r)}</span> },
|
|
||||||
{ key: 'logic', header: t('ruleset.colLogic'), value: (r) => formatLogic(t, r),
|
|
||||||
render: (r) => <span className="kx-adm-muted">{formatLogic(t, r)}</span> },
|
|
||||||
{ key: 'severity', header: t('ruleset.colSeverity'), value: (r) => (r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')),
|
|
||||||
render: (r) => (
|
|
||||||
<span className={`kx-adm-pill kx-adm-pill--${r.severity === 'block' ? 'error' : 'warn'}`}>
|
|
||||||
{r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')}
|
|
||||||
</span>
|
|
||||||
) },
|
|
||||||
],
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="kx-page kx-adm">
|
<div className="kx-page kx-adm">
|
||||||
<header className="kx-adm__head">
|
<header className="kx-adm__head">
|
||||||
@ -145,14 +121,36 @@ export function RulesetVersionsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{rules.length > 0 && (
|
{rules.length > 0 && (
|
||||||
<div className="kx-adm-rules-dt">
|
<div className="kx-table-scroll">
|
||||||
<KxDataTable
|
<table className="kx-table kx-table--zebra kx-adm-rules">
|
||||||
columns={ruleColumns}
|
<thead>
|
||||||
rows={rules}
|
<tr>
|
||||||
rowKey={(r) => r.code}
|
<th>{t('ruleset.colRuleName')}</th>
|
||||||
csvFileName={`ruleset_${activeName ?? 'rules'}`}
|
<th>{t('ruleset.colValue')}</th>
|
||||||
ariaLabel={t('ruleset.ruleList')}
|
<th>{t('ruleset.colLogic')}</th>
|
||||||
/>
|
<th>{t('ruleset.colSeverity')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rules.map((r) => (
|
||||||
|
<tr key={r.code}>
|
||||||
|
<td>
|
||||||
|
<span className="kx-adm-rulename">{r.label ?? r.code}</span>
|
||||||
|
<span className="kx-adm-rulecode">{r.code}</span>
|
||||||
|
</td>
|
||||||
|
<td className="kx-adm-mono">{formatValue(t, r)}</td>
|
||||||
|
<td className="kx-adm-muted">{formatLogic(t, r)}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={`kx-adm-pill kx-adm-pill--${r.severity === 'block' ? 'error' : 'warn'}`}
|
||||||
|
>
|
||||||
|
{r.severity === 'block' ? t('ruleset.block') : t('ruleset.warn')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { Button } from '../../components/ui/Button';
|
import { Button } from '../../components/ui/Button';
|
||||||
import { ErrorState, Skeleton } from '../../components/ui/States';
|
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
|
||||||
import { KxDataTable, type KxColumn } from '../../components/KxDataTable';
|
import { KxDataTable, type KxColumn } from '../../components/KxDataTable';
|
||||||
import { IconCheck, IconPlus } from '../../components/ui/icons';
|
import { IconCheck, IconPlus } from '../../components/ui/icons';
|
||||||
import { errMessage, useToast } from '../work/workShared';
|
import { errMessage, useToast } from '../work/workShared';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* M15 공사/장치 옥션 시연용 샘플 데이터.
|
* M15 공사/장치 옥션 시연용 샘플 데이터.
|
||||||
* ★ M15 역경매 엔진은 백엔드 실구현 완료(auctionApi 실배선). 본 파일은 오프라인/미연결 강등 시 시연용 폴백 데이터일 뿐이며 정상 경로는 실 API 를 소비한다.
|
* ★ M15 역경매 엔진은 미구현 — 실 API 연동 전까지 화면 시연을 위한 정적 데이터.
|
||||||
* 존재하지 않는 엔드포인트를 fetch 하지 않으며, 모든 화면 상단에 "샘플 데이터" 배지를 노출한다.
|
* 존재하지 않는 엔드포인트를 fetch 하지 않으며, 모든 화면 상단에 "샘플 데이터" 배지를 노출한다.
|
||||||
* 응찰/낙찰 등 뮤테이션은 로컬 상태 데모로만 처리한다.
|
* 응찰/낙찰 등 뮤테이션은 로컬 상태 데모로만 처리한다.
|
||||||
* 보안 도메인 규칙(UI 반영):
|
* 보안 도메인 규칙(UI 반영):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* M17 CMS 화면(SCR-35 게시 워크플로 · SCR-36 마이크로사이트 편집기 · SCR-37 다국어 관리) 공유 스타일.
|
* M17 CMS 화면(SCR-35 게시 워크플로 · SCR-36 마이크로사이트 편집기 · SCR-37 다국어 관리) 공유 스타일.
|
||||||
* design.md §1 토큰(tokens.css)만 참조. shared.css 프리미티브(kx-page·kx-card·kx-seg·kx-select·kx-kpi·kx-barlist) 위에 얹는다.
|
* design.md §1 토큰(tokens.css)만 참조. shared.css 프리미티브(kx-page·kx-card·kx-seg·kx-select·kx-kpi·kx-barlist) 위에 얹는다.
|
||||||
* M17 백엔드 실구현 완료(cmsApi 실배선). 화면은 정상 경로에서 실 API 를 소비하며, 로컬 샘플은 강등 시 폴백 데모용이다.
|
* M17 백엔드 미구현 → 전 화면 로컬 샘플 데이터 데모. 존재하지 않는 API 호출 없음.
|
||||||
*/
|
*/
|
||||||
@import '../shared.css';
|
@import '../shared.css';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SCR-33 EDM·캠페인 · SCR-34 스폰서십 샘플 데이터 (M12).
|
* SCR-33 EDM·캠페인 · SCR-34 스폰서십 샘플 데이터 (M12).
|
||||||
* ★ M12 백엔드 실구현 완료(marketingApi 실배선 — 캠페인 상태 전이까지). 화면은 정상 경로에서 실 API 를 소비하며, 본 데이터는 강등 시 폴백이다. (실 이메일 발송은 SMTP 연동 후속 — G-04.)
|
* ★ M12 백엔드 미구현 → 화면은 실 API 미호출, 본 시연 데이터 표시("샘플 데이터" 배지).
|
||||||
* ★ 세그먼트 규모는 M10 관람객 데이터 파생 가정치이며 실제 개인정보는 포함하지 않는다(N2).
|
* ★ 세그먼트 규모는 M10 관람객 데이터 파생 가정치이며 실제 개인정보는 포함하지 않는다(N2).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SCR-30 관람객 등록 관리 · SCR-32 리드 관리 샘플 데이터 (M10/M11).
|
* SCR-30 관람객 등록 관리 · SCR-32 리드 관리 샘플 데이터 (M10/M11).
|
||||||
* ★ M10/M11 백엔드 실구현 완료(visitorApi 실배선). 화면은 정상 경로에서 실 API 를 소비하며, 본 데이터는 NETWORK/NOT_FOUND/NOT_IMPLEMENTED 강등 시에만 노출되는 오프라인 폴백("오프라인 — 샘플" 배지)이다.
|
* ★ M10/M11 백엔드 미구현 → 화면은 실 API 를 호출하지 않고 본 시연 데이터를 표시한다("샘플 데이터" 배지).
|
||||||
* ★ 보안 불변(PII 미노출): 이름·연락처·이메일은 이미 마스킹된 형태로만 보관/노출한다.
|
* ★ 보안 불변(PII 미노출): 이름·연락처·이메일은 이미 마스킹된 형태로만 보관/노출한다.
|
||||||
* 실제 구현 시 원문 PII 는 서버가 보관하고 API 응답에서 마스킹하여 내려준다(N2/R10).
|
* 실제 구현 시 원문 PII 는 서버가 보관하고 API 응답에서 마스킹하여 내려준다(N2/R10).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user