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:
부모
4170824f15
커밋
52749638ef
@ -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) => (
|
||||
{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">
|
||||
{intent.toUpperCase()}
|
||||
{meta.titleKo}
|
||||
</Badge>
|
||||
<span className="text-xs text-label">{INTENT_USAGE[intent]}</span>
|
||||
<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) => (
|
||||
{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">
|
||||
{variant}
|
||||
{meta.titleKo}
|
||||
</Button>
|
||||
<span className="text-xs text-label flex-1">{VARIANT_USAGE[variant]}</span>
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
|
||||
128
frontend/src/lib/theme/variantMeta.ts
Normal file
128
frontend/src/lib/theme/variantMeta.ts
Normal file
@ -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'];
|
||||
267
frontend/src/shared/constants/catalogRegistry.ts
Normal file
267
frontend/src/shared/constants/catalogRegistry.ts
Normal file
@ -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);
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user