refactor(frontend): Badge className 위반 37건 전수 제거

- 4개 catalog(eventStatuses/enforcementResults/enforcementActions/patrolStatuses)에
  intent 필드 추가 + getXxxIntent() 헬퍼 신규
- statusIntent.ts 공통 유틸: 한글/영문 상태 문자열 → BadgeIntent 매핑
  + getRiskIntent(0-100) 점수 기반 매핑
- 모든 Badge className="..." 패턴을 intent prop으로 치환:
  - admin (AuditLogs/AccessControl/SystemConfig/NoticeManagement/DataHub)
  - ai-operations (AIModelManagement/MLOpsPage)
  - enforcement (EventList/EnforcementHistory)
  - field-ops (AIAlert)
  - detection (GearIdentification)
  - patrol (PatrolRoute/FleetOptimization)
  - parent-inference (ParentExclusion)
  - statistics (ExternalService/ReportManagement)
  - surveillance (MapControl)
  - risk-assessment (EnforcementPlan)
  - monitoring (SystemStatusPanel — ServiceCard statusColor → statusIntent 리팩토)
  - dashboard (Dashboard PatrolStatusBadge)

이제 Badge의 테마별 팔레트(라이트 파스텔 + 다크 translucent)가 자동 적용되며,
쇼케이스에서 palette 조정 시 모든 Badge 사용처에 일관되게 반영됨.
This commit is contained in:
htlee 2026-04-08 12:28:23 +09:00
부모 85cb6b40a2
커밋 2483174081
26개의 변경된 파일321개의 추가작업 그리고 113개의 파일을 삭제

파일 보기

@ -176,8 +176,7 @@ export function AccessControl() {
{ key: 'result', label: '결과', width: '70px', sortable: true,
render: (v) => {
const r = v as string;
const c = r === 'SUCCESS' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400';
return <Badge className={`border-0 text-[9px] ${c}`}>{r || '-'}</Badge>;
return <Badge intent={r === 'SUCCESS' ? 'success' : 'critical'} size="xs">{r || '-'}</Badge>;
},
},
{ key: 'failReason', label: '실패 사유',

파일 보기

@ -103,7 +103,7 @@ export function AuditLogs() {
<td className="px-3 py-2 text-heading font-medium">{it.actionCd}</td>
<td className="px-3 py-2 text-muted-foreground">{it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''}</td>
<td className="px-3 py-2">
<Badge className={`border-0 text-[9px] ${it.result === 'SUCCESS' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
<Badge intent={it.result === 'SUCCESS' ? 'success' : 'critical'} size="xs">
{it.result || '-'}
</Badge>
</td>

파일 보기

@ -7,6 +7,15 @@ import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { SaveButton } from '@shared/components/common/SaveButton';
import { getConnectionStatusHex } from '@shared/constants/connectionStatuses';
import type { BadgeIntent } from '@lib/theme/variants';
/** 수집/적재 작업 상태 → BadgeIntent 매핑 (DataHub 로컬 전용) */
function jobStatusIntent(s: string): BadgeIntent {
if (s === '수행중') return 'success';
if (s === '대기중') return 'warning';
if (s === '장애발생') return 'critical';
return 'muted';
}
import {
Database, RefreshCw, Calendar, Wifi, WifiOff, Radio,
Activity, Server, ArrowDownToLine, Clock, AlertTriangle,
@ -128,7 +137,7 @@ const channelColumns: DataColumn<ChannelRecord>[] = [
const on = v === 'ON';
return (
<div className="flex flex-col items-center gap-0.5">
<Badge className={`border-0 text-[9px] font-bold px-3 ${on ? 'bg-blue-600 text-on-vivid' : 'bg-red-500 text-on-vivid'}`}>
<Badge intent={on ? 'info' : 'critical'} size="xs">
{v as string}
</Badge>
{row.lastUpdate && (
@ -208,18 +217,14 @@ const collectColumns: DataColumn<CollectJob>[] = [
{ key: 'serverType', label: '타입', width: '60px', align: 'center', sortable: true,
render: (v) => {
const t = v as string;
const c = t === 'SQL' ? 'bg-blue-500/20 text-blue-400' : t === 'FILE' ? 'bg-green-500/20 text-green-400' : 'bg-purple-500/20 text-purple-400';
return <Badge className={`border-0 text-[9px] ${c}`}>{t}</Badge>;
const intent: BadgeIntent = t === 'SQL' ? 'info' : t === 'FILE' ? 'success' : 'purple';
return <Badge intent={intent} size="xs">{t}</Badge>;
},
},
{ key: 'serverName', label: '서버명', width: '120px', render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
{ key: 'serverIp', label: 'IP', width: '120px', render: (v) => <span className="text-hint font-mono text-[10px]">{v as string}</span> },
{ key: 'status', label: '상태', width: '80px', align: 'center', sortable: true,
render: (v) => {
const s = v as JobStatus;
const c = s === '수행중' ? 'bg-green-500/20 text-green-400' : s === '대기중' ? 'bg-yellow-500/20 text-yellow-400' : s === '장애발생' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground';
return <Badge className={`border-0 text-[9px] ${c}`}>{s}</Badge>;
},
render: (v) => <Badge intent={jobStatusIntent(v as string)} size="xs">{v as string}</Badge>,
},
{ key: 'schedule', label: '스케줄', width: '80px' },
{ key: 'lastRun', label: '최종 수행', width: '140px', sortable: true, render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
@ -277,11 +282,7 @@ const loadColumns: DataColumn<LoadJob>[] = [
{ key: 'targetTable', label: '대상 테이블', width: '140px', render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
{ key: 'targetDb', label: 'DB', width: '70px', align: 'center' },
{ key: 'status', label: '상태', width: '80px', align: 'center', sortable: true,
render: (v) => {
const s = v as JobStatus;
const c = s === '수행중' ? 'bg-green-500/20 text-green-400' : s === '대기중' ? 'bg-yellow-500/20 text-yellow-400' : s === '장애발생' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground';
return <Badge className={`border-0 text-[9px] ${c}`}>{s}</Badge>;
},
render: (v) => <Badge intent={jobStatusIntent(v as string)} size="xs">{v as string}</Badge>,
},
{ key: 'schedule', label: '스케줄', width: '80px' },
{ key: 'lastRun', label: '최종 적재', width: '140px', sortable: true, render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
@ -626,7 +627,6 @@ export function DataHub() {
{/* 연계서버 카드 그리드 */}
<div className="grid grid-cols-2 gap-3">
{filteredAgents.map((agent) => {
const stColor = agent.status === '수행중' ? 'text-green-400 bg-green-500/15' : agent.status === '대기중' ? 'text-yellow-400 bg-yellow-500/15' : agent.status === '장애발생' ? 'text-red-400 bg-red-500/15' : 'text-muted-foreground bg-muted';
return (
<Card key={agent.id} className="bg-surface-raised border-border">
<CardContent className="p-4">
@ -635,7 +635,7 @@ export function DataHub() {
<div className="text-[12px] font-bold text-heading">{agent.name}</div>
<div className="text-[10px] text-hint">{agent.id} · {agent.role}</div>
</div>
<Badge className={`border-0 text-[9px] ${stColor}`}>{agent.status}</Badge>
<Badge intent={jobStatusIntent(agent.status)} size="xs">{agent.status}</Badge>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px] mb-3">
<div className="flex justify-between"><span className="text-hint">Hostname</span><span className="text-label font-mono">{agent.hostname}</span></div>

파일 보기

@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/
import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout';
import type { BadgeIntent } from '@lib/theme/variants';
import {
Bell, Plus, Edit2, Trash2, Eye, EyeOff, Calendar,
Users, Megaphone, AlertTriangle, Info, Search, Filter,
@ -125,10 +126,10 @@ export function NoticeManagement() {
}));
};
const getStatus = (n: SystemNotice) => {
if (n.endDate < now) return { label: '종료', color: 'bg-muted text-muted-foreground' };
if (n.startDate > now) return { label: '예약', color: 'bg-blue-500/20 text-blue-400' };
return { label: '노출 중', color: 'bg-green-500/20 text-green-400' };
const getStatus = (n: SystemNotice): { label: string; intent: BadgeIntent } => {
if (n.endDate < now) return { label: '종료', intent: 'muted' };
if (n.startDate > now) return { label: '예약', intent: 'info' };
return { label: '노출 중', intent: 'success' };
};
// KPI
@ -203,7 +204,7 @@ export function NoticeManagement() {
return (
<tr key={n.id} className="border-b border-border hover:bg-surface-overlay">
<td className="px-2 py-1.5">
<Badge className={`border-0 text-[9px] ${status.color}`}>{status.label}</Badge>
<Badge intent={status.intent} size="xs">{status.label}</Badge>
</td>
<td className="px-2 py-1.5">
<span className={`inline-flex items-center gap-1 text-[10px] ${typeOpt.color}`}>

파일 보기

@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/
import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout';
import type { BadgeIntent } from '@lib/theme/variants';
import {
Settings, Database, Search, ChevronDown, ChevronRight,
Map, Fish, Anchor, Ship, Globe, BarChart3, Download,
@ -269,13 +270,10 @@ export function SystemConfig() {
<tr key={a.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-4 py-2 text-cyan-400 font-mono font-medium">{a.code}</td>
<td className="px-4 py-2">
<Badge className={`border-0 text-[9px] ${
a.major === '서해' ? 'bg-blue-500/20 text-blue-400'
: a.major === '남해' ? 'bg-green-500/20 text-green-400'
: a.major === '동해' ? 'bg-purple-500/20 text-purple-400'
: a.major === '제주' ? 'bg-orange-500/20 text-orange-400'
: 'bg-cyan-500/20 text-cyan-400'
}`}>{a.major}</Badge>
{(() => {
const intent: BadgeIntent = a.major === '서해' ? 'info' : a.major === '남해' ? 'success' : a.major === '동해' ? 'purple' : a.major === '제주' ? 'high' : 'cyan';
return <Badge intent={intent} size="xs">{a.major}</Badge>;
})()}
</td>
<td className="px-4 py-2 text-label">{a.mid}</td>
<td className="px-4 py-2 text-heading font-medium">{a.name}</td>
@ -359,25 +357,16 @@ export function SystemConfig() {
<tr key={f.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-4 py-2 text-cyan-400 font-mono font-medium">{f.code}</td>
<td className="px-4 py-2">
<Badge className={`border-0 text-[9px] ${
f.major === '근해어업' ? 'bg-blue-500/20 text-blue-400'
: f.major === '연안어업' ? 'bg-green-500/20 text-green-400'
: f.major === '양식어업' ? 'bg-cyan-500/20 text-cyan-400'
: f.major === '원양어업' ? 'bg-purple-500/20 text-purple-400'
: f.major === '구획어업' ? 'bg-orange-500/20 text-orange-400'
: f.major === '마을어업' ? 'bg-yellow-500/20 text-yellow-400'
: 'bg-muted text-muted-foreground'
}`}>{f.major}</Badge>
{(() => {
const intent: BadgeIntent = f.major === '근해어업' ? 'info' : f.major === '연안어업' ? 'success' : f.major === '양식어업' ? 'cyan' : f.major === '원양어업' ? 'purple' : f.major === '구획어업' ? 'high' : f.major === '마을어업' ? 'warning' : 'muted';
return <Badge intent={intent} size="xs">{f.major}</Badge>;
})()}
</td>
<td className="px-4 py-2 text-label">{f.mid}</td>
<td className="px-4 py-2 text-heading font-medium">{f.name}</td>
<td className="px-4 py-2 text-muted-foreground">{f.target}</td>
<td className="px-4 py-2">
<Badge className={`border-0 text-[9px] ${
f.permit === '허가' ? 'bg-blue-500/20 text-blue-400'
: f.permit === '면허' ? 'bg-green-500/20 text-green-400'
: 'bg-muted text-muted-foreground'
}`}>{f.permit}</Badge>
<Badge intent={f.permit === '허가' ? 'info' : f.permit === '면허' ? 'success' : 'muted'} size="xs">{f.permit}</Badge>
</td>
<td className="px-4 py-2 text-hint text-[10px]">{f.law}</td>
</tr>
@ -410,15 +399,10 @@ export function SystemConfig() {
<tr key={v.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-3 py-2 text-cyan-400 font-mono font-medium">{v.code}</td>
<td className="px-3 py-2">
<Badge className={`border-0 text-[9px] ${
v.major === '어선' ? 'bg-blue-500/20 text-blue-400'
: v.major === '여객선' ? 'bg-green-500/20 text-green-400'
: v.major === '화물선' ? 'bg-orange-500/20 text-orange-400'
: v.major === '유조선' ? 'bg-red-500/20 text-red-400'
: v.major === '관공선' ? 'bg-purple-500/20 text-purple-400'
: v.major === '함정' ? 'bg-cyan-500/20 text-cyan-400'
: 'bg-muted text-muted-foreground'
}`}>{v.major}</Badge>
{(() => {
const intent: BadgeIntent = v.major === '어선' ? 'info' : v.major === '여객선' ? 'success' : v.major === '화물선' ? 'high' : v.major === '유조선' ? 'critical' : v.major === '관공선' ? 'purple' : v.major === '함정' ? 'cyan' : 'muted';
return <Badge intent={intent} size="xs">{v.major}</Badge>;
})()}
</td>
<td className="px-3 py-2 text-label text-[10px]">{v.mid}</td>
<td className="px-3 py-2 text-heading font-medium">{v.name}</td>

파일 보기

@ -13,6 +13,8 @@ import {
} from 'lucide-react';
import { AreaChart as EcAreaChart, BarChart as EcBarChart, PieChart as EcPieChart } from '@lib/charts';
import { getEngineSeverityIntent, getEngineSeverityLabel } from '@shared/constants/engineSeverities';
import { getStatusIntent } from '@shared/constants/statusIntent';
import type { BadgeIntent } from '@lib/theme/variants';
import { useSettingsStore } from '@stores/settingsStore';
/*
@ -59,8 +61,7 @@ const modelColumns: DataColumn<ModelVersion>[] = [
{ key: 'status', label: '상태', width: '70px', align: 'center', sortable: true,
render: (v) => {
const s = v as string;
const c = s === '운영중' ? 'bg-green-500/20 text-green-400' : s === '테스트' ? 'bg-blue-500/20 text-blue-400' : s === '대기' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-muted text-hint';
return <Badge className={`border-0 text-[9px] ${c}`}>{s}</Badge>;
return <Badge intent={getStatusIntent(s)} size="xs">{s}</Badge>;
},
},
{ key: 'accuracy', label: 'Accuracy', width: '80px', align: 'right', sortable: true, render: (v) => <span className="text-heading font-bold">{v as number}%</span> },
@ -179,8 +180,8 @@ const gearColumns: DataColumn<GearCode>[] = [
{ key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true,
render: (v) => {
const r = v as string;
const c = r === '고위험' ? 'bg-red-500/20 text-red-400' : r === '중위험' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-green-500/20 text-green-400';
return <Badge className={`border-0 text-[9px] ${c}`}>{r}</Badge>;
const intent: BadgeIntent = r === '고위험' ? 'critical' : r === '중위험' ? 'warning' : 'success';
return <Badge intent={intent} size="xs">{r}</Badge>;
},
},
{ key: 'speed', label: '탐지 속도', width: '90px', align: 'center', render: (v) => <span className="text-label font-mono">{v as string}</span> },
@ -625,7 +626,6 @@ export function AIModelManagement() {
{/* 7대 엔진 카드 */}
<div className="space-y-2">
{DETECTION_ENGINES.map((eng) => {
const stColor = eng.status === '운영중' ? 'bg-green-500/20 text-green-400' : eng.status === '테스트' ? 'bg-blue-500/20 text-blue-400' : 'bg-muted text-muted-foreground';
return (
<Card key={eng.id} className="bg-surface-raised border-border">
<CardContent className="p-3 flex items-start gap-4">
@ -637,7 +637,7 @@ export function AIModelManagement() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-[12px] font-bold text-heading">{eng.name}</span>
<Badge className={`border-0 text-[8px] ${stColor}`}>{eng.status}</Badge>
<Badge intent={getStatusIntent(eng.status)} size="xs">{eng.status}</Badge>
</div>
<div className="text-[10px] text-muted-foreground mb-1.5">{eng.purpose}</div>
<div className="text-[10px] text-hint leading-relaxed">{eng.detail}</div>
@ -850,14 +850,14 @@ export function AIModelManagement() {
].map((api, i) => (
<tr key={i} className="border-b border-border hover:bg-surface-overlay">
<td className="py-1.5">
<Badge className={`border-0 text-[8px] font-bold ${api.method === 'GET' ? 'bg-green-500/20 text-green-400' : 'bg-blue-500/20 text-blue-400'}`}>{api.method}</Badge>
<Badge intent={api.method === 'GET' ? 'success' : 'info'} size="xs">{api.method}</Badge>
</td>
<td className="py-1.5 font-mono text-cyan-400">{api.endpoint}</td>
<td className="py-1.5 text-hint">{api.unit}</td>
<td className="py-1.5 text-label">{api.desc}</td>
<td className="py-1.5 text-muted-foreground">{api.sfr}</td>
<td className="py-1.5 text-center">
<Badge className={`border-0 text-[8px] ${api.status === '운영' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>{api.status}</Badge>
<Badge intent={getStatusIntent(api.status)} size="xs">{api.status}</Badge>
</td>
</tr>
))}

파일 보기

@ -4,6 +4,7 @@ import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { getModelStatusIntent, getQualityGateIntent, getExperimentIntent, MODEL_STATUSES, QUALITY_GATE_STATUSES, EXPERIMENT_STATUSES } from '@shared/constants/modelDeploymentStatuses';
import { getStatusIntent } from '@shared/constants/statusIntent';
import {
Cpu, Brain, Database, GitBranch, Activity, RefreshCw, Server, Shield,
FileText, Settings, Layers, Globe, Lock, BarChart3, Code, Play, Square,
@ -266,7 +267,7 @@ export function MLOpsPage() {
<td className="px-3 py-2 text-label">{d.latency}</td>
<td className="px-3 py-2 text-label">{d.falseAlarm}</td>
<td className="px-3 py-2 text-heading font-bold">{d.rps}</td>
<td className="px-3 py-2"><Badge className={`border-0 text-[9px] ${d.status === '정상' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>{d.status}</Badge></td>
<td className="px-3 py-2"><Badge intent={getStatusIntent(d.status)} size="xs">{d.status}</Badge></td>
<td className="px-3 py-2 text-hint">{d.date}</td>
</tr>
))}</tbody>
@ -390,7 +391,7 @@ export function MLOpsPage() {
<div key={j.id} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<span className="text-[10px] text-hint font-mono w-16">{j.id}</span>
<span className="text-[11px] text-heading w-24">{j.model}</span>
<Badge className={`border-0 text-[9px] w-14 text-center ${j.status === 'running' ? 'bg-blue-500/20 text-blue-400 animate-pulse' : j.status === 'done' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>{j.status}</Badge>
<Badge intent={j.status === 'running' ? 'info' : j.status === 'done' ? 'success' : 'critical'} size="xs" className="w-14 text-center">{j.status}</Badge>
<div className="flex-1 h-1.5 bg-switch-background rounded-full overflow-hidden"><div className={`h-full rounded-full ${j.status === 'done' ? 'bg-green-500' : j.status === 'fail' ? 'bg-red-500' : 'bg-blue-500'}`} style={{ width: `${j.progress}%` }} /></div>
<span className="text-[10px] text-muted-foreground w-16">{j.elapsed}</span>
</div>

파일 보기

@ -26,7 +26,7 @@ import {
import { toDateParam, formatDate, formatTime } from '@shared/utils/dateFormat';
import { getViolationColor, getViolationLabel } from '@shared/constants/violationTypes';
import { ALERT_LEVELS, type AlertLevel, getAlertLevelLabel, getAlertLevelIntent } from '@shared/constants/alertLevels';
import { getPatrolStatusClasses, getPatrolStatusLabel } from '@shared/constants/patrolStatuses';
import { getPatrolStatusIntent, getPatrolStatusLabel } from '@shared/constants/patrolStatuses';
import { getKpiUi } from '@shared/constants/kpiUiMap';
import { useSettingsStore } from '@stores/settingsStore';
@ -124,7 +124,7 @@ function PatrolStatusBadge({ status }: { status: string }) {
const { t: tc } = useTranslation('common');
const lang = useSettingsStore((s) => s.language);
return (
<Badge className={`${getPatrolStatusClasses(status)} text-[9px] border px-1.5 py-0`}>
<Badge intent={getPatrolStatusIntent(status)} size="xs">
{getPatrolStatusLabel(status, tc, lang)}
</Badge>
);

파일 보기

@ -489,30 +489,30 @@ function SelectField({ value, onChange, options }: {
}
function ResultBadge({ origin, confidence }: { origin: Origin; confidence: Confidence }) {
const colors: Record<Origin, string> = {
china: 'bg-red-500/20 text-red-400 border-red-500/40',
korea: 'bg-blue-500/20 text-blue-400 border-blue-500/40',
uncertain: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/40',
const intents: Record<Origin, import('@lib/theme/variants').BadgeIntent> = {
china: 'critical',
korea: 'info',
uncertain: 'warning',
};
const labels: Record<Origin, string> = { china: '중국어선 어구', korea: '한국어선 어구', uncertain: '판별 불가' };
const confLabels: Record<Confidence, string> = { high: '높음', medium: '보통', low: '낮음' };
return (
<div className="flex items-center gap-2">
<Badge className={`${colors[origin]} border text-sm px-3 py-1`}>{labels[origin]}</Badge>
<Badge intent={intents[origin]} size="md">{labels[origin]}</Badge>
<span className="text-[10px] text-hint">: {confLabels[confidence]}</span>
</div>
);
}
function AlertBadge({ level }: { level: string }) {
const styles: Record<string, string> = {
CRITICAL: 'bg-red-600/20 text-red-400 border-red-600/40',
HIGH: 'bg-orange-500/20 text-orange-400 border-orange-500/40',
MEDIUM: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/40',
LOW: 'bg-blue-500/20 text-blue-400 border-blue-500/40',
const intents: Record<string, import('@lib/theme/variants').BadgeIntent> = {
CRITICAL: 'critical',
HIGH: 'high',
MEDIUM: 'warning',
LOW: 'info',
};
return <Badge className={`${styles[level]} border text-[10px] px-2 py-0.5`}>{level}</Badge>;
return <Badge intent={intents[level] ?? 'muted'} size="xs">{level}</Badge>;
}
// ─── 어구 비교 레퍼런스 테이블 ──────────

파일 보기

@ -8,7 +8,7 @@ import { useEnforcementStore } from '@stores/enforcementStore';
import { formatDateTime } from '@shared/utils/dateFormat';
import { getViolationLabel, getViolationIntent } from '@shared/constants/violationTypes';
import { getEnforcementActionLabel } from '@shared/constants/enforcementActions';
import { getEnforcementResultLabel, getEnforcementResultClasses } from '@shared/constants/enforcementResults';
import { getEnforcementResultLabel, getEnforcementResultIntent } from '@shared/constants/enforcementResults';
import { useSettingsStore } from '@stores/settingsStore';
/* SFR-11: 단속 이력 관리 — 실제 백엔드 API 연동 */
@ -107,7 +107,7 @@ export function EnforcementHistory() {
render: (v) => {
const code = v as string;
return (
<Badge className={`border-0 text-[9px] ${getEnforcementResultClasses(code)}`}>
<Badge intent={getEnforcementResultIntent(code)} size="xs">
{getEnforcementResultLabel(code, tc, lang)}
</Badge>
);

파일 보기

@ -13,7 +13,7 @@ import {
import { useEventStore } from '@stores/eventStore';
import { formatDateTime } from '@shared/utils/dateFormat';
import { type AlertLevel as AlertLevelType, getAlertLevelLabel, getAlertLevelIntent } from '@shared/constants/alertLevels';
import { getEventStatusClasses, getEventStatusLabel } from '@shared/constants/eventStatuses';
import { getEventStatusIntent, getEventStatusLabel } from '@shared/constants/eventStatuses';
import { getViolationLabel, getViolationIntent } from '@shared/constants/violationTypes';
import { useSettingsStore } from '@stores/settingsStore';
@ -92,7 +92,7 @@ export function EventList() {
render: (val) => {
const s = val as string;
return (
<Badge className={`border-0 text-[9px] ${getEventStatusClasses(s)}`}>
<Badge intent={getEventStatusIntent(s)} size="xs">
{getEventStatusLabel(s, tc, lang)}
</Badge>
);

파일 보기

@ -82,14 +82,10 @@ const cols: DataColumn<AlertRow>[] = [
sortable: true,
render: (v) => {
const s = v as string;
const c =
s === 'DELIVERED'
? 'bg-green-500/20 text-green-400'
: s === 'SENT'
? 'bg-blue-500/20 text-blue-400'
: 'bg-red-500/20 text-red-400';
const intent: 'success' | 'info' | 'critical' =
s === 'DELIVERED' ? 'success' : s === 'SENT' ? 'info' : 'critical';
return (
<Badge className={`border-0 text-[9px] ${c}`}>{STATUS_LABEL[s] ?? s}</Badge>
<Badge intent={intent} size="xs">{STATUS_LABEL[s] ?? s}</Badge>
);
},
},

파일 보기

@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw, Activity, Database, Wifi } from 'lucide-react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import type { BadgeIntent } from '@lib/theme/variants';
import { fetchVesselAnalysis, type VesselAnalysisStats } from '@/services/vesselAnalysisApi';
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
@ -85,7 +86,7 @@ export function SystemStatusPanel() {
icon={<Database className="w-4 h-4" />}
title="KCG AI Backend"
status="UP"
statusColor="text-green-400"
statusIntent="success"
details={[
['포트', ':8080'],
['프로파일', 'local'],
@ -98,7 +99,7 @@ export function SystemStatusPanel() {
icon={<Wifi className="w-4 h-4" />}
title="iran 백엔드 (분석)"
status={stats ? 'CONNECTED' : 'DISCONNECTED'}
statusColor={stats ? 'text-green-400' : 'text-red-400'}
statusIntent={stats ? 'success' : 'critical'}
details={[
['선박 분석', stats ? `${stats.total.toLocaleString()}` : '-'],
['클러스터', stats ? `${stats.clusterCount}` : '-'],
@ -111,7 +112,7 @@ export function SystemStatusPanel() {
icon={<Activity className="w-4 h-4" />}
title="Prediction Service"
status={health?.status || 'UNKNOWN'}
statusColor={health?.status === 'ok' ? 'text-green-400' : 'text-yellow-400'}
statusIntent={health?.status === 'ok' ? 'success' : 'warning'}
details={[
['SNPDB', health?.snpdb === true ? 'OK' : '-'],
['KCGDB', health?.kcgdb === true ? 'OK' : '-'],
@ -134,11 +135,11 @@ export function SystemStatusPanel() {
);
}
function ServiceCard({ icon, title, status, statusColor, details }: {
function ServiceCard({ icon, title, status, statusIntent, details }: {
icon: React.ReactNode;
title: string;
status: string;
statusColor: string;
statusIntent: BadgeIntent;
details: [string, string][];
}) {
return (
@ -148,7 +149,7 @@ function ServiceCard({ icon, title, status, statusColor, details }: {
<span className="text-cyan-400">{icon}</span>
{title}
</div>
<Badge className={`bg-transparent border ${statusColor.replace('text-', 'border-')} ${statusColor} text-[9px]`}>
<Badge intent={statusIntent} size="xs">
{status}
</Badge>
</div>

파일 보기

@ -212,7 +212,7 @@ export function ParentExclusion() {
<tr key={it.id} className="border-t border-border hover:bg-surface-overlay/50">
<td className="px-3 py-2 text-hint font-mono">{it.id}</td>
<td className="px-3 py-2">
<Badge className={`border-0 text-[9px] ${it.scopeType === 'GLOBAL' ? 'bg-red-500/20 text-red-400' : 'bg-orange-500/20 text-orange-400'}`}>
<Badge intent={it.scopeType === 'GLOBAL' ? 'critical' : 'high'} size="xs">
{it.scopeType}
</Badge>
</td>

파일 보기

@ -6,6 +6,7 @@ import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { Users, Ship, Target, BarChart3, Play, CheckCircle, AlertTriangle, Layers, RefreshCw } from 'lucide-react';
import { getStatusIntent } from '@shared/constants/statusIntent';
import { usePatrolStore } from '@stores/patrolStore';
/* SFR-08: AI 경비함정 다함정 협력형 경로 최적화 서비스 */
@ -150,7 +151,7 @@ export function FleetOptimization() {
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: f.color }} />
<span className="text-[11px] font-bold text-heading">{f.name}</span>
</div>
<Badge className={`border-0 text-[8px] ${f.status === '가용' ? 'bg-green-500/20 text-green-400' : f.status === '출동중' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-muted text-muted-foreground'}`}>{f.status}</Badge>
<Badge intent={f.status === '출동중' ? 'warning' : getStatusIntent(f.status)} size="xs">{f.status}</Badge>
</div>
<div className="flex gap-3 mt-1 text-[9px] text-hint">
<span>: {f.zone}</span><span>: {f.speed}</span><span>: {f.fuel}%</span>

파일 보기

@ -125,7 +125,7 @@ export function PatrolRoute() {
className={`px-3 py-2 rounded-lg cursor-pointer transition-colors ${selectedShip === s.id ? 'bg-cyan-600/20 border border-cyan-500/30' : 'bg-surface-overlay border border-transparent hover:border-border'} ${s.status !== '가용' ? 'opacity-40 cursor-not-allowed' : ''}`}>
<div className="flex justify-between items-center">
<span className="text-[11px] font-bold text-heading">{s.name}</span>
<Badge className={`border-0 text-[8px] ${s.status === '가용' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>{s.status}</Badge>
<Badge intent={s.status === '가용' ? 'success' : 'critical'} size="xs">{s.status}</Badge>
</div>
<div className="text-[9px] text-hint mt-0.5">{s.class} · {s.speed} · {s.range}</div>
</div>

파일 보기

@ -5,6 +5,7 @@ import { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { getRiskIntent, getStatusIntent } from '@shared/constants/statusIntent';
import { Shield, AlertTriangle, Ship, Plus, Calendar, Users } from 'lucide-react';
import { BaseMap, STATIC_LAYERS, createMarkerLayer, createRadiusLayer, useMapLayers, type MapHandle } from '@lib/map';
import type { MarkerData } from '@lib/map';
@ -34,12 +35,12 @@ const cols: DataColumn<Plan>[] = [
{ key: 'id', label: 'ID', width: '70px', render: v => <span className="text-hint font-mono text-[10px]">{v as string}</span> },
{ key: 'zone', label: '단속 구역', sortable: true, render: v => <span className="text-heading font-medium">{v as string}</span> },
{ key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true,
render: v => { const n = v as number; return <Badge className={`border-0 text-[9px] ${n > 80 ? 'bg-red-500/20 text-red-400' : n > 60 ? 'bg-orange-500/20 text-orange-400' : 'bg-yellow-500/20 text-yellow-400'}`}>{n}</Badge>; } },
render: v => { const n = v as number; return <Badge intent={getRiskIntent(n)} size="xs">{n}</Badge>; } },
{ key: 'period', label: '단속 시간', width: '160px', render: v => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
{ key: 'ships', label: '참여 함정', render: v => <span className="text-cyan-400">{v as string}</span> },
{ key: 'crew', label: '인력', width: '50px', align: 'right', render: v => <span className="text-heading font-bold">{v as number || '-'}</span> },
{ key: 'status', label: '상태', width: '70px', align: 'center', sortable: true,
render: v => { const s = v as string; return <Badge className={`border-0 text-[9px] ${s === '확정' || s === 'CONFIRMED' ? 'bg-green-500/20 text-green-400' : s === '계획중' || s === 'PLANNED' ? 'bg-blue-500/20 text-blue-400' : 'bg-muted text-muted-foreground'}`}>{s}</Badge>; } },
render: v => { const s = v as string; return <Badge intent={getStatusIntent(s)} size="xs">{s}</Badge>; } },
{ key: 'alert', label: '경보', width: '80px', align: 'center',
render: v => { const a = v as string; return a === '경보 발령' || a === 'ALERT' ? <Badge intent="critical" size="sm">{a}</Badge> : <span className="text-hint text-[10px]">{a}</span>; } },
];

파일 보기

@ -2,6 +2,8 @@ import { useTranslation } from 'react-i18next';
import { Badge } from '@shared/components/ui/badge';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { getStatusIntent } from '@shared/constants/statusIntent';
import type { BadgeIntent } from '@lib/theme/variants';
import { Globe } from 'lucide-react';
/* SFR-14: 외부 서비스(예보·경보) 제공 결과 연계 */
@ -22,9 +24,13 @@ const cols: DataColumn<Service>[] = [
{ key: 'format', label: '포맷', width: '60px', align: 'center' },
{ key: 'cycle', label: '갱신주기', width: '70px' },
{ key: 'privacy', label: '정보등급', width: '70px', align: 'center',
render: v => { const p = v as string; const c = p === '비공개' ? 'bg-red-500/20 text-red-400' : p === '비식별' ? 'bg-yellow-500/20 text-yellow-400' : p === '익명화' ? 'bg-blue-500/20 text-blue-400' : 'bg-green-500/20 text-green-400'; return <Badge className={`border-0 text-[9px] ${c}`}>{p}</Badge>; } },
render: v => {
const p = v as string;
const intent: BadgeIntent = p === '비공개' ? 'critical' : p === '비식별' ? 'warning' : p === '익명화' ? 'info' : 'success';
return <Badge intent={intent} size="xs">{p}</Badge>;
} },
{ key: 'status', label: '상태', width: '60px', align: 'center', sortable: true,
render: v => { const s = v as string; const c = s === '운영' ? 'bg-green-500/20 text-green-400' : s === '테스트' ? 'bg-blue-500/20 text-blue-400' : 'bg-muted text-muted-foreground'; return <Badge className={`border-0 text-[9px] ${c}`}>{s}</Badge>; } },
render: v => { const s = v as string; return <Badge intent={getStatusIntent(s)} size="xs">{s}</Badge>; } },
{ key: 'calls', label: '호출 수', width: '70px', align: 'right', render: v => <span className="text-heading font-bold">{v as string}</span> },
];

파일 보기

@ -11,21 +11,23 @@ import { SaveButton } from '@shared/components/common/SaveButton';
import { SearchInput } from '@shared/components/common/SearchInput';
import { FileText, Plus, Upload, X, Clock, MapPin, Download } from 'lucide-react';
import type { BadgeIntent } from '@lib/theme/variants';
interface Report {
id: string;
name: string;
type: string;
status: string;
statusColor: string;
statusIntent: BadgeIntent;
date: string;
mmsiNote: string;
evidence: number;
}
const reports: Report[] = [
{ id: 'RPT-2024-0142', name: '浙江렌센號', type: 'EEZ 침범', status: 'EEZ', statusColor: 'bg-green-500', date: '2026-01-20 14:30:00', mmsiNote: 'MMSI 변조', evidence: 12 },
{ id: 'RPT-2024-0231', name: '福建海丰號', type: 'EEZ 침범', status: '확인', statusColor: 'bg-blue-500', date: '2026-01-20 14:29:00', mmsiNote: '', evidence: 8 },
{ id: 'RPT-2024-0089', name: '무명선박-A', type: '다크베셀', status: '처리중', statusColor: 'bg-yellow-500', date: '2026-01-20 14:05:00', mmsiNote: '', evidence: 6 },
{ id: 'RPT-2024-0142', name: '浙江렌센號', type: 'EEZ 침범', status: 'EEZ', statusIntent: 'success', date: '2026-01-20 14:30:00', mmsiNote: 'MMSI 변조', evidence: 12 },
{ id: 'RPT-2024-0231', name: '福建海丰號', type: 'EEZ 침범', status: '확인', statusIntent: 'info', date: '2026-01-20 14:29:00', mmsiNote: '', evidence: 8 },
{ id: 'RPT-2024-0089', name: '무명선박-A', type: '다크베셀', status: '처리중', statusIntent: 'warning', date: '2026-01-20 14:05:00', mmsiNote: '', evidence: 6 },
];
export function ReportManagement() {
@ -106,7 +108,7 @@ export function ReportManagement() {
>
<div className="flex items-center justify-between mb-1">
<span className="text-heading font-medium text-sm">{r.name}</span>
<Badge className={`${r.statusColor} text-heading text-[10px]`}>{r.status}</Badge>
<Badge intent={r.statusIntent} size="xs">{r.status}</Badge>
</div>
<div className="text-[11px] text-hint">{r.id}</div>
<div className="flex items-center gap-2 text-[11px] text-hint mt-0.5">

파일 보기

@ -6,6 +6,7 @@ import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { Map, Shield, Crosshair, AlertTriangle, Eye, Anchor, Ship, Filter, Layers, Target, Clock, MapPin, Bell, Navigation, Info } from 'lucide-react';
import { getTrainingZoneIntent, getTrainingZoneHex, getTrainingZoneMeta } from '@shared/constants/trainingZoneTypes';
import type { BadgeIntent } from '@lib/theme/variants';
/*
* (No.462)
@ -128,18 +129,18 @@ const ntmColumns: DataColumn<NtmRecord>[] = [
{ key: 'category', label: '구분', width: '70px', align: 'center', sortable: true,
render: v => {
const c = v as string;
const color = c.includes('사격') || c.includes('군사') ? 'bg-red-500/20 text-red-400'
: c.includes('기뢰') ? 'bg-orange-500/20 text-orange-400'
: c.includes('오염') ? 'bg-yellow-500/20 text-yellow-400'
: 'bg-blue-500/20 text-blue-400';
return <Badge className={`border-0 text-[9px] ${color}`}>{c}</Badge>;
const intent: BadgeIntent = c.includes('사격') || c.includes('군사') ? 'critical'
: c.includes('기뢰') ? 'high'
: c.includes('오염') ? 'warning'
: 'info';
return <Badge intent={intent} size="xs">{c}</Badge>;
},
},
{ key: 'sea', label: '해역', width: '50px', sortable: true },
{ key: 'title', label: '제목', sortable: true, render: v => <span className="text-heading font-medium">{v as string}</span> },
{ key: 'position', label: '위치', width: '120px', render: v => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
{ key: 'status', label: '상태', width: '70px', align: 'center', sortable: true,
render: v => <Badge className={`border-0 text-[9px] ${v === '발령중' ? 'bg-red-500/20 text-red-400' : 'bg-muted text-muted-foreground'}`}>{v as string}</Badge> },
render: v => <Badge intent={v === '발령중' ? 'critical' : 'muted'} size="xs">{v as string}</Badge> },
];
// 훈련구역 색상은 trainingZoneTypes 카탈로그에서 lookup
@ -153,7 +154,7 @@ const columns: DataColumn<TrainingZone>[] = [
{ key: 'lng', label: '경도', width: '110px', render: v => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
{ key: 'radius', label: '반경', width: '60px', align: 'center' },
{ key: 'status', label: '상태', width: '60px', align: 'center', sortable: true,
render: v => <Badge className={`border-0 text-[9px] ${v === '활성' ? 'bg-green-500/20 text-green-400' : 'bg-muted text-muted-foreground'}`}>{v as string}</Badge> },
render: v => <Badge intent={v === '활성' ? 'success' : 'muted'} size="xs">{v as string}</Badge> },
{ key: 'schedule', label: '운용', width: '60px', align: 'center' },
{ key: 'note', label: '비고', render: v => <span className="text-hint">{v as string}</span> },
];

파일 보기

@ -7,6 +7,8 @@
* 사용처: EnforcementHistory( ),
*/
import type { BadgeIntent } from '@lib/theme/variants';
export type EnforcementAction =
| 'CAPTURE'
| 'INSPECT'
@ -19,6 +21,7 @@ export interface EnforcementActionMeta {
code: EnforcementAction;
i18nKey: string;
fallback: { ko: string; en: string };
intent: BadgeIntent;
classes: string;
hex: string;
order: number;
@ -29,6 +32,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'CAPTURE',
i18nKey: 'enforcementAction.CAPTURE',
fallback: { ko: '나포', en: 'Capture' },
intent: 'critical',
classes: 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-400',
hex: '#ef4444',
order: 1,
@ -37,6 +41,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'INSPECT',
i18nKey: 'enforcementAction.INSPECT',
fallback: { ko: '검문', en: 'Inspect' },
intent: 'warning',
classes: 'bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-400',
hex: '#f59e0b',
order: 2,
@ -45,6 +50,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'WARN',
i18nKey: 'enforcementAction.WARN',
fallback: { ko: '경고', en: 'Warn' },
intent: 'info',
classes: 'bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-400',
hex: '#3b82f6',
order: 3,
@ -53,6 +59,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'DISPERSE',
i18nKey: 'enforcementAction.DISPERSE',
fallback: { ko: '퇴거', en: 'Disperse' },
intent: 'purple',
classes: 'bg-violet-100 text-violet-800 dark:bg-violet-500/20 dark:text-violet-400',
hex: '#8b5cf6',
order: 4,
@ -61,6 +68,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'TRACK',
i18nKey: 'enforcementAction.TRACK',
fallback: { ko: '추적', en: 'Track' },
intent: 'cyan',
classes: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-500/20 dark:text-cyan-400',
hex: '#06b6d4',
order: 5,
@ -69,6 +77,7 @@ export const ENFORCEMENT_ACTIONS: Record<EnforcementAction, EnforcementActionMet
code: 'EVIDENCE',
i18nKey: 'enforcementAction.EVIDENCE',
fallback: { ko: '증거수집', en: 'Evidence' },
intent: 'muted',
classes: 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300',
hex: '#64748b',
order: 6,
@ -79,6 +88,10 @@ export function getEnforcementActionClasses(action: string): string {
return getEnforcementActionMeta(action)?.classes ?? 'bg-muted text-muted-foreground';
}
export function getEnforcementActionIntent(action: string): BadgeIntent {
return getEnforcementActionMeta(action)?.intent ?? 'muted';
}
export function getEnforcementActionMeta(action: string): EnforcementActionMeta | undefined {
return ENFORCEMENT_ACTIONS[action as EnforcementAction];
}

파일 보기

@ -7,6 +7,8 @@
* 사용처: EnforcementHistory( ),
*/
import type { BadgeIntent } from '@lib/theme/variants';
export type EnforcementResult =
| 'PUNISHED'
| 'WARNED'
@ -18,6 +20,7 @@ export interface EnforcementResultMeta {
code: EnforcementResult;
i18nKey: string;
fallback: { ko: string; en: string };
intent: BadgeIntent;
classes: string;
order: number;
}
@ -27,6 +30,7 @@ export const ENFORCEMENT_RESULTS: Record<EnforcementResult, EnforcementResultMet
code: 'PUNISHED',
i18nKey: 'enforcementResult.PUNISHED',
fallback: { ko: '처벌', en: 'Punished' },
intent: 'critical',
classes: 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-400',
order: 1,
},
@ -34,6 +38,7 @@ export const ENFORCEMENT_RESULTS: Record<EnforcementResult, EnforcementResultMet
code: 'REFERRED',
i18nKey: 'enforcementResult.REFERRED',
fallback: { ko: '수사의뢰', en: 'Referred' },
intent: 'purple',
classes: 'bg-purple-100 text-purple-800 dark:bg-purple-500/20 dark:text-purple-400',
order: 2,
},
@ -41,6 +46,7 @@ export const ENFORCEMENT_RESULTS: Record<EnforcementResult, EnforcementResultMet
code: 'WARNED',
i18nKey: 'enforcementResult.WARNED',
fallback: { ko: '경고', en: 'Warned' },
intent: 'warning',
classes: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-500/20 dark:text-yellow-400',
order: 3,
},
@ -48,6 +54,7 @@ export const ENFORCEMENT_RESULTS: Record<EnforcementResult, EnforcementResultMet
code: 'RELEASED',
i18nKey: 'enforcementResult.RELEASED',
fallback: { ko: '훈방', en: 'Released' },
intent: 'success',
classes: 'bg-green-100 text-green-800 dark:bg-green-500/20 dark:text-green-400',
order: 4,
},
@ -55,11 +62,16 @@ export const ENFORCEMENT_RESULTS: Record<EnforcementResult, EnforcementResultMet
code: 'FALSE_POSITIVE',
i18nKey: 'enforcementResult.FALSE_POSITIVE',
fallback: { ko: '오탐(정상)', en: 'False Positive' },
intent: 'muted',
classes: 'bg-muted text-muted-foreground',
order: 5,
},
};
export function getEnforcementResultIntent(result: string): BadgeIntent {
return getEnforcementResultMeta(result)?.intent ?? 'muted';
}
export function getEnforcementResultMeta(result: string): EnforcementResultMeta | undefined {
return ENFORCEMENT_RESULTS[result as EnforcementResult];
}

파일 보기

@ -7,6 +7,8 @@
* 사용처: EventList( ), ,
*/
import type { BadgeIntent } from '@lib/theme/variants';
export type EventStatus =
| 'NEW'
| 'ACK'
@ -18,7 +20,8 @@ export interface EventStatusMeta {
code: EventStatus;
i18nKey: string;
fallback: { ko: string; en: string };
classes: string; // bg + text 묶음
intent: BadgeIntent;
classes: string; // bg + text 묶음 (legacy - use intent 권장)
order: number;
}
@ -27,6 +30,7 @@ export const EVENT_STATUSES: Record<EventStatus, EventStatusMeta> = {
code: 'NEW',
i18nKey: 'eventStatus.NEW',
fallback: { ko: '신규', en: 'New' },
intent: 'critical',
classes: 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-400',
order: 1,
},
@ -34,6 +38,7 @@ export const EVENT_STATUSES: Record<EventStatus, EventStatusMeta> = {
code: 'ACK',
i18nKey: 'eventStatus.ACK',
fallback: { ko: '확인', en: 'Acknowledged' },
intent: 'high',
classes: 'bg-orange-100 text-orange-800 dark:bg-orange-500/20 dark:text-orange-400',
order: 2,
},
@ -41,6 +46,7 @@ export const EVENT_STATUSES: Record<EventStatus, EventStatusMeta> = {
code: 'IN_PROGRESS',
i18nKey: 'eventStatus.IN_PROGRESS',
fallback: { ko: '처리중', en: 'In Progress' },
intent: 'info',
classes: 'bg-blue-100 text-blue-800 dark:bg-blue-500/20 dark:text-blue-400',
order: 3,
},
@ -48,6 +54,7 @@ export const EVENT_STATUSES: Record<EventStatus, EventStatusMeta> = {
code: 'RESOLVED',
i18nKey: 'eventStatus.RESOLVED',
fallback: { ko: '완료', en: 'Resolved' },
intent: 'success',
classes: 'bg-green-100 text-green-800 dark:bg-green-500/20 dark:text-green-400',
order: 4,
},
@ -55,11 +62,16 @@ export const EVENT_STATUSES: Record<EventStatus, EventStatusMeta> = {
code: 'FALSE_POSITIVE',
i18nKey: 'eventStatus.FALSE_POSITIVE',
fallback: { ko: '오탐', en: 'False Positive' },
intent: 'muted',
classes: 'bg-muted text-muted-foreground',
order: 5,
},
};
export function getEventStatusIntent(status: string): BadgeIntent {
return getEventStatusMeta(status)?.intent ?? 'muted';
}
export function getEventStatusMeta(status: string): EventStatusMeta | undefined {
return EVENT_STATUSES[status as EventStatus];
}

파일 보기

@ -29,3 +29,4 @@ export * from './vesselAnalysisStatuses';
export * from './connectionStatuses';
export * from './trainingZoneTypes';
export * from './kpiUiMap';
export * from './statusIntent';

파일 보기

@ -7,6 +7,8 @@
* 사용처: Dashboard PatrolStatusBadge, ShipAgent
*/
import type { BadgeIntent } from '@lib/theme/variants';
export type PatrolStatus =
| 'AVAILABLE'
| 'ON_PATROL'
@ -20,6 +22,7 @@ export interface PatrolStatusMeta {
code: PatrolStatus;
i18nKey: string;
fallback: { ko: string; en: string };
intent: BadgeIntent;
classes: string;
order: number;
}
@ -29,6 +32,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'IN_PURSUIT',
i18nKey: 'patrolStatus.IN_PURSUIT',
fallback: { ko: '추적중', en: 'In Pursuit' },
intent: 'critical',
classes:
'bg-red-100 text-red-800 border-red-300 dark:bg-red-500/20 dark:text-red-400 dark:border-red-500/30',
order: 1,
@ -37,6 +41,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'INSPECTING',
i18nKey: 'patrolStatus.INSPECTING',
fallback: { ko: '검문중', en: 'Inspecting' },
intent: 'high',
classes:
'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-500/20 dark:text-orange-400 dark:border-orange-500/30',
order: 2,
@ -45,6 +50,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'ON_PATROL',
i18nKey: 'patrolStatus.ON_PATROL',
fallback: { ko: '초계중', en: 'On Patrol' },
intent: 'info',
classes:
'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-500/20 dark:text-blue-400 dark:border-blue-500/30',
order: 3,
@ -53,6 +59,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'RETURNING',
i18nKey: 'patrolStatus.RETURNING',
fallback: { ko: '귀항중', en: 'Returning' },
intent: 'purple',
classes:
'bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-500/20 dark:text-purple-400 dark:border-purple-500/30',
order: 4,
@ -61,6 +68,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'AVAILABLE',
i18nKey: 'patrolStatus.AVAILABLE',
fallback: { ko: '가용', en: 'Available' },
intent: 'success',
classes:
'bg-green-100 text-green-800 border-green-300 dark:bg-green-500/20 dark:text-green-400 dark:border-green-500/30',
order: 5,
@ -69,6 +77,7 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'STANDBY',
i18nKey: 'patrolStatus.STANDBY',
fallback: { ko: '대기', en: 'Standby' },
intent: 'muted',
classes:
'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-500/20 dark:text-slate-400 dark:border-slate-500/30',
order: 6,
@ -77,12 +86,17 @@ export const PATROL_STATUSES: Record<PatrolStatus, PatrolStatusMeta> = {
code: 'MAINTENANCE',
i18nKey: 'patrolStatus.MAINTENANCE',
fallback: { ko: '정비중', en: 'Maintenance' },
intent: 'warning',
classes:
'bg-yellow-100 text-yellow-800 border-yellow-300 dark:bg-yellow-500/20 dark:text-yellow-400 dark:border-yellow-500/30',
order: 7,
},
};
export function getPatrolStatusIntent(status: string): BadgeIntent {
return getPatrolStatusMeta(status)?.intent ?? 'muted';
}
/** 한글 라벨도 키로 받아주는 호환성 매핑 (mock 데이터에서 한글 사용 중) */
const LEGACY_KO_LABELS: Record<string, PatrolStatus> = {
'추적 중': 'IN_PURSUIT',

파일 보기

@ -0,0 +1,163 @@
/**
* BadgeIntent
*
* ad-hoc (/ mock )
* intent에 . .
*
* :
* -
* - /mock
*
* :
* <Badge intent={getStatusIntent(s)} size="xs">{s}</Badge>
*/
import type { BadgeIntent } from '@lib/theme/variants';
const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
// 정상/긍정
'정상': 'success',
'운영': 'success',
'운영중': 'success',
'활성': 'success',
'완료': 'success',
'확정': 'success',
'가용': 'success',
'승인': 'success',
'성공': 'success',
'통과': 'success',
'배포': 'success',
active: 'success',
running: 'info',
online: 'success',
healthy: 'success',
success: 'success',
ok: 'success',
passed: 'success',
deployed: 'success',
confirmed: 'success',
CONFIRMED: 'success',
ACTIVE: 'success',
PASSED: 'success',
RUNNING: 'info',
DEPLOYED: 'success',
// 정보/대기
'대기': 'info',
'계획': 'info',
'계획중': 'info',
'진행': 'info',
'진행중': 'info',
'처리': 'info',
'처리중': 'info',
'생성': 'info',
'예약': 'info',
'예정': 'info',
'테스트': 'info',
pending: 'info',
PENDING: 'info',
planning: 'info',
PLANNING: 'info',
PLANNED: 'info',
scheduled: 'info',
testing: 'info',
TESTING: 'info',
STAGING: 'info',
CANARY: 'info',
// 주의
'주의': 'warning',
'경고': 'warning',
'검토': 'warning',
'검토필요': 'warning',
'수정': 'warning',
'변경': 'warning',
'점검': 'warning',
warning: 'warning',
WARNING: 'warning',
review: 'warning',
reviewing: 'warning',
maintenance: 'warning',
// 심각/에러
'긴급': 'critical',
'오류': 'critical',
'실패': 'critical',
'에러': 'critical',
'차단': 'critical',
'정지': 'critical',
'거부': 'critical',
'폐기': 'critical',
'만료': 'critical',
critical: 'critical',
CRITICAL: 'critical',
error: 'critical',
ERROR: 'critical',
failed: 'critical',
FAILED: 'critical',
blocked: 'critical',
rejected: 'critical',
REJECTED: 'critical',
expired: 'critical',
EXPIRED: 'critical',
// 높음
'높음': 'high',
'높은': 'high',
high: 'high',
HIGH: 'high',
// 보라/특수
'분석': 'purple',
'학습': 'purple',
'추론': 'purple',
'배포중': 'purple',
analyzing: 'purple',
training: 'purple',
// 청록/모니터링
'모니터': 'cyan',
'모니터링': 'cyan',
'감시': 'cyan',
'추적': 'cyan',
'추적중': 'cyan',
monitoring: 'cyan',
tracking: 'cyan',
// 비활성/중립
'비활성': 'muted',
'미배포': 'muted',
'없음': 'muted',
'-': 'muted',
'기타': 'muted',
'오탐': 'muted',
inactive: 'muted',
INACTIVE: 'muted',
disabled: 'muted',
DISABLED: 'muted',
none: 'muted',
unknown: 'muted',
UNKNOWN: 'muted',
ARCHIVED: 'muted',
DEV: 'muted',
};
/**
* BadgeIntent로 .
* 'muted' .
*/
export function getStatusIntent(status: string | null | undefined): BadgeIntent {
if (!status) return 'muted';
return STATUS_INTENT_MAP[status] ?? STATUS_INTENT_MAP[status.toLowerCase()] ?? 'muted';
}
/**
* (0-100) intent로 .
* 80 critical, 60 high, 40 warning, info
*/
export function getRiskIntent(score: number): BadgeIntent {
if (score >= 80) return 'critical';
if (score >= 60) return 'high';
if (score >= 40) return 'warning';
return 'info';
}