kcg-ai-monitoring/frontend/src/design-system/sections/CatalogSection.tsx
htlee d7f8db88ee fix(frontend): Badge 다크 팔레트를 translucent로 통일 + 쇼케이스 한/영 병기
- badgeVariants 다크 팔레트를 classes 기반 4종 카탈로그와 동일 패턴으로 통일
  - 이전: dark:bg-X-400 dark:text-slate-900 dark:border-X-600 (솔리드)
  - 이후: dark:bg-X-500/20 dark:text-X-400 dark:border-X-500/30 (translucent)
  - 라이트 팔레트는 그대로 유지 (이미 통일되어 있음)
- CatalogSection: 각 카탈로그 항목을 [code / 한글 배지 / 영문 배지] 3열 행으로 렌더
  - 한글/영문 라벨 두 버전을 한눈에 비교 검토 가능
  - 추적 ID Trk는 행 전체를 감싸서 호버/복사 동작
2026-04-08 11:28:02 +09:00

304 lines
10 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 getKoLabel(meta: AnyMeta): string {
return meta.fallback?.ko ?? meta.label ?? meta.code;
}
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) {
return [meta.classes.bg, meta.classes.text, meta.classes.border].filter(Boolean).join(' ');
}
return undefined;
}
/** 단일 배지 렌더 — Badge(intent) 또는 span(classes) */
function renderBadge(meta: AnyMeta, label: string): ReactNode {
if (meta.intent) {
return (
<Badge intent={meta.intent} size="sm">
{label}
</Badge>
);
}
const classes =
getFallbackClasses(meta) ??
'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-500/20 dark:text-slate-300 dark:border-slate-500/30';
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded-md border text-[12px] font-semibold ${classes}`}
>
{label}
</span>
);
}
/** 카탈로그 항목을 행 단위로 렌더: [code] [한글 배지] [영문 배지] */
function CatalogBadges({ catalog, idPrefix }: { catalog: AnyCatalog; idPrefix: string }) {
const entries = Object.values(catalog);
return (
<div className="space-y-1.5">
{entries.map((meta) => {
const koLabel = getKoLabel(meta);
const enLabel = getEnLabel(meta);
const trkId = `${idPrefix}-${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">
{meta.code}
</code>
<div className="flex-1">{renderBadge(meta, koLabel)}</div>
<div className="flex-1">
{enLabel ? (
renderBadge(meta, enLabel)
) : (
<span className="text-[10px] text-hint italic">(no en)</span>
)}
</div>
</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>
))}
</>
);
}