From 2483174081272d465bdb8db0dbacf7a50f2ca861 Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 8 Apr 2026 12:28:23 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20Badge=20className=20?= =?UTF-8?q?=EC=9C=84=EB=B0=98=2037=EA=B1=B4=20=EC=A0=84=EC=88=98=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 사용처에 일관되게 반영됨. --- frontend/src/features/admin/AccessControl.tsx | 3 +- frontend/src/features/admin/AuditLogs.tsx | 2 +- frontend/src/features/admin/DataHub.tsx | 30 ++-- .../src/features/admin/NoticeManagement.tsx | 11 +- frontend/src/features/admin/SystemConfig.tsx | 44 ++--- .../ai-operations/AIModelManagement.tsx | 16 +- .../src/features/ai-operations/MLOpsPage.tsx | 5 +- frontend/src/features/dashboard/Dashboard.tsx | 4 +- .../features/detection/GearIdentification.tsx | 22 +-- .../enforcement/EnforcementHistory.tsx | 4 +- .../src/features/enforcement/EventList.tsx | 4 +- frontend/src/features/field-ops/AIAlert.tsx | 10 +- .../features/monitoring/SystemStatusPanel.tsx | 13 +- .../parent-inference/ParentExclusion.tsx | 2 +- .../src/features/patrol/FleetOptimization.tsx | 3 +- frontend/src/features/patrol/PatrolRoute.tsx | 2 +- .../risk-assessment/EnforcementPlan.tsx | 5 +- .../features/statistics/ExternalService.tsx | 10 +- .../features/statistics/ReportManagement.tsx | 12 +- .../src/features/surveillance/MapControl.tsx | 15 +- .../shared/constants/enforcementActions.ts | 13 ++ .../shared/constants/enforcementResults.ts | 12 ++ .../src/shared/constants/eventStatuses.ts | 14 +- frontend/src/shared/constants/index.ts | 1 + .../src/shared/constants/patrolStatuses.ts | 14 ++ frontend/src/shared/constants/statusIntent.ts | 163 ++++++++++++++++++ 26 files changed, 321 insertions(+), 113 deletions(-) create mode 100644 frontend/src/shared/constants/statusIntent.ts diff --git a/frontend/src/features/admin/AccessControl.tsx b/frontend/src/features/admin/AccessControl.tsx index 08fda37..67e5013 100644 --- a/frontend/src/features/admin/AccessControl.tsx +++ b/frontend/src/features/admin/AccessControl.tsx @@ -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 {r || '-'}; + return {r || '-'}; }, }, { key: 'failReason', label: '실패 사유', diff --git a/frontend/src/features/admin/AuditLogs.tsx b/frontend/src/features/admin/AuditLogs.tsx index a0d1d59..08d9625 100644 --- a/frontend/src/features/admin/AuditLogs.tsx +++ b/frontend/src/features/admin/AuditLogs.tsx @@ -103,7 +103,7 @@ export function AuditLogs() { {it.actionCd} {it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''} - + {it.result || '-'} diff --git a/frontend/src/features/admin/DataHub.tsx b/frontend/src/features/admin/DataHub.tsx index c6a8220..df29c54 100644 --- a/frontend/src/features/admin/DataHub.tsx +++ b/frontend/src/features/admin/DataHub.tsx @@ -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[] = [ const on = v === 'ON'; return (
- + {v as string} {row.lastUpdate && ( @@ -208,18 +217,14 @@ const collectColumns: DataColumn[] = [ { 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 {t}; + const intent: BadgeIntent = t === 'SQL' ? 'info' : t === 'FILE' ? 'success' : 'purple'; + return {t}; }, }, { key: 'serverName', label: '서버명', width: '120px', render: (v) => {v as string} }, { key: 'serverIp', label: 'IP', width: '120px', render: (v) => {v as string} }, { 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 {s}; - }, + render: (v) => {v as string}, }, { key: 'schedule', label: '스케줄', width: '80px' }, { key: 'lastRun', label: '최종 수행', width: '140px', sortable: true, render: (v) => {v as string} }, @@ -277,11 +282,7 @@ const loadColumns: DataColumn[] = [ { key: 'targetTable', label: '대상 테이블', width: '140px', render: (v) => {v as string} }, { 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 {s}; - }, + render: (v) => {v as string}, }, { key: 'schedule', label: '스케줄', width: '80px' }, { key: 'lastRun', label: '최종 적재', width: '140px', sortable: true, render: (v) => {v as string} }, @@ -626,7 +627,6 @@ export function DataHub() { {/* 연계서버 카드 그리드 */}
{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 ( @@ -635,7 +635,7 @@ export function DataHub() {
{agent.name}
{agent.id} · {agent.role}에이전트
- {agent.status} + {agent.status}
Hostname{agent.hostname}
diff --git a/frontend/src/features/admin/NoticeManagement.tsx b/frontend/src/features/admin/NoticeManagement.tsx index d3c8e7a..7608264 100644 --- a/frontend/src/features/admin/NoticeManagement.tsx +++ b/frontend/src/features/admin/NoticeManagement.tsx @@ -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 ( - {status.label} + {status.label} diff --git a/frontend/src/features/admin/SystemConfig.tsx b/frontend/src/features/admin/SystemConfig.tsx index 1453e3c..b97f823 100644 --- a/frontend/src/features/admin/SystemConfig.tsx +++ b/frontend/src/features/admin/SystemConfig.tsx @@ -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() { {a.code} - {a.major} + {(() => { + const intent: BadgeIntent = a.major === '서해' ? 'info' : a.major === '남해' ? 'success' : a.major === '동해' ? 'purple' : a.major === '제주' ? 'high' : 'cyan'; + return {a.major}; + })()} {a.mid} {a.name} @@ -359,25 +357,16 @@ export function SystemConfig() { {f.code} - {f.major} + {(() => { + const intent: BadgeIntent = f.major === '근해어업' ? 'info' : f.major === '연안어업' ? 'success' : f.major === '양식어업' ? 'cyan' : f.major === '원양어업' ? 'purple' : f.major === '구획어업' ? 'high' : f.major === '마을어업' ? 'warning' : 'muted'; + return {f.major}; + })()} {f.mid} {f.name} {f.target} - {f.permit} + {f.permit} {f.law} @@ -410,15 +399,10 @@ export function SystemConfig() { {v.code} - {v.major} + {(() => { + const intent: BadgeIntent = v.major === '어선' ? 'info' : v.major === '여객선' ? 'success' : v.major === '화물선' ? 'high' : v.major === '유조선' ? 'critical' : v.major === '관공선' ? 'purple' : v.major === '함정' ? 'cyan' : 'muted'; + return {v.major}; + })()} {v.mid} {v.name} diff --git a/frontend/src/features/ai-operations/AIModelManagement.tsx b/frontend/src/features/ai-operations/AIModelManagement.tsx index 8c20d1e..e7b19df 100644 --- a/frontend/src/features/ai-operations/AIModelManagement.tsx +++ b/frontend/src/features/ai-operations/AIModelManagement.tsx @@ -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[] = [ { 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 {s}; + return {s}; }, }, { key: 'accuracy', label: 'Accuracy', width: '80px', align: 'right', sortable: true, render: (v) => {v as number}% }, @@ -179,8 +180,8 @@ const gearColumns: DataColumn[] = [ { 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 {r}; + const intent: BadgeIntent = r === '고위험' ? 'critical' : r === '중위험' ? 'warning' : 'success'; + return {r}; }, }, { key: 'speed', label: '탐지 속도', width: '90px', align: 'center', render: (v) => {v as string} }, @@ -625,7 +626,6 @@ export function AIModelManagement() { {/* 7대 엔진 카드 */}
{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 ( @@ -637,7 +637,7 @@ export function AIModelManagement() {
{eng.name} - {eng.status} + {eng.status}
{eng.purpose}
{eng.detail}
@@ -850,14 +850,14 @@ export function AIModelManagement() { ].map((api, i) => ( - {api.method} + {api.method} {api.endpoint} {api.unit} {api.desc} {api.sfr} - {api.status} + {api.status} ))} diff --git a/frontend/src/features/ai-operations/MLOpsPage.tsx b/frontend/src/features/ai-operations/MLOpsPage.tsx index 8d8661a..e82fa1a 100644 --- a/frontend/src/features/ai-operations/MLOpsPage.tsx +++ b/frontend/src/features/ai-operations/MLOpsPage.tsx @@ -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() { {d.latency} {d.falseAlarm} {d.rps} - {d.status} + {d.status} {d.date} ))} @@ -390,7 +391,7 @@ export function MLOpsPage() {
{j.id} {j.model} - {j.status} + {j.status}
{j.elapsed}
diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx index 1423dfb..98f3cd7 100644 --- a/frontend/src/features/dashboard/Dashboard.tsx +++ b/frontend/src/features/dashboard/Dashboard.tsx @@ -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 ( - + {getPatrolStatusLabel(status, tc, lang)} ); diff --git a/frontend/src/features/detection/GearIdentification.tsx b/frontend/src/features/detection/GearIdentification.tsx index 828b6b3..9900a4f 100644 --- a/frontend/src/features/detection/GearIdentification.tsx +++ b/frontend/src/features/detection/GearIdentification.tsx @@ -489,30 +489,30 @@ function SelectField({ value, onChange, options }: { } function ResultBadge({ origin, confidence }: { origin: Origin; confidence: Confidence }) { - const colors: Record = { - 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 = { + china: 'critical', + korea: 'info', + uncertain: 'warning', }; const labels: Record = { china: '중국어선 어구', korea: '한국어선 어구', uncertain: '판별 불가' }; const confLabels: Record = { high: '높음', medium: '보통', low: '낮음' }; return (
- {labels[origin]} + {labels[origin]} 신뢰도: {confLabels[confidence]}
); } function AlertBadge({ level }: { level: string }) { - const styles: Record = { - 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 = { + CRITICAL: 'critical', + HIGH: 'high', + MEDIUM: 'warning', + LOW: 'info', }; - return {level}; + return {level}; } // ─── 어구 비교 레퍼런스 테이블 ────────── diff --git a/frontend/src/features/enforcement/EnforcementHistory.tsx b/frontend/src/features/enforcement/EnforcementHistory.tsx index 418fe02..79838e9 100644 --- a/frontend/src/features/enforcement/EnforcementHistory.tsx +++ b/frontend/src/features/enforcement/EnforcementHistory.tsx @@ -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 ( - + {getEnforcementResultLabel(code, tc, lang)} ); diff --git a/frontend/src/features/enforcement/EventList.tsx b/frontend/src/features/enforcement/EventList.tsx index e3c2b88..45e9dde 100644 --- a/frontend/src/features/enforcement/EventList.tsx +++ b/frontend/src/features/enforcement/EventList.tsx @@ -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 ( - + {getEventStatusLabel(s, tc, lang)} ); diff --git a/frontend/src/features/field-ops/AIAlert.tsx b/frontend/src/features/field-ops/AIAlert.tsx index ef78be7..4c12489 100644 --- a/frontend/src/features/field-ops/AIAlert.tsx +++ b/frontend/src/features/field-ops/AIAlert.tsx @@ -82,14 +82,10 @@ const cols: DataColumn[] = [ 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 ( - {STATUS_LABEL[s] ?? s} + {STATUS_LABEL[s] ?? s} ); }, }, diff --git a/frontend/src/features/monitoring/SystemStatusPanel.tsx b/frontend/src/features/monitoring/SystemStatusPanel.tsx index 4cf1d69..99bc29e 100644 --- a/frontend/src/features/monitoring/SystemStatusPanel.tsx +++ b/frontend/src/features/monitoring/SystemStatusPanel.tsx @@ -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={} title="KCG AI Backend" status="UP" - statusColor="text-green-400" + statusIntent="success" details={[ ['포트', ':8080'], ['프로파일', 'local'], @@ -98,7 +99,7 @@ export function SystemStatusPanel() { icon={} 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={} 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 }: { {icon} {title}
- + {status}
diff --git a/frontend/src/features/parent-inference/ParentExclusion.tsx b/frontend/src/features/parent-inference/ParentExclusion.tsx index 3454877..ee160b5 100644 --- a/frontend/src/features/parent-inference/ParentExclusion.tsx +++ b/frontend/src/features/parent-inference/ParentExclusion.tsx @@ -212,7 +212,7 @@ export function ParentExclusion() { {it.id} - + {it.scopeType} diff --git a/frontend/src/features/patrol/FleetOptimization.tsx b/frontend/src/features/patrol/FleetOptimization.tsx index b638423..c66e7d7 100644 --- a/frontend/src/features/patrol/FleetOptimization.tsx +++ b/frontend/src/features/patrol/FleetOptimization.tsx @@ -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() {
{f.name}
- {f.status} + {f.status}
구역: {f.zone}속력: {f.speed}연료: {f.fuel}% diff --git a/frontend/src/features/patrol/PatrolRoute.tsx b/frontend/src/features/patrol/PatrolRoute.tsx index b77a5bd..d098708 100644 --- a/frontend/src/features/patrol/PatrolRoute.tsx +++ b/frontend/src/features/patrol/PatrolRoute.tsx @@ -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' : ''}`}>
{s.name} - {s.status} + {s.status}
{s.class} · {s.speed} · {s.range}
diff --git a/frontend/src/features/risk-assessment/EnforcementPlan.tsx b/frontend/src/features/risk-assessment/EnforcementPlan.tsx index 8b39e5f..3d0979b 100644 --- a/frontend/src/features/risk-assessment/EnforcementPlan.tsx +++ b/frontend/src/features/risk-assessment/EnforcementPlan.tsx @@ -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[] = [ { key: 'id', label: 'ID', width: '70px', render: v => {v as string} }, { key: 'zone', label: '단속 구역', sortable: true, render: v => {v as string} }, { key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true, - render: v => { const n = v as number; return 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}점; } }, + render: v => { const n = v as number; return {n}점; } }, { key: 'period', label: '단속 시간', width: '160px', render: v => {v as string} }, { key: 'ships', label: '참여 함정', render: v => {v as string} }, { key: 'crew', label: '인력', width: '50px', align: 'right', render: v => {v as number || '-'} }, { key: 'status', label: '상태', width: '70px', align: 'center', sortable: true, - render: v => { const s = v as string; return {s}; } }, + render: v => { const s = v as string; return {s}; } }, { key: 'alert', label: '경보', width: '80px', align: 'center', render: v => { const a = v as string; return a === '경보 발령' || a === 'ALERT' ? {a} : {a}; } }, ]; diff --git a/frontend/src/features/statistics/ExternalService.tsx b/frontend/src/features/statistics/ExternalService.tsx index c53401f..776ad2c 100644 --- a/frontend/src/features/statistics/ExternalService.tsx +++ b/frontend/src/features/statistics/ExternalService.tsx @@ -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[] = [ { 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 {p}; } }, + render: v => { + const p = v as string; + const intent: BadgeIntent = p === '비공개' ? 'critical' : p === '비식별' ? 'warning' : p === '익명화' ? 'info' : 'success'; + return {p}; + } }, { 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 {s}; } }, + render: v => { const s = v as string; return {s}; } }, { key: 'calls', label: '호출 수', width: '70px', align: 'right', render: v => {v as string} }, ]; diff --git a/frontend/src/features/statistics/ReportManagement.tsx b/frontend/src/features/statistics/ReportManagement.tsx index 5934b1f..b1ea820 100644 --- a/frontend/src/features/statistics/ReportManagement.tsx +++ b/frontend/src/features/statistics/ReportManagement.tsx @@ -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() { >
{r.name} - {r.status} + {r.status}
{r.id}
diff --git a/frontend/src/features/surveillance/MapControl.tsx b/frontend/src/features/surveillance/MapControl.tsx index e3f9ec6..90d07e9 100644 --- a/frontend/src/features/surveillance/MapControl.tsx +++ b/frontend/src/features/surveillance/MapControl.tsx @@ -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[] = [ { 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 {c}; + const intent: BadgeIntent = c.includes('사격') || c.includes('군사') ? 'critical' + : c.includes('기뢰') ? 'high' + : c.includes('오염') ? 'warning' + : 'info'; + return {c}; }, }, { key: 'sea', label: '해역', width: '50px', sortable: true }, { key: 'title', label: '제목', sortable: true, render: v => {v as string} }, { key: 'position', label: '위치', width: '120px', render: v => {v as string} }, { key: 'status', label: '상태', width: '70px', align: 'center', sortable: true, - render: v => {v as string} }, + render: v => {v as string} }, ]; // 훈련구역 색상은 trainingZoneTypes 카탈로그에서 lookup @@ -153,7 +154,7 @@ const columns: DataColumn[] = [ { key: 'lng', label: '경도', width: '110px', render: v => {v as string} }, { key: 'radius', label: '반경', width: '60px', align: 'center' }, { key: 'status', label: '상태', width: '60px', align: 'center', sortable: true, - render: v => {v as string} }, + render: v => {v as string} }, { key: 'schedule', label: '운용', width: '60px', align: 'center' }, { key: 'note', label: '비고', render: v => {v as string} }, ]; diff --git a/frontend/src/shared/constants/enforcementActions.ts b/frontend/src/shared/constants/enforcementActions.ts index e9249af..f791e67 100644 --- a/frontend/src/shared/constants/enforcementActions.ts +++ b/frontend/src/shared/constants/enforcementActions.ts @@ -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 = { 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 = { 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 = { 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 = { 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 = { 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]; } diff --git a/frontend/src/shared/constants/index.ts b/frontend/src/shared/constants/index.ts index 6d395b6..112a84f 100644 --- a/frontend/src/shared/constants/index.ts +++ b/frontend/src/shared/constants/index.ts @@ -29,3 +29,4 @@ export * from './vesselAnalysisStatuses'; export * from './connectionStatuses'; export * from './trainingZoneTypes'; export * from './kpiUiMap'; +export * from './statusIntent'; diff --git a/frontend/src/shared/constants/patrolStatuses.ts b/frontend/src/shared/constants/patrolStatuses.ts index 721cd9d..d1bd898 100644 --- a/frontend/src/shared/constants/patrolStatuses.ts +++ b/frontend/src/shared/constants/patrolStatuses.ts @@ -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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { '추적 중': 'IN_PURSUIT', diff --git a/frontend/src/shared/constants/statusIntent.ts b/frontend/src/shared/constants/statusIntent.ts new file mode 100644 index 0000000..0ec87f6 --- /dev/null +++ b/frontend/src/shared/constants/statusIntent.ts @@ -0,0 +1,163 @@ +/** + * 일반 상태 문자열 → BadgeIntent 매핑 유틸 + * + * 정식 카탈로그에 없는 ad-hoc 상태 문자열(한글/영문 섞여 있는 mock 데이터 등)을 + * 임시로 intent에 매핑. 프로젝트 전역에서 재사용 가능. + * + * 원칙: + * - 가능하면 전용 카탈로그를 만들어 사용하는 것이 우선 + * - 이 유틸은 정형화되지 않은 데모/mock 데이터 대응 임시 매핑용 + * + * 사용: + * {s} + */ + +import type { BadgeIntent } from '@lib/theme/variants'; + +const STATUS_INTENT_MAP: Record = { + // 정상/긍정 + '정상': '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'; +}