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:
부모
85cb6b40a2
커밋
2483174081
@ -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',
|
||||
|
||||
163
frontend/src/shared/constants/statusIntent.ts
Normal file
163
frontend/src/shared/constants/statusIntent.ts
Normal file
@ -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';
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user