refactor(frontend): 쇼케이스 SSOT 구조 — 카탈로그 레지스트리 + variant 메타

Phase A: 쇼케이스의 카탈로그/variant 정보를 중앙 상수로 끌어올림

- shared/constants/catalogRegistry.ts 신규
  - 19+ 카탈로그의 id/showcaseId/titleKo/titleEn/description/source/items를
    단일 레지스트리로 통합 관리
  - 새 카탈로그 추가 = 레지스트리에 1줄 추가로 쇼케이스 자동 노출
  - CATALOG_REGISTRY + getCatalogById()
- lib/theme/variantMeta.ts 신규
  - BADGE_INTENT_META: 8 intent의 titleKo/titleEn/description
  - BUTTON_VARIANT_META: 5 variant의 titleKo/titleEn/description
  - BADGE_INTENT_ORDER/SIZE_ORDER, BUTTON_VARIANT_ORDER/SIZE_ORDER
- 쇼케이스 섹션 리팩토링 — 하드코딩 제거
  - CatalogSection: CATALOG_REGISTRY 자동 열거 (CATALOGS 배열 삭제)
  - BadgeSection: BADGE_INTENT_META에서 의미 가이드 + titleKo 참조
  - ButtonSection: BUTTON_VARIANT_META에서 의미 가이드 + titleKo 참조

효과:
- 카탈로그의 라벨/색상/intent 변경 시 쇼케이스와 실 페이지 동시 반영
- Badge/Button의 variant 의미가 variantMeta 한 곳에서 관리됨
- 쇼케이스 섹션에 분산돼 있던 하드코딩 제거 (INTENT_USAGE, VARIANT_USAGE 등)

다음 단계: 실 페이지를 PageContainer/PageHeader/Button/Input으로 마이그레이션
This commit is contained in:
htlee 2026-04-08 11:42:43 +09:00
부모 4170824f15
커밋 52749638ef
5개의 변경된 파일476개의 추가작업 그리고 261개의 파일을 삭제

파일 보기

@ -1,20 +1,10 @@
import { TrkSectionHeader, Trk } from '../lib/Trk';
import { Badge } from '@shared/components/ui/badge';
import type { BadgeIntent, BadgeSize } from '@lib/theme/variants';
const INTENTS: BadgeIntent[] = ['critical', 'high', 'warning', 'info', 'success', 'muted', 'purple', 'cyan'];
const SIZES: BadgeSize[] = ['xs', 'sm', 'md', 'lg'];
const INTENT_USAGE: Record<BadgeIntent, string> = {
critical: '심각 · 긴급 · 위험 (빨강 계열)',
high: '높음 · 경고 (주황 계열)',
warning: '주의 · 보류 (노랑 계열)',
info: '일반 · 정보 (파랑 계열, 기본값)',
success: '성공 · 완료 · 정상 (초록 계열)',
muted: '비활성 · 중립 · 기타 (회색 계열)',
purple: '분석 · AI · 특수 (보라 계열)',
cyan: '모니터링 · 스트림 (청록 계열)',
};
import {
BADGE_INTENT_META,
BADGE_INTENT_ORDER,
BADGE_SIZE_ORDER,
} from '@lib/theme/variantMeta';
export function BadgeSection() {
return (
@ -22,18 +12,20 @@ export function BadgeSection() {
<TrkSectionHeader
id="TRK-SEC-badge"
title="Badge"
description="8 intent × 4 size = 32 변형. CVA + cn()로 className override 허용, !important 없음."
description={`${BADGE_INTENT_ORDER.length} intent × ${BADGE_SIZE_ORDER.length} size = ${BADGE_INTENT_ORDER.length * BADGE_SIZE_ORDER.length} 변형. CVA + cn()로 className override 허용, !important 없음.`}
/>
{/* 32 변형 그리드 */}
<h3 className="text-sm font-semibold text-heading mb-2 mt-2">32 </h3>
{/* 변형 그리드 */}
<h3 className="text-sm font-semibold text-heading mb-2 mt-2"> </h3>
<Trk id="TRK-BADGE-matrix" className="ds-sample">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-left text-[10px] text-hint font-mono pb-2 pr-3">intent / size </th>
{SIZES.map((size) => (
<th className="text-left text-[10px] text-hint font-mono pb-2 pr-3">
intent / size
</th>
{BADGE_SIZE_ORDER.map((size) => (
<th key={size} className="text-left text-[10px] text-hint font-mono pb-2 px-2">
{size}
</th>
@ -41,10 +33,10 @@ export function BadgeSection() {
</tr>
</thead>
<tbody>
{INTENTS.map((intent) => (
{BADGE_INTENT_ORDER.map((intent) => (
<tr key={intent}>
<td className="text-[11px] text-label font-mono pr-3 py-1.5">{intent}</td>
{SIZES.map((size) => (
{BADGE_SIZE_ORDER.map((size) => (
<td key={size} className="px-2 py-1.5">
<Trk id={`TRK-BADGE-${intent}-${size}`} inline>
<Badge intent={intent} size={size}>
@ -60,19 +52,22 @@ export function BadgeSection() {
</div>
</Trk>
{/* intent 의미 가이드 */}
{/* intent 의미 가이드 (variantMeta에서 자동 열거) */}
<h3 className="text-sm font-semibold text-heading mb-2 mt-6">Intent </h3>
<div className="ds-grid ds-grid-2">
{INTENTS.map((intent) => (
<Trk key={intent} id={`TRK-BADGE-usage-${intent}`} className="ds-sample">
<div className="flex items-center gap-3">
<Badge intent={intent} size="md">
{intent.toUpperCase()}
</Badge>
<span className="text-xs text-label">{INTENT_USAGE[intent]}</span>
</div>
</Trk>
))}
{BADGE_INTENT_ORDER.map((intent) => {
const meta = BADGE_INTENT_META[intent];
return (
<Trk key={intent} id={`TRK-BADGE-usage-${intent}`} className="ds-sample">
<div className="flex items-center gap-3">
<Badge intent={intent} size="md">
{meta.titleKo}
</Badge>
<span className="text-xs text-label flex-1">{meta.description}</span>
</div>
</Trk>
);
})}
</div>
{/* 사용 예시 코드 */}
@ -82,7 +77,7 @@ export function BadgeSection() {
{`import { Badge } from '@shared/components/ui/badge';
import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
// 카탈로그 API와 결합
// 카탈로그 API와 결합 — 라벨/색상 변경은 카탈로그 파일에서만
<Badge intent={getAlertLevelIntent('CRITICAL')} size="sm">
{getAlertLevelLabel('CRITICAL', t, lang)}
</Badge>

파일 보기

@ -1,37 +1,32 @@
import { TrkSectionHeader, Trk } from '../lib/Trk';
import { Button } from '@shared/components/ui/button';
import type { ButtonVariant, ButtonSize } from '@lib/theme/variants';
import {
BUTTON_VARIANT_META,
BUTTON_VARIANT_ORDER,
BUTTON_SIZE_ORDER,
} from '@lib/theme/variantMeta';
import { Plus, Download, Trash2, Search, Save } from 'lucide-react';
const VARIANTS: ButtonVariant[] = ['primary', 'secondary', 'ghost', 'outline', 'destructive'];
const SIZES: ButtonSize[] = ['sm', 'md', 'lg'];
const VARIANT_USAGE: Record<ButtonVariant, string> = {
primary: '주요 액션 · 기본 CTA (페이지당 1개 권장)',
secondary: '보조 액션 · 툴바 버튼 (기본값)',
ghost: '고요한 액션 · 리스트 행 내부',
outline: '강조 보조 · 필터 활성화 상태',
destructive: '삭제 · 비활성화 등 위험 액션',
};
export function ButtonSection() {
return (
<>
<TrkSectionHeader
id="TRK-SEC-button"
title="Button"
description="5 variant × 3 size = 15 변형. CVA 기반, 직접 className 작성 금지."
description={`${BUTTON_VARIANT_ORDER.length} variant × ${BUTTON_SIZE_ORDER.length} size = ${BUTTON_VARIANT_ORDER.length * BUTTON_SIZE_ORDER.length} 변형. CVA 기반, 직접 className 작성 금지.`}
/>
{/* 매트릭스 */}
<h3 className="text-sm font-semibold text-heading mb-2 mt-2">15 </h3>
<h3 className="text-sm font-semibold text-heading mb-2 mt-2"> </h3>
<Trk id="TRK-BUTTON-matrix" className="ds-sample">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-left text-[10px] text-hint font-mono pb-2 pr-3">variant / size </th>
{SIZES.map((size) => (
<th className="text-left text-[10px] text-hint font-mono pb-2 pr-3">
variant / size
</th>
{BUTTON_SIZE_ORDER.map((size) => (
<th key={size} className="text-left text-[10px] text-hint font-mono pb-2 px-3">
{size}
</th>
@ -39,10 +34,10 @@ export function ButtonSection() {
</tr>
</thead>
<tbody>
{VARIANTS.map((variant) => (
{BUTTON_VARIANT_ORDER.map((variant) => (
<tr key={variant}>
<td className="text-[11px] text-label font-mono pr-3 py-2">{variant}</td>
{SIZES.map((size) => (
{BUTTON_SIZE_ORDER.map((size) => (
<td key={size} className="px-3 py-2">
<Trk id={`TRK-BUTTON-${variant}-${size}`} inline>
<Button variant={variant} size={size}>
@ -91,19 +86,22 @@ export function ButtonSection() {
</div>
</Trk>
{/* variant 의미 */}
{/* variant 의미 가이드 (variantMeta에서 자동 열거) */}
<h3 className="text-sm font-semibold text-heading mb-2 mt-6">Variant </h3>
<div className="ds-grid ds-grid-2">
{VARIANTS.map((variant) => (
<Trk key={variant} id={`TRK-BUTTON-usage-${variant}`} className="ds-sample">
<div className="flex items-center gap-3">
<Button variant={variant} size="sm">
{variant}
</Button>
<span className="text-xs text-label flex-1">{VARIANT_USAGE[variant]}</span>
</div>
</Trk>
))}
{BUTTON_VARIANT_ORDER.map((variant) => {
const meta = BUTTON_VARIANT_META[variant];
return (
<Trk key={variant} id={`TRK-BUTTON-usage-${variant}`} className="ds-sample">
<div className="flex items-center gap-3">
<Button variant={variant} size="sm">
{meta.titleKo}
</Button>
<span className="text-xs text-label flex-1">{meta.description}</span>
</div>
</Trk>
);
})}
</div>
{/* 사용 예시 */}

파일 보기

@ -2,37 +2,14 @@ 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 { CATALOG_REGISTRY, type CatalogEntry } from '@shared/constants/catalogRegistry';
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';
/**
* `CATALOG_REGISTRY` .
* `catalogRegistry.ts` .
* .
*/
/** 카탈로그 메타 공통 속성 (일부만 있을 수 있음) */
interface AnyMeta {
code: string;
intent?: BadgeIntent;
@ -41,8 +18,6 @@ interface AnyMeta {
label?: string;
}
type AnyCatalog = Record<string, AnyMeta>;
function getKoLabel(meta: AnyMeta): string {
return meta.fallback?.ko ?? meta.label ?? meta.code;
}
@ -51,7 +26,6 @@ function getEnLabel(meta: AnyMeta): string | undefined {
return meta.fallback?.en;
}
/** classes가 문자열인 경우 그대로, 객체인 경우 bg+text 조합 */
function getFallbackClasses(meta: AnyMeta): string | undefined {
if (typeof meta.classes === 'string') return meta.classes;
if (typeof meta.classes === 'object' && meta.classes) {
@ -60,7 +34,6 @@ function getFallbackClasses(meta: AnyMeta): string | undefined {
return undefined;
}
/** 단일 배지 렌더 — Badge(intent) 또는 span(classes) */
function renderBadge(meta: AnyMeta, label: string): ReactNode {
if (meta.intent) {
return (
@ -81,15 +54,14 @@ function renderBadge(meta: AnyMeta, label: string): ReactNode {
);
}
/** 카탈로그 항목을 행 단위로 렌더: [code] [한글 배지] [영문 배지] */
function CatalogBadges({ catalog, idPrefix }: { catalog: AnyCatalog; idPrefix: string }) {
const entries = Object.values(catalog);
function CatalogBadges({ entry }: { entry: CatalogEntry }) {
const items = Object.values(entry.items) as AnyMeta[];
return (
<div className="space-y-1.5">
{entries.map((meta) => {
{items.map((meta) => {
const koLabel = getKoLabel(meta);
const enLabel = getEnLabel(meta);
const trkId = `${idPrefix}-${meta.code}`;
const trkId = `${entry.showcaseId}-${meta.code}`;
return (
<Trk key={meta.code} id={trkId} className="flex items-center gap-3 rounded-sm">
<code className="text-[10px] text-hint font-mono whitespace-nowrap w-32 shrink-0 truncate">
@ -110,169 +82,18 @@ function CatalogBadges({ catalog, idPrefix }: { catalog: AnyCatalog; idPrefix: s
);
}
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. 모든 위험도·상태·유형 배지의 단일 정의."
title={`분류 카탈로그 (${CATALOG_REGISTRY.length}+)`}
description="백엔드 enum/code_master 기반 SSOT. 쇼케이스와 실 페이지가 동일 레지스트리 참조."
/>
<Trk id="TRK-CAT-intro" className="ds-sample mb-4">
<p className="text-xs text-label leading-relaxed">
API를 :
API를 (/intent/ ):
</p>
<code className="ds-code mt-2">
{`import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
@ -281,21 +102,27 @@ export function CatalogSection() {
{getAlertLevelLabel(event.level, t, lang)}
</Badge>`}
</code>
<p className="text-[11px] text-hint mt-2">
<strong> </strong>: <code className="font-mono">shared/constants/catalogRegistry.ts</code>
.
</p>
</Trk>
{CATALOGS.map((entry) => (
<Trk key={entry.id} id={entry.id} className="ds-sample mb-3">
{CATALOG_REGISTRY.map((entry) => (
<Trk key={entry.id} id={entry.showcaseId} 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 className="flex items-baseline gap-2 flex-wrap">
<h3 className="text-sm font-semibold text-heading">
{entry.titleKo} · {entry.titleEn}
</h3>
<code className="text-[10px] text-hint font-mono">{entry.showcaseId}</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} />
<CatalogBadges entry={entry} />
</Trk>
))}
</>

파일 보기

@ -0,0 +1,128 @@
/**
* variant (Single Source of Truth)
*
* variants.ts는 CVA .
* variant의 **//**
* .
*
* ) BadgeSection의 "intent 의미 가이드" .
* variant .
*/
import type { BadgeIntent, BadgeSize, ButtonVariant, ButtonSize } from './variants';
export interface VariantMeta<K extends string> {
key: K;
titleKo: string;
titleEn: string;
description: string;
}
// ── Badge intent 의미 ──────────────────────────
export const BADGE_INTENT_META: Record<BadgeIntent, VariantMeta<BadgeIntent>> = {
critical: {
key: 'critical',
titleKo: '심각',
titleEn: 'Critical',
description: '심각 · 긴급 · 위험 (빨강 계열)',
},
high: {
key: 'high',
titleKo: '높음',
titleEn: 'High',
description: '높음 · 경고 (주황 계열)',
},
warning: {
key: 'warning',
titleKo: '주의',
titleEn: 'Warning',
description: '주의 · 보류 (노랑 계열)',
},
info: {
key: 'info',
titleKo: '정보',
titleEn: 'Info',
description: '일반 · 정보 (파랑 계열, 기본값)',
},
success: {
key: 'success',
titleKo: '성공',
titleEn: 'Success',
description: '성공 · 완료 · 정상 (초록 계열)',
},
muted: {
key: 'muted',
titleKo: '중립',
titleEn: 'Muted',
description: '비활성 · 중립 · 기타 (회색 계열)',
},
purple: {
key: 'purple',
titleKo: '분석',
titleEn: 'Purple',
description: '분석 · AI · 특수 (보라 계열)',
},
cyan: {
key: 'cyan',
titleKo: '모니터',
titleEn: 'Cyan',
description: '모니터링 · 스트림 (청록 계열)',
},
};
export const BADGE_INTENT_ORDER: BadgeIntent[] = [
'critical',
'high',
'warning',
'info',
'success',
'muted',
'purple',
'cyan',
];
export const BADGE_SIZE_ORDER: BadgeSize[] = ['xs', 'sm', 'md', 'lg'];
// ── Button variant 의미 ────────────────────────
export const BUTTON_VARIANT_META: Record<ButtonVariant, VariantMeta<ButtonVariant>> = {
primary: {
key: 'primary',
titleKo: '주요',
titleEn: 'Primary',
description: '주요 액션 · 기본 CTA (페이지당 1개 권장)',
},
secondary: {
key: 'secondary',
titleKo: '보조',
titleEn: 'Secondary',
description: '보조 액션 · 툴바 버튼 (기본값)',
},
ghost: {
key: 'ghost',
titleKo: '고요',
titleEn: 'Ghost',
description: '고요한 액션 · 리스트 행 내부',
},
outline: {
key: 'outline',
titleKo: '윤곽',
titleEn: 'Outline',
description: '강조 보조 · 필터 활성화 상태',
},
destructive: {
key: 'destructive',
titleKo: '위험',
titleEn: 'Destructive',
description: '삭제 · 비활성화 등 위험 액션',
},
};
export const BUTTON_VARIANT_ORDER: ButtonVariant[] = [
'primary',
'secondary',
'ghost',
'outline',
'destructive',
];
export const BUTTON_SIZE_ORDER: ButtonSize[] = ['sm', 'md', 'lg'];

파일 보기

@ -0,0 +1,267 @@
/**
* (Single Source of Truth)
*
* ** **.
* :
* - (: '심각' '매우 심각') +
* - intent/
* - ( )
*
* `items: Record<Code, Meta>` (Meta는 ).
* heterogeneous CatalogEntry의 items는 Record<string, AnyMeta> .
*/
import { ALERT_LEVELS } from './alertLevels';
import { VIOLATION_TYPES } from './violationTypes';
import { EVENT_STATUSES } from './eventStatuses';
import { ENFORCEMENT_ACTIONS } from './enforcementActions';
import { ENFORCEMENT_RESULTS } from './enforcementResults';
import { PATROL_STATUSES } from './patrolStatuses';
import { ENGINE_SEVERITIES } from './engineSeverities';
import { DEVICE_STATUSES } from './deviceStatuses';
import {
PARENT_RESOLUTION_STATUSES,
LABEL_SESSION_STATUSES,
} from './parentResolutionStatuses';
import {
MODEL_STATUSES,
QUALITY_GATE_STATUSES,
EXPERIMENT_STATUSES,
} from './modelDeploymentStatuses';
import { GEAR_GROUP_TYPES } from './gearGroupTypes';
import { DARK_VESSEL_PATTERNS } from './darkVesselPatterns';
import { USER_ACCOUNT_STATUSES } from './userAccountStatuses';
import { LOGIN_RESULTS } from './loginResultStatuses';
import { PERMIT_STATUSES, GEAR_JUDGMENTS } from './permissionStatuses';
import {
VESSEL_SURVEILLANCE_STATUSES,
VESSEL_RISK_RINGS,
} from './vesselAnalysisStatuses';
import { CONNECTION_STATUSES } from './connectionStatuses';
import { TRAINING_ZONE_TYPES } from './trainingZoneTypes';
/**
* UI
*/
export interface CatalogEntry {
/** 안정적 ID (쇼케이스 추적 ID의 슬러그 부분) */
id: string;
/** 쇼케이스 추적 ID (TRK-CAT-*) */
showcaseId: string;
/** 쇼케이스 섹션 제목 (한글) */
titleKo: string;
/** 쇼케이스 섹션 제목 (영문) */
titleEn: string;
/** 1줄 설명 */
description: string;
/** 출처 (백엔드 enum / code_master 등) */
source?: string;
/** 카탈로그 데이터 — items의 각 meta는 { code, fallback?, intent?, classes?, ... } 구조 */
items: Record<string, unknown>;
}
/**
*
*
* :
* 1. shared/constants/ (items record + )
* 2. CatalogEntry
* 3. +
*/
export const CATALOG_REGISTRY: CatalogEntry[] = [
{
id: 'alert-level',
showcaseId: 'TRK-CAT-alert-level',
titleKo: '위험도',
titleEn: 'Alert Level',
description: 'CRITICAL / HIGH / MEDIUM / LOW — 모든 이벤트/알림 배지',
source: 'backend code_master EVENT_LEVEL',
items: ALERT_LEVELS,
},
{
id: 'violation-type',
showcaseId: 'TRK-CAT-violation-type',
titleKo: '위반 유형',
titleEn: 'Violation Type',
description: '중국불법조업 / 환적의심 / EEZ침범 등',
source: 'backend ViolationType enum',
items: VIOLATION_TYPES,
},
{
id: 'event-status',
showcaseId: 'TRK-CAT-event-status',
titleKo: '이벤트 상태',
titleEn: 'Event Status',
description: 'NEW / ACK / IN_PROGRESS / RESOLVED / FALSE_POSITIVE',
source: 'backend code_master EVENT_STATUS',
items: EVENT_STATUSES,
},
{
id: 'enforcement-action',
showcaseId: 'TRK-CAT-enforcement-action',
titleKo: '단속 조치',
titleEn: 'Enforcement Action',
description: 'CAPTURE / INSPECT / WARN / DISPERSE / TRACK / EVIDENCE',
source: 'backend code_master ENFORCEMENT_ACTION',
items: ENFORCEMENT_ACTIONS,
},
{
id: 'enforcement-result',
showcaseId: 'TRK-CAT-enforcement-result',
titleKo: '단속 결과',
titleEn: 'Enforcement Result',
description: 'PUNISHED / REFERRED / WARNED / RELEASED / FALSE_POSITIVE',
source: 'backend code_master ENFORCEMENT_RESULT',
items: ENFORCEMENT_RESULTS,
},
{
id: 'patrol-status',
showcaseId: 'TRK-CAT-patrol-status',
titleKo: '함정 상태',
titleEn: 'Patrol Status',
description: '출동 / 순찰 / 복귀 / 정박 / 정비 / 대기',
source: 'backend code_master PATROL_STATUS',
items: PATROL_STATUSES,
},
{
id: 'engine-severity',
showcaseId: 'TRK-CAT-engine-severity',
titleKo: '엔진 심각도',
titleEn: 'Engine Severity',
description: 'AI 모델/분석엔진 오류 심각도',
items: ENGINE_SEVERITIES,
},
{
id: 'device-status',
showcaseId: 'TRK-CAT-device-status',
titleKo: '함정 Agent 장치 상태',
titleEn: 'Device Status',
description: 'ONLINE / OFFLINE / SYNCING / NOT_DEPLOYED',
items: DEVICE_STATUSES,
},
{
id: 'parent-resolution',
showcaseId: 'TRK-CAT-parent-resolution',
titleKo: '모선 확정 상태',
titleEn: 'Parent Resolution Status',
description: 'PENDING / CONFIRMED / REJECTED / REVIEWING',
items: PARENT_RESOLUTION_STATUSES,
},
{
id: 'label-session',
showcaseId: 'TRK-CAT-label-session',
titleKo: '라벨 세션',
titleEn: 'Label Session Status',
description: '모선 학습 세션 상태',
items: LABEL_SESSION_STATUSES,
},
{
id: 'model-status',
showcaseId: 'TRK-CAT-model-status',
titleKo: 'AI 모델 상태',
titleEn: 'Model Status',
description: 'DEV / STAGING / CANARY / PROD / ARCHIVED',
items: MODEL_STATUSES,
},
{
id: 'quality-gate',
showcaseId: 'TRK-CAT-quality-gate',
titleKo: '품질 게이트',
titleEn: 'Quality Gate Status',
description: '모델 배포 전 품질 검증',
items: QUALITY_GATE_STATUSES,
},
{
id: 'experiment',
showcaseId: 'TRK-CAT-experiment',
titleKo: 'ML 실험',
titleEn: 'Experiment Status',
description: 'MLOps 실험 상태',
items: EXPERIMENT_STATUSES,
},
{
id: 'gear-group',
showcaseId: 'TRK-CAT-gear-group',
titleKo: '어구 그룹 유형',
titleEn: 'Gear Group Type',
description: 'FLEET / GEAR_IN_ZONE / GEAR_OUT_ZONE',
items: GEAR_GROUP_TYPES,
},
{
id: 'dark-vessel',
showcaseId: 'TRK-CAT-dark-vessel',
titleKo: '다크베셀 패턴',
titleEn: 'Dark Vessel Pattern',
description: 'AIS 끊김/스푸핑 패턴 5종',
items: DARK_VESSEL_PATTERNS,
},
{
id: 'user-account',
showcaseId: 'TRK-CAT-user-account',
titleKo: '사용자 계정 상태',
titleEn: 'User Account Status',
description: 'ACTIVE / LOCKED / INACTIVE / PENDING',
items: USER_ACCOUNT_STATUSES,
},
{
id: 'login-result',
showcaseId: 'TRK-CAT-login-result',
titleKo: '로그인 결과',
titleEn: 'Login Result',
description: 'SUCCESS / FAILED / LOCKED',
items: LOGIN_RESULTS,
},
{
id: 'permit-status',
showcaseId: 'TRK-CAT-permit-status',
titleKo: '허가 상태',
titleEn: 'Permit Status',
description: '선박 허가 유효 / 만료 / 정지',
items: PERMIT_STATUSES,
},
{
id: 'gear-judgment',
showcaseId: 'TRK-CAT-gear-judgment',
titleKo: '어구 판정',
titleEn: 'Gear Judgment',
description: '합법 / 의심 / 불법',
items: GEAR_JUDGMENTS,
},
{
id: 'vessel-surveillance',
showcaseId: 'TRK-CAT-vessel-surveillance',
titleKo: '선박 감시 상태',
titleEn: 'Vessel Surveillance Status',
description: '관심선박 추적 상태',
items: VESSEL_SURVEILLANCE_STATUSES,
},
{
id: 'vessel-risk-ring',
showcaseId: 'TRK-CAT-vessel-risk-ring',
titleKo: '선박 위험 링',
titleEn: 'Vessel Risk Ring',
description: '지도 마커 위험도 링',
items: VESSEL_RISK_RINGS,
},
{
id: 'connection',
showcaseId: 'TRK-CAT-connection',
titleKo: '연결 상태',
titleEn: 'Connection Status',
description: 'OK / WARNING / ERROR',
items: CONNECTION_STATUSES,
},
{
id: 'training-zone',
showcaseId: 'TRK-CAT-training-zone',
titleKo: '훈련 수역',
titleEn: 'Training Zone Type',
description: 'NAVY / AIRFORCE / ARMY / ADD / KCG',
items: TRAINING_ZONE_TYPES,
},
];
/** ID로 특정 카탈로그 조회 */
export function getCatalogById(id: string): CatalogEntry | undefined {
return CATALOG_REGISTRY.find((c) => c.id === id);
}