쇼케이스 (/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+ 페이지 마이그레이션 진행 예정.
279 lines
9.7 KiB
TypeScript
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>
|
|
))}
|
|
</>
|
|
);
|
|
}
|