kcg-ai-monitoring/frontend/src/design-system/sections/CatalogSection.tsx
htlee e0b51efc54 feat(frontend): 디자인 시스템 쇼케이스 페이지 + 신규 공통 컴포넌트
쇼케이스 (/design-system.html):
- 별도 Vite entry (System Flow 패턴 재사용, 메인 SPA 분리)
- 10개 섹션: Intro / Token / Typography / Badge / Button / Form /
  Card / Layout / Catalog (19+) / Guide
- 추적 ID 체계 (TRK-CATEGORY-SLUG):
  - hover 시 툴팁 + "ID 복사 모드"에서 클릭 시 클립보드 복사
  - URL hash 딥링크 (#trk=TRK-BADGE-critical-sm) 스크롤+하이라이트
  - 산출문서/논의에서 특정 변형 정확히 참조 가능
- Dark/Light 테마 토글로 양쪽 시각 검증

신규 공통 컴포넌트:
- Button (@shared/components/ui/button.tsx)
  - 5 variant × 3 size = 15 변형
  - primary/secondary/ghost/outline/destructive × sm/md/lg
- Input / Select / Textarea / Checkbox / Radio
  - Input · Select 공통 inputVariants 공유 (sm/md/lg × default/error/success)
- PageContainer / PageHeader / Section (shared/components/layout/)
  - PageContainer: size sm/md/lg + fullBleed (지도/풀화면 예외)
  - PageHeader: title + description + icon + demo 배지 + actions 슬롯
  - Section: Card + CardHeader + CardTitle + CardContent 단축

variants.ts 확장:
- buttonVariants / inputVariants / pageContainerVariants CVA 정의
- Button/Input/Select는 variants.ts에서 import하여 fast-refresh 경고 회피

빌드 검증 완료:
- TypeScript 타입 체크 통과
- ESLint 통과 (경고 0)
- vite build: designSystem-*.js 54KB (메인 SPA와 분리)

이 쇼케이스가 확정된 후 실제 40+ 페이지 마이그레이션 진행 예정.
2026-04-08 11:09:36 +09:00

279 lines
9.7 KiB
TypeScript

import { type ReactNode } from 'react';
import { TrkSectionHeader, Trk } from '../lib/Trk';
import { Badge } from '@shared/components/ui/badge';
import type { BadgeIntent } from '@lib/theme/variants';
import { ALERT_LEVELS } from '@shared/constants/alertLevels';
import { VIOLATION_TYPES } from '@shared/constants/violationTypes';
import { EVENT_STATUSES } from '@shared/constants/eventStatuses';
import { ENFORCEMENT_ACTIONS } from '@shared/constants/enforcementActions';
import { ENFORCEMENT_RESULTS } from '@shared/constants/enforcementResults';
import { PATROL_STATUSES } from '@shared/constants/patrolStatuses';
import { ENGINE_SEVERITIES } from '@shared/constants/engineSeverities';
import { DEVICE_STATUSES } from '@shared/constants/deviceStatuses';
import {
PARENT_RESOLUTION_STATUSES,
LABEL_SESSION_STATUSES,
} from '@shared/constants/parentResolutionStatuses';
import {
MODEL_STATUSES,
QUALITY_GATE_STATUSES,
EXPERIMENT_STATUSES,
} from '@shared/constants/modelDeploymentStatuses';
import { GEAR_GROUP_TYPES } from '@shared/constants/gearGroupTypes';
import { DARK_VESSEL_PATTERNS } from '@shared/constants/darkVesselPatterns';
import { USER_ACCOUNT_STATUSES } from '@shared/constants/userAccountStatuses';
import { LOGIN_RESULTS } from '@shared/constants/loginResultStatuses';
import { PERMIT_STATUSES, GEAR_JUDGMENTS } from '@shared/constants/permissionStatuses';
import {
VESSEL_SURVEILLANCE_STATUSES,
VESSEL_RISK_RINGS,
} from '@shared/constants/vesselAnalysisStatuses';
import { CONNECTION_STATUSES } from '@shared/constants/connectionStatuses';
import { TRAINING_ZONE_TYPES } from '@shared/constants/trainingZoneTypes';
/** 카탈로그 메타 공통 속성 (일부만 있을 수 있음) */
interface AnyMeta {
code: string;
intent?: BadgeIntent;
fallback?: { ko: string; en: string };
classes?: string | { bg?: string; text?: string; border?: string };
label?: string;
}
type AnyCatalog = Record<string, AnyMeta>;
function getLabel(meta: AnyMeta): string {
return meta.fallback?.ko ?? meta.label ?? meta.code;
}
/** classes가 문자열인 경우 그대로, 객체인 경우 bg+text 조합 */
function getFallbackClasses(meta: AnyMeta): string | undefined {
if (typeof meta.classes === 'string') return meta.classes;
if (typeof meta.classes === 'object' && meta.classes) {
return [meta.classes.bg, meta.classes.text, meta.classes.border].filter(Boolean).join(' ');
}
return undefined;
}
/** 카탈로그 항목을 Badge 또는 fallback span으로 렌더 */
function CatalogBadges({ catalog, idPrefix }: { catalog: AnyCatalog; idPrefix: string }) {
const entries = Object.values(catalog);
return (
<div className="flex flex-wrap gap-2">
{entries.map((meta) => {
const label = getLabel(meta);
const trkId = `${idPrefix}-${meta.code}`;
const content: ReactNode = meta.intent ? (
<Badge intent={meta.intent} size="sm">
{label}
</Badge>
) : (
<span
className={`inline-flex items-center px-2 py-0.5 rounded-md border text-[12px] font-semibold ${getFallbackClasses(meta) ?? 'bg-slate-500/20 text-slate-300 border-slate-500/30'}`}
>
{label}
</span>
);
return (
<Trk key={meta.code} id={trkId} inline>
{content}
</Trk>
);
})}
</div>
);
}
interface CatalogEntry {
id: string;
title: string;
description: string;
catalog: AnyCatalog;
source?: string;
}
const CATALOGS: CatalogEntry[] = [
{
id: 'TRK-CAT-alert-level',
title: '위험도 · AlertLevel',
description: 'CRITICAL / HIGH / MEDIUM / LOW — 모든 이벤트/알림 배지',
catalog: ALERT_LEVELS as unknown as AnyCatalog,
source: 'backend code_master EVENT_LEVEL',
},
{
id: 'TRK-CAT-violation-type',
title: '위반 유형 · ViolationType',
description: '중국불법조업 / 환적의심 / EEZ침범 등',
catalog: VIOLATION_TYPES as unknown as AnyCatalog,
source: 'backend ViolationType enum',
},
{
id: 'TRK-CAT-event-status',
title: '이벤트 상태 · EventStatus',
description: 'NEW / ACK / IN_PROGRESS / RESOLVED / FALSE_POSITIVE',
catalog: EVENT_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-enforcement-action',
title: '단속 조치 · EnforcementAction',
description: 'CAPTURE / INSPECT / WARN / DISPERSE / TRACK / EVIDENCE',
catalog: ENFORCEMENT_ACTIONS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-enforcement-result',
title: '단속 결과 · EnforcementResult',
description: 'PUNISHED / REFERRED / WARNED / RELEASED / FALSE_POSITIVE',
catalog: ENFORCEMENT_RESULTS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-patrol-status',
title: '함정 상태 · PatrolStatus',
description: '출동 / 순찰 / 복귀 / 정박 / 정비 / 대기',
catalog: PATROL_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-engine-severity',
title: '엔진 심각도 · EngineSeverity',
description: 'AI 모델/분석엔진 오류 심각도',
catalog: ENGINE_SEVERITIES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-device-status',
title: '함정 Agent 장치 상태 · DeviceStatus',
description: 'ONLINE / OFFLINE / SYNCING / NOT_DEPLOYED',
catalog: DEVICE_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-parent-resolution',
title: '모선 확정 상태 · ParentResolutionStatus',
description: 'PENDING / CONFIRMED / REJECTED / REVIEWING',
catalog: PARENT_RESOLUTION_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-label-session',
title: '라벨 세션 · LabelSessionStatus',
description: '모선 학습 세션 상태',
catalog: LABEL_SESSION_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-model-status',
title: 'AI 모델 상태 · ModelStatus',
description: 'DEV / STAGING / CANARY / PROD / ARCHIVED',
catalog: MODEL_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-quality-gate',
title: '품질 게이트 · QualityGateStatus',
description: '모델 배포 전 품질 검증',
catalog: QUALITY_GATE_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-experiment',
title: 'ML 실험 · ExperimentStatus',
description: 'MLOps 실험 상태',
catalog: EXPERIMENT_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-gear-group',
title: '어구 그룹 유형 · GearGroupType',
description: 'FLEET / GEAR_IN_ZONE / GEAR_OUT_ZONE',
catalog: GEAR_GROUP_TYPES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-dark-vessel',
title: '다크베셀 패턴 · DarkVesselPattern',
description: 'AIS 끊김/스푸핑 패턴 5종',
catalog: DARK_VESSEL_PATTERNS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-user-account',
title: '사용자 계정 상태 · UserAccountStatus',
description: 'ACTIVE / LOCKED / INACTIVE / PENDING',
catalog: USER_ACCOUNT_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-login-result',
title: '로그인 결과 · LoginResult',
description: 'SUCCESS / FAILED / LOCKED',
catalog: LOGIN_RESULTS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-permit-status',
title: '허가 상태 · PermitStatus',
description: '선박 허가 유효/만료/정지',
catalog: PERMIT_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-gear-judgment',
title: '어구 판정 · GearJudgment',
description: '합법 / 의심 / 불법',
catalog: GEAR_JUDGMENTS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-vessel-surveillance',
title: '선박 감시 상태 · VesselSurveillanceStatus',
description: '관심선박 추적 상태',
catalog: VESSEL_SURVEILLANCE_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-vessel-risk-ring',
title: '선박 위험 링 · VesselRiskRing',
description: '지도 마커 위험도 링',
catalog: VESSEL_RISK_RINGS as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-connection',
title: '연결 상태 · ConnectionStatus',
description: 'OK / WARNING / ERROR',
catalog: CONNECTION_STATUSES as unknown as AnyCatalog,
},
{
id: 'TRK-CAT-training-zone',
title: '훈련 수역 · TrainingZoneType',
description: 'NAVY / AIRFORCE / ARMY / ADD / KCG',
catalog: TRAINING_ZONE_TYPES as unknown as AnyCatalog,
},
];
export function CatalogSection() {
return (
<>
<TrkSectionHeader
id="TRK-SEC-catalog"
title="분류 카탈로그 (19+)"
description="백엔드 enum/code_master 기반 SSOT. 모든 위험도·상태·유형 배지의 단일 정의."
/>
<Trk id="TRK-CAT-intro" className="ds-sample mb-4">
<p className="text-xs text-label leading-relaxed">
API를 :
</p>
<code className="ds-code mt-2">
{`import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
<Badge intent={getAlertLevelIntent(event.level)} size="sm">
{getAlertLevelLabel(event.level, t, lang)}
</Badge>`}
</code>
</Trk>
{CATALOGS.map((entry) => (
<Trk key={entry.id} id={entry.id} className="ds-sample mb-3">
<div className="mb-2">
<div className="flex items-baseline gap-2">
<h3 className="text-sm font-semibold text-heading">{entry.title}</h3>
<code className="text-[10px] text-hint font-mono">{entry.id}</code>
</div>
<p className="text-[11px] text-hint">{entry.description}</p>
{entry.source && (
<p className="text-[10px] text-hint italic mt-0.5">: {entry.source}</p>
)}
</div>
<CatalogBadges catalog={entry.catalog} idPrefix={entry.id} />
</Trk>
))}
</>
);
}