diff --git a/frontend/src/features/admin/AccessControl.tsx b/frontend/src/features/admin/AccessControl.tsx index c9fd72e..2028d31 100644 --- a/frontend/src/features/admin/AccessControl.tsx +++ b/frontend/src/features/admin/AccessControl.tsx @@ -146,15 +146,23 @@ export function AccessControl() { { key: 'userId', label: '관리', width: '90px', align: 'center', sortable: false, render: (_v, row) => (
- + +
), diff --git a/frontend/src/features/admin/DataModelVerification.tsx b/frontend/src/features/admin/DataModelVerification.tsx index a627667..7b13047 100644 --- a/frontend/src/features/admin/DataModelVerification.tsx +++ b/frontend/src/features/admin/DataModelVerification.tsx @@ -124,7 +124,7 @@ export function DataModelVerification() {
- + 검증 절차 (4단계)
@@ -168,14 +168,14 @@ export function DataModelVerification() { )}
- - {s.phase} + + {s.phase}
-
{s.responsible}
+
{s.responsible}
    {s.actions.map(a => (
  • - + {a}
  • ))} @@ -190,7 +190,7 @@ export function DataModelVerification() {
    - + 검증 참여자
    @@ -207,7 +207,7 @@ export function DataModelVerification() {
    - + 데이터 주제영역 ({SUBJECT_AREAS.reduce((s, a) => s + a.count, 0)}개 테이블)
    @@ -229,7 +229,7 @@ export function DataModelVerification() {
    - + 논리 데이터 모델 검증 기준 및 결과 {LOGICAL_CHECKS.filter(c => c.status === '통과').length}/{LOGICAL_CHECKS.length} 통과
    @@ -249,7 +249,7 @@ export function DataModelVerification() { {c.category} {c.item} {c.desc} - {c.result} + {c.result} {c.status} ))} @@ -279,7 +279,7 @@ export function DataModelVerification() { {tab === 'physical' && (
    - + 물리 데이터 모델 검증 기준 및 결과 {PHYSICAL_CHECKS.filter(c => c.status === '통과').length}/{PHYSICAL_CHECKS.length} 통과 {PHYSICAL_CHECKS.some(c => c.status === '주의') && ( @@ -302,7 +302,7 @@ export function DataModelVerification() { {c.category} {c.item} {c.desc} - {c.result} + {c.result} {c.status} ))} @@ -315,7 +315,7 @@ export function DataModelVerification() { {tab === 'duplication' && (
    - + 중복 테이블·컬럼 및 데이터 정합성 점검 {DUPLICATION_CHECKS.filter(c => c.status === '통과').length}/{DUPLICATION_CHECKS.length} 통과
    @@ -335,7 +335,7 @@ export function DataModelVerification() { {c.target} {c.desc} {c.scope} - {c.result} + {c.result} {c.status} ))} @@ -348,7 +348,7 @@ export function DataModelVerification() { {tab === 'history' && (
    - + 검증 결과 이력 {VERIFICATION_HISTORY.length}건
    @@ -372,7 +372,7 @@ export function DataModelVerification() { {h.phase} {h.reviewer} {h.target} - {h.issues > 0 ? {h.issues}건 : 0건} + {h.issues > 0 ? {h.issues}건 : 0건} {h.result} ))} diff --git a/frontend/src/features/admin/DataRetentionPolicy.tsx b/frontend/src/features/admin/DataRetentionPolicy.tsx index e0df961..e44dfdf 100644 --- a/frontend/src/features/admin/DataRetentionPolicy.tsx +++ b/frontend/src/features/admin/DataRetentionPolicy.tsx @@ -108,7 +108,7 @@ export function DataRetentionPolicy() {
    - + 전체 보관 구조 (4-Tier)
    {STORAGE_ARCHITECTURE.map(s => (
    - + {s.tier}

    {s.desc}

    @@ -184,7 +184,7 @@ export function DataRetentionPolicy() { ['CCTV 30일 보관 준수', '미채택 영상 30일 초과 1건', '주의'], ].map(([k, v, s]) => (
    - {s === '완료' || s === '정상' ? : } + {s === '완료' || s === '정상' ? : } {k} {v}
    @@ -213,7 +213,7 @@ export function DataRetentionPolicy() {
    - + 데이터 유형별 보관기간 기준표 {RETENTION_TABLE.length}종 관리
    @@ -235,7 +235,7 @@ export function DataRetentionPolicy() { {r.type} {r.category} {r.basis} - {r.period} + {r.period} {r.format} {r.volume} {r.status} @@ -273,7 +273,7 @@ export function DataRetentionPolicy() { {/* 파기 승인 워크플로우 */}
    - + 파기 승인 절차 (4단계)
    @@ -284,14 +284,14 @@ export function DataRetentionPolicy() { )}
    - - {s.phase} + + {s.phase}
    -
    {s.responsible}
    +
    {s.responsible}
      {s.actions.map(a => (
    • - + {a}
    • ))} @@ -305,7 +305,7 @@ export function DataRetentionPolicy() { {/* 파기 방식 */}
      - + 파기 방식 정의
      @@ -326,7 +326,7 @@ export function DataRetentionPolicy() { - + ))} @@ -341,7 +341,7 @@ export function DataRetentionPolicy() {
      - + 보존 연장 예외 현황 {EXCEPTIONS.filter(e => e.status === '연장 중').length}건 연장 중
      @@ -364,7 +364,7 @@ export function DataRetentionPolicy() {
      - + @@ -375,13 +375,13 @@ export function DataRetentionPolicy() {
      - + 보존 연장 사유 유형
      {EXCEPTION_RULES.map(r => (
      - +
      {r.rule}
      {r.desc}
      @@ -398,7 +398,7 @@ export function DataRetentionPolicy() { {tab === 'audit' && (
      - + 파기 감사 대장 {DISPOSAL_AUDIT_LOG.length}건
      @@ -424,7 +424,7 @@ export function DataRetentionPolicy() {
      - + diff --git a/frontend/src/features/admin/PerformanceMonitoring.tsx b/frontend/src/features/admin/PerformanceMonitoring.tsx index 02517e2..ed51c45 100644 --- a/frontend/src/features/admin/PerformanceMonitoring.tsx +++ b/frontend/src/features/admin/PerformanceMonitoring.tsx @@ -153,7 +153,7 @@ export function PerformanceMonitoring() {
      - + 사용자 그룹별 SLO (총 2,900명 + 추정) 본청 200 · 상황실 100 확정
      @@ -225,20 +225,20 @@ export function PerformanceMonitoring() { {/* 성능 영향 최소화 전략 */}
      - + 성능 영향 최소화 전략 (글로벌 AIS 대응)
      {IMPACT_REDUCTION.map((s, i) => (
      -
      {i + 1}
      +
      {i + 1}
      {s.strategy} {s.per}
      대상: {s.target}
      -
      효과: {s.effect}
      +
      효과: {s.effect}
      ))} @@ -253,7 +253,7 @@ export function PerformanceMonitoring() {
      - + PER-01 서비스 응답성 — SLO vs 실측 (p50/p95/p99)
      TER-03 검증 통과 @@ -273,7 +273,7 @@ export function PerformanceMonitoring() { {RESPONSE_SLO.map(r => (
      - + @@ -287,7 +287,7 @@ export function PerformanceMonitoring() {
      - + 상황실 전용 SLO (24/7 100명)
      @@ -304,8 +304,8 @@ export function PerformanceMonitoring() {
      목표: {s.target}
      - {s.current} - {s.met ? : } + {s.current} + {s.met ? : }
      ))} @@ -314,32 +314,32 @@ export function PerformanceMonitoring() {
      - + 측정 방법론
      • - +
        샘플링: 1초 간격 p50/p95/p99 집계
      • - +
        도구: OpenTelemetry + Prometheus + Grafana
      • - +
        APM: 분산 추적 + Trace ID 요청 단위 관통
      • - +
        API 재시도: 3회 · Exponential Backoff · 타임아웃 3초
      • - +
        경보: SLO 위반 지속 5분 → PagerDuty
      • - +
        원인 분석: RED/USE 방법론 + 로그·메트릭·추적 상관 분석
      @@ -354,7 +354,7 @@ export function PerformanceMonitoring() { {/* 동시접속·TPS */}
      - + PER-02 동시접속·처리용량 (정상 피크 600 / 작전 피크 900)
      @@ -375,7 +375,7 @@ export function PerformanceMonitoring() { {/* 배치 작업 현황 */}
      - + PER-03 배치 · 대용량 처리 현황 SLA 준수 6/7
      @@ -396,7 +396,7 @@ export function PerformanceMonitoring() {
      - + @@ -408,7 +408,7 @@ export function PerformanceMonitoring() { {/* 처리 볼륨 산정 */}
      - + 데이터 처리 볼륨 (국내 + S&P 글로벌)
      @@ -420,12 +420,12 @@ export function PerformanceMonitoring() {
      일 적재 (필터·압축 후)
      330 ~ 900 GB
      -
      경계 필터링 50~80% 감축
      +
      경계 필터링 50~80% 감축
      3년 누적 (티어드)
      ~360 TB ~ 1 PB
      -
      NAS 100TB → 객체스토리지 이관
      +
      NAS 100TB → 객체스토리지 이관
      @@ -438,7 +438,7 @@ export function PerformanceMonitoring() {
      - + PER-04 AI 모델 성능 지표
      @@ -467,7 +467,7 @@ export function PerformanceMonitoring() {
      - + @@ -479,28 +479,28 @@ export function PerformanceMonitoring() {
      - + 모델 성능 저하 대응
      • - +
        학습/검증/테스트 분할: 70/15/15 비율, K-Fold 5
      • - +
        드리프트 탐지: 입력 분포 KL divergence 주간 모니터링
      • - +
        성능 저하 임계: F1 3%p 하락 시 자동 재학습 트리거
      • - +
        설명가능성: Feature Importance + SHAP 값 제공
      • - +
        A/B 테스트: Shadow → Canary 5% → 50% → 100% 단계 배포
      @@ -508,7 +508,7 @@ export function PerformanceMonitoring() {
      - + 추론 성능 (GPU 활용)
      @@ -551,7 +551,7 @@ export function PerformanceMonitoring() { {/* 가용성 */}
      - + PER-05 가용성 및 장애복구 (목표 ≥ 99.9%)
      {m.desc} {m.target} {m.encryption}{m.recovery}{m.recovery} {m.status}
      {e.dataType} {e.reason} {e.originalExpiry}{e.extendedTo}{e.extendedTo} {e.approver} {e.status}
      {d.target} {d.type} {d.method}{d.volume}{d.volume} {d.operator} {d.approver} {d.result}
      {r.target}{r.slo}{r.slo} {r.p50} {r.p95} {r.p99}{j.name} {j.schedule} {j.volume}{j.sla}{j.sla} {j.avg} {j.lastRun}
      {m.precision}% {m.recall}% {m.f1}{m.rocAuc}{m.rocAuc} {m.target} {m.status === 'good' ? '통과' : '주의'}
      @@ -570,7 +570,7 @@ export function PerformanceMonitoring() { - + @@ -583,7 +583,7 @@ export function PerformanceMonitoring() { {/* 확장성 */}
      - + PER-06 확장성 및 자원 사용률 2배(6,000명) 확장 목표
      @@ -613,34 +613,34 @@ export function PerformanceMonitoring() {
      - + 연간 가동률 목표
      -
      99.9%
      +
      99.9%
      월간 다운타임 ≤ 43분
      - + RTO 평균
      -
      ≤ 60초
      +
      ≤ 60초
      자동 페일오버 · Self-healing
      - + RPO 평균
      -
      ≤ 10초
      +
      ≤ 10초
      실시간 복제 + 백업 이중화
      - + Scale-out 여유
      -
      ×2
      +
      ×2
      6,000명까지 선형 확장
      diff --git a/frontend/src/features/admin/PermissionsPanel.tsx b/frontend/src/features/admin/PermissionsPanel.tsx index 666a256..93b1738 100644 --- a/frontend/src/features/admin/PermissionsPanel.tsx +++ b/frontend/src/features/admin/PermissionsPanel.tsx @@ -6,6 +6,7 @@ import { import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; import { fetchRoles, fetchPermTree, createRole, deleteRole, updateRolePermissions, type RoleWithPermissions, type PermTreeNode, type PermEntry, @@ -360,10 +361,13 @@ export function PermissionsPanel() {

      - +
      @@ -380,28 +384,44 @@ export function PermissionsPanel() {
      역할
      {canCreateRole && ( - + +
      {showCreate && (
      - setNewRoleCd(e.target.value.toUpperCase())} - placeholder="ROLE_CD (대문자)" - className="w-full bg-background border border-border rounded px-2 py-1 text-[10px] text-heading" /> - setNewRoleNm(e.target.value)} - placeholder={tc('aria.roleName')} - className="w-full bg-background border border-border rounded px-2 py-1 text-[10px] text-heading" /> + setNewRoleCd(e.target.value.toUpperCase())} + placeholder="ROLE_CD (대문자)" + /> + setNewRoleNm(e.target.value)} + placeholder={tc('aria.roleName')} + />
      - - + +
      diff --git a/frontend/src/features/ai-operations/AIAssistant.tsx b/frontend/src/features/ai-operations/AIAssistant.tsx index ee3a99d..943dec0 100644 --- a/frontend/src/features/ai-operations/AIAssistant.tsx +++ b/frontend/src/features/ai-operations/AIAssistant.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { PageContainer, PageHeader } from '@shared/components/layout'; @@ -80,7 +82,7 @@ export function AIAssistant() { @@ -92,7 +94,7 @@ export function AIAssistant() {
      {SAMPLE_CONVERSATIONS.map(c => (
      setSelectedConv(c.id)} - className={`px-2 py-1.5 rounded-lg cursor-pointer text-[10px] ${selectedConv === c.id ? 'bg-green-600/15 text-green-400 border border-green-500/20' : 'text-muted-foreground hover:bg-surface-overlay'}`}> + className={`px-2 py-1.5 rounded-lg cursor-pointer text-[10px] ${selectedConv === c.id ? 'bg-green-600/15 text-green-600 dark:text-green-400 border border-green-500/20' : 'text-muted-foreground hover:bg-surface-overlay'}`}>
      {c.title}
      {c.time}
      @@ -112,7 +114,7 @@ export function AIAssistant() {
      {msg.role === 'assistant' && (
      - +
      )}
      0 && (
      {msg.refs.map(r => ( - + {r} ))} @@ -133,7 +135,7 @@ export function AIAssistant() {
      {msg.role === 'user' && (
      - +
      )}
      @@ -141,17 +143,22 @@ export function AIAssistant() {
      {/* 입력창 */}
      - setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSend()} placeholder="질의를 입력하세요... (법령, 단속 절차, AI 분석 결과 등)" - className="flex-1 bg-surface-overlay border border-slate-700/50 rounded-xl px-4 py-2.5 text-[11px] text-heading placeholder:text-hint focus:outline-none focus:border-green-500/50" + className="flex-1" + /> +
      diff --git a/frontend/src/features/ai-operations/AIModelManagement.tsx b/frontend/src/features/ai-operations/AIModelManagement.tsx index 4a66cb8..448147a 100644 --- a/frontend/src/features/ai-operations/AIModelManagement.tsx +++ b/frontend/src/features/ai-operations/AIModelManagement.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; 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 { @@ -57,7 +58,7 @@ const MODELS: ModelVersion[] = [ ]; const modelColumns: DataColumn[] = [ - { key: 'version', label: '버전', width: '80px', sortable: true, render: (v) => {v as string} }, + { key: 'version', label: '버전', width: '80px', sortable: true, render: (v) => {v as string} }, { key: 'status', label: '상태', width: '70px', align: 'center', sortable: true, render: (v) => { const s = v as string; @@ -68,7 +69,7 @@ const modelColumns: DataColumn[] = [ { key: 'recall', label: 'Recall', width: '70px', align: 'right', sortable: true, render: (v) => {v as number}% }, { key: 'f1', label: 'F1', width: '70px', align: 'right', sortable: true, render: (v) => {v as number}% }, { key: 'falseAlarm', label: '오탐률', width: '70px', align: 'right', sortable: true, - render: (v) => { const n = v as number; return {n}%; }, + render: (v) => { const n = v as number; return {n}%; }, }, { key: 'trainData', label: '학습데이터', width: '100px', align: 'right' }, { key: 'deployDate', label: '배포일', width: '100px', sortable: true, render: (v) => {v as string} }, @@ -175,7 +176,7 @@ const GEAR_CODES: GearCode[] = [ ]; const gearColumns: DataColumn[] = [ - { key: 'code', label: '코드', width: '60px', render: (v) => {v as string} }, + { key: 'code', label: '코드', width: '60px', render: (v) => {v as string} }, { key: 'name', label: '어구명 (유형)', sortable: true, render: (v) => {v as string} }, { key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true, render: (v) => { @@ -396,14 +397,14 @@ export function AIModelManagement() {
      - 운영 모델: {currentModel.version} + 운영 모델: {currentModel.version} Accuracy {currentModel.accuracy}%
      } @@ -412,12 +413,12 @@ export function AIModelManagement() { {/* KPI */}
      {[ - { label: '탐지 정확도', value: `${currentModel.accuracy}%`, icon: Target, color: 'text-green-400', bg: 'bg-green-500/10' }, - { label: '오탐률', value: `${currentModel.falseAlarm}%`, icon: AlertTriangle, color: 'text-yellow-400', bg: 'bg-yellow-500/10' }, - { label: '평균 리드타임', value: '12min', icon: Clock, color: 'text-cyan-400', bg: 'bg-cyan-500/10' }, - { label: '학습 데이터', value: currentModel.trainData, icon: Database, color: 'text-blue-400', bg: 'bg-blue-500/10' }, - { label: '모델 버전', value: MODELS.length + '개', icon: GitBranch, color: 'text-purple-400', bg: 'bg-purple-500/10' }, - { label: '탐지 규칙', value: rules.filter((r) => r.enabled).length + '/' + rules.length, icon: Shield, color: 'text-orange-400', bg: 'bg-orange-500/10' }, + { label: '탐지 정확도', value: `${currentModel.accuracy}%`, icon: Target, color: 'text-green-600 dark:text-green-400', bg: 'bg-green-500/10' }, + { label: '오탐률', value: `${currentModel.falseAlarm}%`, icon: AlertTriangle, color: 'text-yellow-600 dark:text-yellow-400', bg: 'bg-yellow-500/10' }, + { label: '평균 리드타임', value: '12min', icon: Clock, color: 'text-cyan-600 dark:text-cyan-400', bg: 'bg-cyan-500/10' }, + { label: '학습 데이터', value: currentModel.trainData, icon: Database, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-500/10' }, + { label: '모델 버전', value: MODELS.length + '개', icon: GitBranch, color: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-500/10' }, + { label: '탐지 규칙', value: rules.filter((r) => r.enabled).length + '/' + rules.length, icon: Shield, color: 'text-orange-600 dark:text-orange-400', bg: 'bg-orange-500/10' }, ].map((kpi) => (
      @@ -454,13 +455,13 @@ export function AIModelManagement() { {/* 업데이트 알림 */}
      - +
      새로운 모델 v2.4.0 테스트 완료
      정확도 93.2% (+3.1%) · 오탐률 7.8% (-2.1%) · 다크베셀 탐지 강화
      - +
      @@ -495,7 +496,7 @@ export function AIModelManagement() {
      가중치
      -
      {rule.weight}%
      +
      {rule.weight}%
      @@ -505,7 +506,7 @@ export function AIModelManagement() { {/* 가중치 합계 */} -
      위험도 가중치
      +
      위험도 가중치
      {rules.filter((r) => r.enabled).map((r, i) => (
      @@ -564,7 +565,7 @@ export function AIModelManagement() { {/* 파이프라인 스테이지 */}
      {PIPELINE_STAGES.map((stage, i) => { - const stColor = stage.status === '정상' ? 'text-green-400' : stage.status === '진행중' ? 'text-blue-400' : 'text-hint'; + const stColor = stage.status === '정상' ? 'text-green-600 dark:text-green-400' : stage.status === '진행중' ? 'text-blue-600 dark:text-blue-400' : 'text-hint'; return (
      @@ -695,7 +696,7 @@ export function AIModelManagement() {
      {kpi.label} - + {kpi.current}{kpi.unit} {achieved ? '(달성)' : `(목표 ${kpi.target}${kpi.unit})`}
      @@ -761,7 +762,7 @@ export function AIModelManagement() { - + 5종 어구 특성 비교 요약 @@ -781,7 +782,7 @@ export function AIModelManagement() { {DAR03_GEAR_SUMMARY.map((g) => (
      @@ -790,7 +791,7 @@ export function AIModelManagement() { {g.iuuRisk} - + ))} @@ -802,7 +803,7 @@ export function AIModelManagement() { - + 어구별 구조 도식 비교

      @@ -815,7 +816,7 @@ export function AIModelManagement() {

      - {g.no} + {g.no}
      {g.name}
      {g.nameEn}
      @@ -854,7 +855,7 @@ export function AIModelManagement() { - + 어구별 AIS 신호 특성 및 이상 판정 기준 @@ -873,7 +874,7 @@ export function AIModelManagement() { {DAR03_AIS_SIGNALS.map((s) => (
      @@ -891,13 +892,13 @@ export function AIModelManagement() {
        {s.threshold.map((th) => (
      • - + {th}
      • ))}
      - + ))} @@ -921,8 +922,8 @@ export function AIModelManagement() {
      906
      허가 선박
      -
      7
      탐지 엔진
      -
      5
      업종 분류
      +
      7
      탐지 엔진
      +
      5
      업종 분류
      @@ -974,7 +975,7 @@ export function AIModelManagement() {
      - 대상 선박 현황 (906척, 6개 업종) + 대상 선박 현황 (906척, 6개 업종)
      {a.component} {a.uptime}{a.rto}{a.rto} {a.rpo} {a.lastIncident} {a.status === 'good' ? '정상' : '주의'}
      - {g.no} + {g.no} {g.name} {g.faoCode} {g.aisType}{g.gCodes}{g.gCodes}
      - {s.no} + {s.no} {s.name} {s.aisType} {s.gCodes}{s.gCodes}
      @@ -991,7 +992,7 @@ export function AIModelManagement() { {TARGET_VESSELS.map((v) => ( - + @@ -1014,7 +1015,7 @@ export function AIModelManagement() {
      - 알람 심각도 체계 + 알람 심각도 체계
      {ALARM_SEVERITY.map((a) => ( @@ -1064,8 +1065,8 @@ export function AIModelManagement() {
      12
      API 엔드포인트
      -
      3
      저장 단위
      -
      99.7%
      가용률
      +
      3
      저장 단위
      +
      99.7%
      가용률
      @@ -1114,7 +1115,7 @@ export function AIModelManagement() {
      - + RESTful API 엔드포인트
      {v.code}{v.code} {v.name} {v.count} {v.zones}
      @@ -1155,7 +1156,7 @@ export function AIModelManagement() { - + @@ -1175,7 +1176,7 @@ export function AIModelManagement() {
      - + API 호출 예시
      @@ -1231,7 +1232,7 @@ export function AIModelManagement() {
      - + 후속 서비스 연계 매핑
      @@ -1255,7 +1256,7 @@ export function AIModelManagement() {
      {s.desc}
      {s.apis.map((a) => ( - {a} + {a} ))}
      @@ -1272,13 +1273,13 @@ export function AIModelManagement() {
      {[ { label: '총 호출', value: '142,856', color: 'text-heading' }, - { label: 'grid 조회', value: '68,420', color: 'text-blue-400' }, - { label: 'zone 조회', value: '32,115', color: 'text-green-400' }, - { label: 'time 조회', value: '18,903', color: 'text-yellow-400' }, - { label: 'vessel 조회', value: '15,210', color: 'text-orange-400' }, - { label: 'alarms', value: '8,208', color: 'text-red-400' }, - { label: '평균 응답', value: '23ms', color: 'text-cyan-400' }, - { label: '오류율', value: '0.03%', color: 'text-green-400' }, + { label: 'grid 조회', value: '68,420', color: 'text-blue-600 dark:text-blue-400' }, + { label: 'zone 조회', value: '32,115', color: 'text-green-600 dark:text-green-400' }, + { label: 'time 조회', value: '18,903', color: 'text-yellow-600 dark:text-yellow-400' }, + { label: 'vessel 조회', value: '15,210', color: 'text-orange-600 dark:text-orange-400' }, + { label: 'alarms', value: '8,208', color: 'text-red-600 dark:text-red-400' }, + { label: '평균 응답', value: '23ms', color: 'text-cyan-600 dark:text-cyan-400' }, + { label: '오류율', value: '0.03%', color: 'text-green-600 dark:text-green-400' }, ].map((s) => (
      {s.value}
      diff --git a/frontend/src/features/ai-operations/MLOpsPage.tsx b/frontend/src/features/ai-operations/MLOpsPage.tsx index b4fad15..79e7fe2 100644 --- a/frontend/src/features/ai-operations/MLOpsPage.tsx +++ b/frontend/src/features/ai-operations/MLOpsPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@shared/components/ui/button'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { PageContainer, PageHeader } from '@shared/components/layout'; @@ -117,7 +118,7 @@ export function MLOpsPage() { ( ))} @@ -160,7 +161,7 @@ export function MLOpsPage() { DEPLOYED {m.name} {m.ver} - F1 {m.f1}% + F1 {m.f1}%
      ))}
      @@ -188,7 +189,7 @@ export function MLOpsPage() { {TEMPLATES.map((t, i) => (
      setSelectedTmpl(i)} className={`p-3 rounded-lg text-center cursor-pointer transition-colors ${selectedTmpl === i ? 'bg-blue-600/15 border border-blue-500/30' : 'bg-surface-overlay border border-transparent hover:border-border'}`}> - +
      {t.name}
      {t.desc}
      @@ -198,7 +199,7 @@ export function MLOpsPage() {
      실험 목록
      - +
      {EXPERIMENTS.map(e => ( @@ -209,7 +210,7 @@ export function MLOpsPage() {
      {e.epoch} {e.time} - {e.f1 > 0 && F1 {e.f1}} + {e.f1 > 0 && F1 {e.f1}}
      ))}
      @@ -262,7 +263,7 @@ export function MLOpsPage() {
      {DEPLOYS.map((d, i) => ( - + @@ -289,7 +290,7 @@ export function MLOpsPage() { {MODELS.filter(m => m.status === 'APPROVED').map(m => (
      {m.name} {m.ver} - +
      ))} @@ -314,15 +315,15 @@ export function MLOpsPage() { "version": "v2.1.0" }`} />
      - +
      RESPONSE
      - 상태 200 OK - 지연 23ms + 상태 200 OK + 지연 23ms
      {`{
         "risk_score": 87.5,
      @@ -354,7 +355,7 @@ export function MLOpsPage() {
                     { key: 'llmtest' as LLMSubTab, label: 'LLM 테스트' },
                   ]).map(t => (
                     
      +                className={`px-4 py-2 text-[11px] font-medium border-b-2 ${llmSub === t.key ? 'text-blue-600 dark:text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>{t.label}
                   ))}
                 
       
      @@ -368,7 +369,7 @@ export function MLOpsPage() {
                           {LLM_MODELS.map((m, i) => (
                             
      setSelectedLLM(i)} className={`p-3 rounded-lg text-center cursor-pointer ${selectedLLM === i ? 'bg-blue-600/15 border border-blue-500/30' : 'bg-surface-overlay border border-transparent'}`}> - +
      {m.name}
      {m.sub}
      @@ -382,7 +383,7 @@ export function MLOpsPage() {
      {k}
      {v}
      ))} - + @@ -418,10 +419,10 @@ export function MLOpsPage() {
      {k}{v}
      ))} - +
      -
      HPS 시도 결과
      Best: Trial #3 (F1=0.912)
      +
      HPS 시도 결과
      Best: Trial #3 (F1=0.912)
      {api.method} {api.endpoint}{api.endpoint} {api.unit} {api.desc} {api.sfr}
      {d.model}{d.ver}{d.ver} {d.endpoint}
      {d.traffic}%
      {d.latency}
      {['Trial', 'LR', 'Batch', 'Dropout', 'Hidden', 'F1 Score', ''].map(h => )}{HPS_TRIALS.map(t => ( @@ -506,7 +507,7 @@ export function MLOpsPage() {
      - +
      diff --git a/frontend/src/features/auth/LoginPage.tsx b/frontend/src/features/auth/LoginPage.tsx index 6f488fe..5636d99 100644 --- a/frontend/src/features/auth/LoginPage.tsx +++ b/frontend/src/features/auth/LoginPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Shield, Eye, EyeOff, Lock, User, Fingerprint, KeyRound, AlertCircle } from 'lucide-react'; +import { Button } from '@shared/components/ui/button'; import { useAuth } from '@/app/auth/AuthContext'; import { LoginError } from '@/services/authApi'; import { DemoQuickLogin, type DemoAccount } from './DemoQuickLogin'; @@ -105,7 +106,7 @@ export function LoginPage() { {/* 로고 영역 */}
      - +

      {t('title')}

      {t('subtitle')}

      @@ -122,7 +123,7 @@ export function LoginPage() { disabled={m.disabled} className={`flex-1 flex flex-col items-center gap-1 py-3 transition-colors ${ authMethod === m.key - ? 'bg-blue-600/10 border-b-2 border-blue-500 text-blue-400' + ? 'bg-blue-600/10 border-b-2 border-blue-500 text-blue-600 dark:text-blue-400' : 'text-hint hover:bg-surface-overlay hover:text-label' } ${m.disabled ? 'opacity-40 cursor-not-allowed' : ''}`} title={m.disabled ? '향후 도입 예정' : ''} @@ -188,16 +189,18 @@ export function LoginPage() {
      {error && ( -
      +
      {error}
      )} - + {/* 데모 퀵로그인 (VITE_SHOW_DEMO_LOGIN=true일 때만 렌더링) */} @@ -215,7 +218,7 @@ export function LoginPage() { {/* GPKI 인증 (Phase 9 도입 예정) */} {authMethod === 'gpki' && (
      - +

      {t('gpki.title')}

      향후 도입 예정 (Phase 9)

      @@ -224,7 +227,7 @@ export function LoginPage() { {/* SSO 연동 (Phase 9 도입 예정) */} {authMethod === 'sso' && (
      - +

      {t('sso.title')}

      향후 도입 예정 (Phase 9)

      diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx index 670d95a..99efbb8 100644 --- a/frontend/src/features/dashboard/Dashboard.tsx +++ b/frontend/src/features/dashboard/Dashboard.tsx @@ -55,7 +55,7 @@ function RiskBar({ value, size = 'default' }: { value: number; size?: 'default' // backend riskScore.score는 0~100 정수. 0~1 범위로 들어오는 경우도 호환. const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value)); const color = pct > 70 ? 'bg-red-500' : pct > 50 ? 'bg-orange-500' : pct > 30 ? 'bg-yellow-500' : 'bg-blue-500'; - const textColor = pct > 70 ? 'text-red-400' : pct > 50 ? 'text-orange-400' : pct > 30 ? 'text-yellow-400' : 'text-blue-400'; + const textColor = pct > 70 ? 'text-red-600 dark:text-red-400' : pct > 50 ? 'text-orange-600 dark:text-orange-400' : pct > 30 ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400'; const barW = size === 'sm' ? 'w-16' : 'w-24'; return (
      @@ -78,7 +78,7 @@ function KpiCard({ label, value, prev, icon: Icon, color, desc }: KpiCardProps)
      -
      +
      {isUp ? : } {Math.abs(diff)}
      @@ -207,16 +207,16 @@ function SeaAreaMap() {
      위협 등급
      - 낮음 + 낮음
      - 높음 + 높음
      {/* LIVE 인디케이터 */}
      - 실시간 해역 위협도 + 실시간 해역 위협도
      ); @@ -468,8 +468,8 @@ export function Dashboard() { 85 ? '#ef4444' : area.risk > 60 ? '#f97316' : area.risk > 40 ? '#eab308' : '#3b82f6' }}>{area.risk} - {area.trend === 'up' && } - {area.trend === 'down' && } + {area.trend === 'up' && } + {area.trend === 'down' && } {area.trend === 'stable' && }
      ))} @@ -544,7 +544,7 @@ export function Dashboard() { - + 해상 기상 현황 @@ -557,19 +557,19 @@ export function Dashboard() {
      돌풍 {WEATHER_DATA.wind.gust}m/s
      - +
      {WEATHER_DATA.wave.height}m
      파고
      주기 {WEATHER_DATA.wave.period}s
      - +
      {WEATHER_DATA.temp.air}°C
      기온
      수온 {WEATHER_DATA.temp.water}°C
      - +
      {WEATHER_DATA.visibility}km
      시정
      해상{WEATHER_DATA.seaState}급
      diff --git a/frontend/src/features/detection/ChinaFishing.tsx b/frontend/src/features/detection/ChinaFishing.tsx index e13c997..d37cdfa 100644 --- a/frontend/src/features/detection/ChinaFishing.tsx +++ b/frontend/src/features/detection/ChinaFishing.tsx @@ -1,6 +1,10 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; +import { Select } from '@shared/components/ui/select'; +import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { PageContainer } from '@shared/components/layout'; import { Search, Clock, ChevronRight, ChevronLeft, Cloud, @@ -339,22 +343,19 @@ export function ChinaFishing() { return ( {/* ── 모드 탭 (AI 대시보드 / 환적 탐지) ── */} -
      + {modeTabs.map((tab) => ( - + ))} -
      + {/* 환적 탐지 모드 */} {mode === 'transfer' && } @@ -372,7 +373,7 @@ export function ChinaFishing() {
      )} - {apiError &&
      에러: {apiError}
      } + {apiError &&
      {tcCommon('error.errorPrefix', { msg: apiError })}
      } {apiLoading && (
      @@ -389,16 +390,21 @@ export function ChinaFishing() { 기준 : {formatDateTime(new Date())}
      - +
      @@ -456,13 +462,13 @@ export function ChinaFishing() {
      - 종합 위험지수 + 종합 위험지수
      - 종합 안전지수 + 종합 안전지수
      @@ -480,29 +486,32 @@ export function ChinaFishing() { 관심영역 안전도 데모 데이터
      - - +

      설정한 관심 영역을 선택후 조회를 눌러주세요.

      - + 특이운항 - 정상 + 정상
      - + 불법조업 - 정상 + 정상
      - + 비허가 - 정상 + 정상
      {/* 탭 헤더 — 특이운항만 활성, 나머지 3개는 데이터 소스 미연동 */} -
      + {vesselTabs.map((tab) => { const disabled = tab !== '특이운항'; return ( - + ); })} -
      + {/* 선박 목록 */}
      @@ -599,22 +604,20 @@ export function ChinaFishing() { {/* 탭 — 월별 집계 API 미연동 */} -
      + {statsTabs.map((tab) => ( - + ))} -
      +
      {/* 월별 통계 - API 미지원, 준비중 안내 */} @@ -659,9 +662,9 @@ export function ChinaFishing() { {/* 다운로드 버튼 */}
      - +
      @@ -677,7 +680,7 @@ export function ChinaFishing() { 최근 위성영상 분석 데모 데이터
      - +
      @@ -704,11 +707,11 @@ export function ChinaFishing() { 기상 예보 데모 데이터
      - +
      - +
      전남서부남해앞바다
      @@ -730,7 +733,7 @@ export function ChinaFishing() { VTS연계 현황 데모 데이터
      - +
      {VTS_ITEMS.map((vts) => ( @@ -738,22 +741,28 @@ export function ChinaFishing() { key={vts.name} className={`flex items-center gap-1.5 px-2 py-1 rounded text-[10px] ${ vts.active - ? 'bg-orange-500/15 text-orange-400 border border-orange-500/20' + ? 'bg-orange-500/15 text-orange-600 dark:text-orange-400 border border-orange-500/20' : 'bg-surface-overlay text-muted-foreground border border-slate-700/30' }`} > - + {vts.name}
      ))}
      - - +
      diff --git a/frontend/src/features/detection/DarkVesselDetection.tsx b/frontend/src/features/detection/DarkVesselDetection.tsx index 6d7c179..0b34ba1 100644 --- a/frontend/src/features/detection/DarkVesselDetection.tsx +++ b/frontend/src/features/detection/DarkVesselDetection.tsx @@ -93,15 +93,18 @@ export function DarkVesselDetection() { { key: 'darkScore', label: '의심점수', width: '80px', align: 'center', sortable: true, render: (v) => { const n = v as number; - return = 70 ? 'text-red-400' : n >= 50 ? 'text-orange-400' : 'text-yellow-400'}`}>{n}; + const c = n >= 70 ? 'text-red-600 dark:text-red-400' + : n >= 50 ? 'text-orange-600 dark:text-orange-400' + : 'text-yellow-600 dark:text-yellow-400'; + return {n}; } }, { key: 'name', label: '선박 유형', sortable: true, - render: (v) => {v as string} }, + render: (v) => {v as string} }, { key: 'mmsi', label: 'MMSI', width: '100px', render: (v) => { const mmsi = v as string; return ( - @@ -116,7 +119,10 @@ export function DarkVesselDetection() { { key: 'risk', label: '위험도', width: '70px', align: 'center', sortable: true, render: (v) => { const n = v as number; - return = 70 ? 'text-red-400' : n >= 50 ? 'text-yellow-400' : 'text-green-400'}`}>{n}; + const c = n >= 70 ? 'text-red-600 dark:text-red-400' + : n >= 50 ? 'text-yellow-600 dark:text-yellow-400' + : 'text-green-600 dark:text-green-400'; + return {n}; } }, { key: 'darkPatterns', label: '의심 패턴', minWidth: '120px', render: (v) => {v as string} }, @@ -252,7 +258,7 @@ export function DarkVesselDetection() { } /> - {error &&
      에러: {error}
      } + {error &&
      {tc('error.errorPrefix', { msg: error })}
      } {loading && (
      @@ -263,10 +269,10 @@ export function DarkVesselDetection() { {/* KPI — tier 기반 */}
      {[ - { l: '전체', v: tierCounts.total, c: 'text-red-400', filter: '' }, - { l: 'CRITICAL', v: tierCounts.CRITICAL, c: 'text-red-400', filter: 'CRITICAL' }, - { l: 'HIGH', v: tierCounts.HIGH, c: 'text-orange-400', filter: 'HIGH' }, - { l: 'WATCH', v: tierCounts.WATCH, c: 'text-yellow-400', filter: 'WATCH' }, + { l: '전체', v: tierCounts.total, c: 'text-red-600 dark:text-red-400', filter: '' }, + { l: 'CRITICAL', v: tierCounts.CRITICAL, c: 'text-red-600 dark:text-red-400', filter: 'CRITICAL' }, + { l: 'HIGH', v: tierCounts.HIGH, c: 'text-orange-600 dark:text-orange-400', filter: 'HIGH' }, + { l: 'WATCH', v: tierCounts.WATCH, c: 'text-yellow-600 dark:text-yellow-400', filter: 'WATCH' }, ].map((k) => (
      setTierFilter(k.filter)} @@ -304,7 +310,7 @@ export function DarkVesselDetection() {
      - {tierCounts.CRITICAL}척 + {tierCounts.CRITICAL}척 CRITICAL Dark Vessel
      diff --git a/frontend/src/features/detection/GearDetection.tsx b/frontend/src/features/detection/GearDetection.tsx index 31cd0f1..ef1d3b2 100644 --- a/frontend/src/features/detection/GearDetection.tsx +++ b/frontend/src/features/detection/GearDetection.tsx @@ -2,6 +2,8 @@ import { useEffect, useState, useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { Button } from '@shared/components/ui/button'; +import { Checkbox } from '@shared/components/ui/checkbox'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { Anchor, AlertTriangle, Loader2, Filter, X } from 'lucide-react'; @@ -143,11 +145,13 @@ function FilterCheckGroup({ label, selected, onChange, options }: {
      {label} {selected.size > 0 && ({selected.size})}
      {options.map(o => ( - + toggle(o.value)} + label={o.label} + className="w-3 h-3" + /> ))}
      @@ -169,7 +173,7 @@ export function GearDetection() { { key: 'id', label: 'ID', width: '70px', render: v => {v as string} }, { key: 'type', label: '그룹 유형', width: '100px', sortable: true, render: v => {v as string} }, { key: 'owner', label: '어구 그룹', sortable: true, - render: v => {v as string} }, + render: v => {v as string} }, { key: 'memberCount', label: '멤버', width: '50px', align: 'center', render: v => {v as number}척 }, { key: 'zone', label: '설치 해역', width: '130px', sortable: true, @@ -191,7 +195,13 @@ export function GearDetection() {
      ) : - }, { key: 'risk', label: '위험도', width: '65px', align: 'center', sortable: true, - render: v => { const r = v as string; const c = r === '고위험' ? 'text-red-400' : r === '중위험' ? 'text-yellow-400' : 'text-green-400'; return {r}; } }, + render: v => { + const r = v as string; + const c = r === '고위험' ? 'text-red-600 dark:text-red-400' + : r === '중위험' ? 'text-yellow-600 dark:text-yellow-400' + : 'text-green-600 dark:text-green-400'; + return {r}; + } }, { key: 'parentStatus', label: '모선 상태', width: '90px', sortable: true, render: v => { const s = v as string; @@ -200,13 +210,13 @@ export function GearDetection() { return {label}; } }, { key: 'parentMmsi', label: '추정 모선', width: '100px', - render: v => { const m = v as string; return m !== '-' ? {m} : -; } }, + render: v => { const m = v as string; return m !== '-' ? {m} : -; } }, { key: 'topScore', label: '후보 일치', width: '75px', align: 'center', sortable: true, render: (v: unknown) => { const s = v as number; if (s <= 0) return -; const pct = Math.round(s * 100); - const c = pct >= 72 ? 'text-green-400' : pct >= 50 ? 'text-yellow-400' : 'text-hint'; + const c = pct >= 72 ? 'text-green-600 dark:text-green-400' : pct >= 50 ? 'text-yellow-600 dark:text-yellow-400' : 'text-hint'; return {pct}%; } }, { key: 'lastSignal', label: '최종 신호', width: '80px', render: v => {v as string} }, @@ -460,7 +470,7 @@ export function GearDetection() {
      )} - {error &&
      에러: {error}
      } + {error &&
      {tc('error.errorPrefix', { msg: error })}
      } {loading && (
      @@ -472,9 +482,9 @@ export function GearDetection() {
      {[ { l: '전체 어구 그룹', v: filteredData.length, c: 'text-heading' }, - { l: '불법 의심', v: filteredData.filter(d => d.status.includes('불법')).length, c: 'text-red-400' }, - { l: '확인 중', v: filteredData.filter(d => d.status === '확인 중').length, c: 'text-yellow-400' }, - { l: '정상', v: filteredData.filter(d => d.status === '정상').length, c: 'text-green-400' }, + { l: '불법 의심', v: filteredData.filter(d => d.status.includes('불법')).length, c: 'text-red-600 dark:text-red-400' }, + { l: '확인 중', v: filteredData.filter(d => d.status === '확인 중').length, c: 'text-yellow-600 dark:text-yellow-400' }, + { l: '정상', v: filteredData.filter(d => d.status === '정상').length, c: 'text-green-600 dark:text-green-400' }, ].map(k => (
      {k.v}{k.l} @@ -510,11 +520,15 @@ export function GearDetection() { {hasActiveFilter && ( <> {filteredData.length}/{DATA.length}건 - + icon={} + > + 초기화 + )}
      @@ -562,11 +576,15 @@ export function GearDetection() { {/* 패널 내 초기화 */}
      {filteredData.length}/{DATA.length}건 표시 - + icon={} + > + 전체 초기화 +
      )} @@ -620,8 +638,8 @@ export function GearDetection() {
      - - {DATA.length}건 + + {DATA.length}건 어구 그룹
      {/* 리플레이 컨트롤러 (활성 시 표시) */} diff --git a/frontend/src/features/detection/GearIdentification.tsx b/frontend/src/features/detection/GearIdentification.tsx index 575a430..4a2184f 100644 --- a/frontend/src/features/detection/GearIdentification.tsx +++ b/frontend/src/features/detection/GearIdentification.tsx @@ -7,6 +7,7 @@ import { } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { Button } from '@shared/components/ui/button'; import { getAlertLevelIntent, isValidAlertLevel } from '@shared/constants/alertLevels'; import { getZoneCodeLabel } from '@shared/constants/zoneCodes'; import { formatDateTime } from '@shared/utils/dateFormat'; @@ -573,7 +574,7 @@ function GearComparisonTable() { - + 한·중 어구 비교 레퍼런스 (GB/T 5147-2003 기반) @@ -593,18 +594,18 @@ function GearComparisonTable() {
      -
      중국어선 특징
      +
      중국어선 특징
      {row.chinaFeatures.map((f, i) => (
      - -{f} + -{f}
      ))}
      -
      한국어선 특징
      +
      한국어선 특징
      {row.koreaFeatures.map((f, i) => (
      - -{f} + -{f}
      ))}
      @@ -714,7 +715,7 @@ export function GearIdentification() {

      - + {t('gearId.title')}

      @@ -722,13 +723,14 @@ export function GearIdentification() {

      - +
      @@ -741,7 +743,7 @@ export function GearIdentification() {
      자동탐지 연계 MMSI - {autoSelected.mmsi} + {autoSelected.mmsi} · 어구 {autoSelected.gearCode} @@ -751,12 +753,14 @@ export function GearIdentification() { 하단 자동탐지 목록에서 클릭한 선박 정보가 아래 폼에 프리필되었습니다. 추가 정보 입력 후 판별 실행하세요.
      - +
      )} @@ -878,19 +882,22 @@ export function GearIdentification() { {/* 판별 버튼 */}
      - - +
      @@ -950,7 +957,7 @@ export function GearIdentification() { - + 판별 근거 ({result.reasons.length}건) @@ -958,7 +965,7 @@ export function GearIdentification() {
      {result.reasons.map((reason, i) => (
      - + {reason}
      ))} @@ -970,7 +977,7 @@ export function GearIdentification() { {result.warnings.length > 0 && ( - + 경고 / 위반 사항 ({result.warnings.length}건) @@ -979,8 +986,8 @@ export function GearIdentification() {
      {result.warnings.map((warning, i) => (
      - - {warning} + + {warning}
      ))}
      @@ -992,13 +999,13 @@ export function GearIdentification() { - + AI 탐지 Rule (해당 어구) {result.gearType === 'trawl' && ( -
      +                    
       {`# 트롤 탐지 조건 (Trawl Detection Rule)
       if   speed        in range(2.0, 5.0)   # knots
       and  trajectory   == 'parallel_sweep'   # 반복 평행선
      @@ -1013,7 +1020,7 @@ and  speed_sync           > 0.92              # 2선 속도 동기화`}
                           
      )} {result.gearType === 'gillnet' && ( -
      +                    
       {`# 자망 탐지 조건 (Gillnet Detection Rule)
       if   speed          < 2.0     # knots
       and  stop_duration  > 30      # min
      @@ -1028,7 +1035,7 @@ and  sar_vessel_detect  == True       # SAR 위치 확인
                           
      )} {result.gearType === 'purseSeine' && ( -
      +                    
       {`# 선망 탐지 조건 (Purse Seine Detection Rule)
       if   trajectory        == 'circular'        # 원형 궤적
       and  speed_change      > 5.0               # kt (고→저 급변)
      @@ -1044,7 +1051,7 @@ and  vessel_spacing       < 1000     # m
                           
      )} {result.gearType === 'setNet' && ( -
      +                    
       {`# 정치망 — EEZ 내 중국어선 미허가 어구
       # GB/T 코드: Z__ (ZD: 단묘장망, ZS: 복묘장망)
       #
      @@ -1070,7 +1077,7 @@ and  vessel_spacing       < 1000     # m
                     
                       
                         
      -                    
      +                    
                           다중 센서 교차 검증 파이프라인
                         
                       
      @@ -1164,20 +1171,23 @@ function AutoGearDetectionSection({
               
      - + 최근 자동탐지 결과 (prediction, 최근 1시간 중국 선박)
      GET /api/analysis/gear-detections · MMSI 412 · gear_code / gear_judgment NOT NULL · 행 클릭 시 상단 입력 폼에 프리필
      - +
      - {error &&
      에러: {error}
      } + {error &&
      {t('error.errorPrefix', { msg: error })}
      } {loading &&
      } {!loading && ( @@ -1212,7 +1222,7 @@ function AutoGearDetectionSection({ }`} title="클릭하면 상단 입력 폼에 자동으로 채워집니다" > -
      + - + - + @@ -180,7 +184,7 @@ export function LabelSession() { - + diff --git a/frontend/src/features/parent-inference/ParentReview.tsx b/frontend/src/features/parent-inference/ParentReview.tsx index ad96e70..8f3e2a7 100644 --- a/frontend/src/features/parent-inference/ParentReview.tsx +++ b/frontend/src/features/parent-inference/ParentReview.tsx @@ -3,6 +3,7 @@ import { CheckCircle, XCircle, RotateCcw, Loader2, GitMerge } from 'lucide-react import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; import { Select } from '@shared/components/ui/select'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; @@ -151,39 +152,40 @@ export function ParentReview() {
      신규 모선 확정 등록 (테스트)
      - setNewGroupKey(e.target.value)} placeholder="group_key (예: 渔船A)" - className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" + className="flex-1" /> - setNewSubCluster(e.target.value)} placeholder="sub_cluster_id" - className="w-32 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" + className="w-32" /> - setNewMmsi(e.target.value)} placeholder="parent MMSI" - className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" + className="w-40" /> - +
      @@ -192,7 +194,7 @@ export function ParentReview() { {!canUpdate && ( -
      +
      조회 전용 모드 (UPDATE 권한 없음). 확정/거부/리셋 액션이 비활성화됩니다.
      @@ -202,7 +204,7 @@ export function ParentReview() { {error && ( -
      {tc('error.errorPrefix', { msg: error })}
      +
      {tc('error.errorPrefix', { msg: error })}
      )} @@ -247,39 +249,42 @@ export function ParentReview() { {getParentResolutionLabel(it.status, tc, lang)} -
      + diff --git a/frontend/src/features/statistics/ReportManagement.tsx b/frontend/src/features/statistics/ReportManagement.tsx index d0ee776..54d720d 100644 --- a/frontend/src/features/statistics/ReportManagement.tsx +++ b/frontend/src/features/statistics/ReportManagement.tsx @@ -121,8 +121,8 @@ export function ReportManagement() {
      증거 {r.evidence}건
      - - + +
      ))} @@ -136,9 +136,9 @@ export function ReportManagement() {
      보고서 미리보기
      - +
      diff --git a/frontend/src/features/surveillance/LiveMapView.tsx b/frontend/src/features/surveillance/LiveMapView.tsx index 2b8f162..0d3282f 100644 --- a/frontend/src/features/surveillance/LiveMapView.tsx +++ b/frontend/src/features/surveillance/LiveMapView.tsx @@ -56,7 +56,7 @@ function RiskBar({ value, size = 'md' }: { value: number; size?: 'sm' | 'md' })
      - {value.toFixed(2)} + {value.toFixed(2)}
      ); } @@ -244,7 +244,7 @@ export function LiveMapView() { {loading && (
      - + 로드 중...
      )} @@ -252,8 +252,8 @@ export function LiveMapView() { {!serviceAvailable && !loading && (
      - - 분석 서비스 오프라인 + + 분석 서비스 오프라인

      이벤트 데이터만 표시됩니다.

      @@ -278,7 +278,7 @@ export function LiveMapView() { {evt.type}
      - +
      {evt.mmsi} · {evt.nationality} · {evt.time}
      @@ -318,7 +318,7 @@ export function LiveMapView() { {/* 실시간 표시 */}
      - LIVE + LIVE 경보 {mapEvents.length}건 · 분석 {vesselItems.length}척
      @@ -333,7 +333,7 @@ export function LiveMapView() {
      - +
      {selectedEvent.vesselName}
      @@ -348,7 +348,7 @@ export function LiveMapView() {
      위험도 점수
      - {Math.round(selectedEvent.risk * 100)} + {Math.round(selectedEvent.risk * 100)} /100
      @@ -364,29 +364,29 @@ export function LiveMapView() {
      - + AI 판단 근거 신뢰도: High
      - - {selectedEvent.type} + + {selectedEvent.type}
      선박: {selectedEvent.vesselName} ({selectedEvent.mmsi})
      - - 위치 정보 + + 위치 정보
      좌표: {selectedEvent.lat.toFixed(4)}, {selectedEvent.lng.toFixed(4)}
      - - 발생 시각 + + 발생 시각
      {selectedEvent.time}
      diff --git a/frontend/src/features/surveillance/MapControl.tsx b/frontend/src/features/surveillance/MapControl.tsx index 3927575..b6527ea 100644 --- a/frontend/src/features/surveillance/MapControl.tsx +++ b/frontend/src/features/surveillance/MapControl.tsx @@ -124,7 +124,7 @@ const NTM_DATA: NtmRecord[] = [ const NTM_CATEGORIES = ['전체', '사격훈련', '군사훈련', '기뢰제거', '해양공사', '항로표지', '항로변경', '해양오염', '수중작업']; const ntmColumns: DataColumn[] = [ - { key: 'no', label: '통보번호', width: '120px', sortable: true, render: v => {v as string} }, + { key: 'no', label: '통보번호', width: '120px', sortable: true, render: v => {v as string} }, { key: 'date', label: '발령일', width: '90px', sortable: true, render: v => {v as string} }, { key: 'category', label: '구분', width: '70px', align: 'center', sortable: true, render: v => { @@ -146,7 +146,7 @@ const ntmColumns: DataColumn[] = [ // 훈련구역 색상은 trainingZoneTypes 카탈로그에서 lookup const columns: DataColumn[] = [ - { key: 'id', label: '구역번호', width: '80px', sortable: true, render: v => {v as string} }, + { key: 'id', label: '구역번호', width: '80px', sortable: true, render: v => {v as string} }, { key: 'type', label: '구분', width: '60px', align: 'center', sortable: true, render: v => {v as string} }, { key: 'sea', label: '해역', width: '60px', sortable: true }, @@ -301,7 +301,7 @@ export function MapControl() { { key: 'ntm' as Tab, label: '항행통보', icon: Bell }, ]).map(t => ( ))} @@ -310,7 +310,7 @@ export function MapControl() { {['', '서해', '남해', '동해', '제주'].map(s => ( ))} @@ -324,9 +324,9 @@ export function MapControl() {
      {[ { label: '전체 통보', value: NTM_DATA.length, color: 'text-heading' }, - { label: '발령중', value: NTM_DATA.filter(n => n.status === '발령중').length, color: 'text-red-400' }, + { label: '발령중', value: NTM_DATA.filter(n => n.status === '발령중').length, color: 'text-red-600 dark:text-red-400' }, { label: '해제', value: NTM_DATA.filter(n => n.status === '해제').length, color: 'text-muted-foreground' }, - { label: '사격훈련', value: NTM_DATA.filter(n => n.category.includes('사격')).length, color: 'text-orange-400' }, + { label: '사격훈련', value: NTM_DATA.filter(n => n.category.includes('사격')).length, color: 'text-orange-600 dark:text-orange-400' }, ].map(k => (
      {k.value} @@ -341,14 +341,14 @@ export function MapControl() { 구분: {NTM_CATEGORIES.map(c => ( + className={`px-2.5 py-1 rounded text-[10px] ${(c === '전체' && !ntmCatFilter) || ntmCatFilter === c ? 'bg-cyan-600 hover:bg-cyan-500 text-on-vivid font-bold' : 'text-hint hover:bg-surface-overlay'}`}>{c} ))}
      {/* 최근 발령 중 통보 하이라이트 */}
      - 현재 발령 중 항행통보 + 현재 발령 중 항행통보
      {NTM_DATA.filter(n => n.status === '발령중').map(n => ( @@ -411,7 +411,7 @@ export function MapControl() {
      {/* 표시 구역 수 */}
      - {visibleZones.length}개 + {visibleZones.length}개 훈련구역 표시 중
      {h}
      {v.mmsi}{v.mmsi} {v.vesselType ?? '-'} {v.gearCode} diff --git a/frontend/src/features/detection/RealGearGroups.tsx b/frontend/src/features/detection/RealGearGroups.tsx index fd537a2..4d2c053 100644 --- a/frontend/src/features/detection/RealGearGroups.tsx +++ b/frontend/src/features/detection/RealGearGroups.tsx @@ -2,6 +2,9 @@ import { useEffect, useState, useCallback } from 'react'; import { Loader2, RefreshCw, MapPin } from 'lucide-react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { Button } from '@shared/components/ui/button'; +import { Select } from '@shared/components/ui/select'; +import type { BadgeIntent } from '@lib/theme/variants'; import { fetchGroups, type GearGroupItem } from '@/services/vesselAnalysisApi'; import { getGearGroupTypeIntent, getGearGroupTypeLabel } from '@shared/constants/gearGroupTypes'; import { getParentResolutionIntent, getParentResolutionLabel } from '@shared/constants/parentResolutionStatuses'; @@ -62,29 +65,37 @@ export function RealGearGroups() {
      - setFilterType(e.target.value)} + > - - + +
      {/* 통계 */}
      - - - - - + + + + +
      - {error &&
      에러: {error}
      } + {error &&
      {tc('error.errorPrefix', { msg: error })}
      } {loading &&
      } {!loading && ( @@ -142,11 +153,22 @@ export function RealGearGroups() { ); } -function StatBox({ label, value, color }: { label: string; value: number; color: string }) { +const INTENT_TEXT_CLASS: Record = { + critical: 'text-red-600 dark:text-red-400', + high: 'text-orange-600 dark:text-orange-400', + warning: 'text-yellow-600 dark:text-yellow-400', + info: 'text-blue-600 dark:text-blue-400', + success: 'text-green-600 dark:text-green-400', + muted: 'text-heading', + purple: 'text-purple-600 dark:text-purple-400', + cyan: 'text-cyan-600 dark:text-cyan-400', +}; + +function StatBox({ label, value, intent = 'muted' }: { label: string; value: number; intent?: BadgeIntent }) { return (
      {label}
      -
      {value}
      +
      {value}
      ); } diff --git a/frontend/src/features/detection/RealVesselAnalysis.tsx b/frontend/src/features/detection/RealVesselAnalysis.tsx index 95221b5..d1446b7 100644 --- a/frontend/src/features/detection/RealVesselAnalysis.tsx +++ b/frontend/src/features/detection/RealVesselAnalysis.tsx @@ -3,6 +3,9 @@ import { Loader2, RefreshCw, EyeOff, AlertTriangle, Radar } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { Button } from '@shared/components/ui/button'; +import { Select } from '@shared/components/ui/select'; +import type { BadgeIntent } from '@lib/theme/variants'; import { getAlertLevelIntent } from '@shared/constants/alertLevels'; import { getVesselTypeLabel } from '@shared/constants/vesselTypes'; import type { VesselAnalysisItem } from '@/services/vesselAnalysisApi'; @@ -118,31 +121,38 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore
      - setZoneFilter(e.target.value)} + > - - + +
      {/* 통계 카드 — items(= mode 필터 적용된 선박) 기준 집계 */}
      - - - - - - + + + + + +
      - {error &&
      에러: {error}
      } + {error &&
      {t('error.errorPrefix', { msg: error })}
      } {loading &&
      } {!loading && ( @@ -168,7 +178,7 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore )} {sortedByRisk.slice(0, 100).map((v) => (
      {v.mmsi}{v.mmsi} {getVesselTypeLabel(v.classification.vesselType, t, lang)} {v.classification.confidence > 0 && ( @@ -193,7 +203,7 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore {v.algorithms.gpsSpoofing.spoofingScore > 0 ? ( - {v.algorithms.gpsSpoofing.spoofingScore.toFixed(2)} + {v.algorithms.gpsSpoofing.spoofingScore.toFixed(2)} ) : -} @@ -220,11 +230,22 @@ export function RealVesselAnalysis({ mode, title, icon, mmsiPrefix, minRiskScore ); } -function StatBox({ label, value, color }: { label: string; value: number | undefined; color: string }) { +const INTENT_TEXT_CLASS: Record = { + critical: 'text-red-600 dark:text-red-400', + high: 'text-orange-600 dark:text-orange-400', + warning: 'text-yellow-600 dark:text-yellow-400', + info: 'text-blue-600 dark:text-blue-400', + success: 'text-green-600 dark:text-green-400', + muted: 'text-heading', + purple: 'text-purple-600 dark:text-purple-400', + cyan: 'text-cyan-600 dark:text-cyan-400', +}; + +function StatBox({ label, value, intent = 'muted' }: { label: string; value: number | undefined; intent?: BadgeIntent }) { return (
      {label}
      -
      {(value ?? 0).toLocaleString()}
      +
      {(value ?? 0).toLocaleString()}
      ); } diff --git a/frontend/src/features/detection/components/DarkDetailPanel.tsx b/frontend/src/features/detection/components/DarkDetailPanel.tsx index 7b388bf..a41ecb5 100644 --- a/frontend/src/features/detection/components/DarkDetailPanel.tsx +++ b/frontend/src/features/detection/components/DarkDetailPanel.tsx @@ -73,7 +73,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { {/* 헤더 */}
      - + 판정 상세 {darkTier} {darkScore}점 @@ -87,12 +87,12 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { {/* 선박 기본 정보 */}
      - + 선박 정보
      MMSI - @@ -116,7 +116,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { {/* 점수 산출 내역 */}
      - + 점수 산출 내역 ({breakdown.items.length}개 패턴 적용)
      @@ -126,7 +126,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { {/* GAP 상세 */}
      - + GAP 상세
      @@ -152,7 +152,7 @@ export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { {/* 과거 이력 */}
      - + 과거 이력 (7일)
      diff --git a/frontend/src/features/detection/components/GearDetailPanel.tsx b/frontend/src/features/detection/components/GearDetailPanel.tsx index 691ff35..cb33138 100644 --- a/frontend/src/features/detection/components/GearDetailPanel.tsx +++ b/frontend/src/features/detection/components/GearDetailPanel.tsx @@ -270,7 +270,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {/* 헤더 */}
      - + 어구 판정 상세 {gear.id} @@ -287,7 +287,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {gear.gCodes.length > 0 && (
      - + G코드 위반 내역 총 {gear.gearViolationScore}점
      @@ -312,7 +312,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {/* 어구 그룹 정보 */}
      - + 어구 그룹 정보
      @@ -344,7 +344,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {/* 모선 추론 정보 */}
      - + 모선 추론 {parentStatusLabel}
      @@ -352,7 +352,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { 추정 모선 {gear.parentMmsi !== '-' && gear.parentMmsi ? ( - @@ -366,7 +366,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {/* 모선 추론 후보 상세 (Correlation) */}
      - + 추론 후보 상세 {corrLoading && } {correlations.length}건 @@ -393,7 +393,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { className="w-3 h-3 rounded border-border accent-emerald-500 shrink-0 cursor-pointer" aria-label={`${c.targetMmsi} 리플레이 선택`} /> @@ -467,9 +467,9 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) {
      {/* 헤더 */}
      - + 후보 검토 - {cand.targetMmsi} + {cand.targetMmsi} {cand.targetName}
      @@ -575,7 +575,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {hasPairTrawl && (
      - + 쌍끌이 트롤 공조 G-06
      @@ -583,7 +583,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { 상대 선박 {gear.pairTrawlPairMmsi ? ( - @@ -615,7 +615,7 @@ export function GearDetailPanel({ gear, onClose }: GearDetailPanelProps) { {/* 위치 + 액션 */}
      - + 위치
      diff --git a/frontend/src/features/detection/components/VesselAnomalyPanel.tsx b/frontend/src/features/detection/components/VesselAnomalyPanel.tsx index c562c5b..52c64cb 100644 --- a/frontend/src/features/detection/components/VesselAnomalyPanel.tsx +++ b/frontend/src/features/detection/components/VesselAnomalyPanel.tsx @@ -38,14 +38,14 @@ export function VesselAnomalyPanel({ segments, loading, error, totalHistoryCount
      - + 특이운항 판별 구간 {segments.length > 0 && ( · {segments.length}구간 - {criticalCount > 0 && CRITICAL {criticalCount}} - {warningCount > 0 && WARNING {warningCount}} - {infoCount > 0 && INFO {infoCount}} + {criticalCount > 0 && CRITICAL {criticalCount}} + {warningCount > 0 && WARNING {warningCount}} + {infoCount > 0 && INFO {infoCount}} )}
      @@ -55,7 +55,7 @@ export function VesselAnomalyPanel({ segments, loading, error, totalHistoryCount
      {error && ( -
      +
      {error}
      diff --git a/frontend/src/features/detection/components/VesselMiniMap.tsx b/frontend/src/features/detection/components/VesselMiniMap.tsx index d301a78..68f4842 100644 --- a/frontend/src/features/detection/components/VesselMiniMap.tsx +++ b/frontend/src/features/detection/components/VesselMiniMap.tsx @@ -182,7 +182,7 @@ export function VesselMiniMap({ mmsi, vesselName, hoursBack = 24, segments = [],
      - +
      {vesselName ?? mmsi} @@ -218,7 +218,7 @@ export function VesselMiniMap({ mmsi, vesselName, hoursBack = 24, segments = [],
      )} {!loading && error && ( -
      +
      {error}
      )} diff --git a/frontend/src/features/enforcement/EventList.tsx b/frontend/src/features/enforcement/EventList.tsx index 1924eb6..651ef3e 100644 --- a/frontend/src/features/enforcement/EventList.tsx +++ b/frontend/src/features/enforcement/EventList.tsx @@ -131,7 +131,7 @@ export function EventList() { }, }, { key: 'vesselName', label: '선박명', minWidth: '100px', maxWidth: '220px', sortable: true, - render: (val) => {val as string}, + render: (val) => {val as string}, }, { key: 'mmsi', label: 'MMSI', minWidth: '90px', maxWidth: '120px', render: (_val, row) => { @@ -140,7 +140,7 @@ export function EventList() { return ( )} {isActionable && ( <> @@ -317,7 +317,7 @@ export function EventList() { {/* 에러 표시 */} {error && ( -
      +
      데이터 로딩 실패: {error}
      )} @@ -327,7 +327,7 @@ export function EventList() {
      이벤트 데이터 업로드 -
      @@ -343,7 +343,7 @@ export function EventList() { {/* 로딩 인디케이터 */} {loading && (
      - + 로딩 중...
      )} diff --git a/frontend/src/features/field-ops/AIAlert.tsx b/frontend/src/features/field-ops/AIAlert.tsx index 4c12489..44932a0 100644 --- a/frontend/src/features/field-ops/AIAlert.tsx +++ b/frontend/src/features/field-ops/AIAlert.tsx @@ -37,7 +37,7 @@ const cols: DataColumn[] = [ key: 'eventId', label: '이벤트', width: '80px', - render: (v) => EVT-{v as number}, + render: (v) => EVT-{v as number}, }, { key: 'time', @@ -58,7 +58,7 @@ const cols: DataColumn[] = [ { key: 'recipient', label: '수신 대상', - render: (v) => {v as string}, + render: (v) => {v as string}, }, { key: 'confidence', @@ -70,7 +70,7 @@ const cols: DataColumn[] = [ const s = v as string; if (!s) return -; const n = parseFloat(s); - const color = n > 0.9 ? 'text-red-400' : n > 0.8 ? 'text-orange-400' : 'text-yellow-400'; + const color = n > 0.9 ? 'text-red-600 dark:text-red-400' : n > 0.8 ? 'text-orange-600 dark:text-orange-400' : 'text-yellow-600 dark:text-yellow-400'; return {(n * 100).toFixed(0)}%; }, }, @@ -149,10 +149,10 @@ export function AIAlert() { if (error) { return ( -
      +
      알림 조회 실패: {error} -
      @@ -164,15 +164,15 @@ export function AIAlert() {
      {[ { l: '총 발송', v: totalElements, c: 'text-heading' }, - { l: '수신확인', v: deliveredCount, c: 'text-green-400' }, - { l: '실패', v: failedCount, c: 'text-red-400' }, + { l: '수신확인', v: deliveredCount, c: 'text-green-600 dark:text-green-400' }, + { l: '실패', v: failedCount, c: 'text-red-600 dark:text-red-400' }, ].map((k) => (
      @@ -70,7 +70,7 @@ export function MobileService() {
      {/* 긴급 경보 */}
      -
      [긴급] EEZ 침범 탐지
      +
      [긴급] EEZ 침범 탐지
      N37°12' E124°38' · 08:47
      {/* 지도 영역 — MapLibre GL */} @@ -125,14 +125,14 @@ export function MobileService() { { icon: WifiOff, name: '오프라인 지원', desc: '통신 불안정 시 지도·객체 임시 저장' }, ].map(f => (
      - +
      {f.name}
      {f.desc}
      ))}
      -
      푸시 알림 설정
      +
      푸시 알림 설정
      {PUSH_SETTINGS.map(p => (
      diff --git a/frontend/src/features/parent-inference/LabelSession.tsx b/frontend/src/features/parent-inference/LabelSession.tsx index c9761a7..c7973fd 100644 --- a/frontend/src/features/parent-inference/LabelSession.tsx +++ b/frontend/src/features/parent-inference/LabelSession.tsx @@ -3,6 +3,7 @@ import { Tag, X, Loader2 } from 'lucide-react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; import { Select } from '@shared/components/ui/select'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; @@ -119,26 +120,29 @@ export function LabelSession() {
      신규 학습 세션 등록 - {!canCreate && 권한 없음} + {!canCreate && 권한 없음}
      - setGroupKey(e.target.value)} placeholder="group_key" - className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} /> - setSubCluster(e.target.value)} placeholder="sub" - className="w-24 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} /> - setLabelMmsi(e.target.value)} placeholder="정답 parent MMSI" - className="w-48 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreate} /> - +
      - {error &&
      {tc('error.errorPrefix', { msg: error })}
      } + {error &&
      {tc('error.errorPrefix', { msg: error })}
      } {loading && (
      @@ -171,7 +175,7 @@ export function LabelSession() {
      {it.id} {it.groupKey} {it.subClusterId}{it.labelParentMmsi}{it.labelParentMmsi} {getLabelSessionLabel(it.status, tc, lang)} {it.status === 'ACTIVE' && ( )} diff --git a/frontend/src/features/parent-inference/ParentExclusion.tsx b/frontend/src/features/parent-inference/ParentExclusion.tsx index 66e0c32..11ff785 100644 --- a/frontend/src/features/parent-inference/ParentExclusion.tsx +++ b/frontend/src/features/parent-inference/ParentExclusion.tsx @@ -3,6 +3,7 @@ import { Ban, RotateCcw, Loader2, Globe, Layers } from 'lucide-react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; +import { Input } from '@shared/components/ui/input'; import { Select } from '@shared/components/ui/select'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; @@ -138,23 +139,26 @@ export function ParentExclusion() {
      GROUP 제외 (특정 그룹 한정) - {!canCreateGroup && 권한 없음} + {!canCreateGroup && 권한 없음}
      - setGrpKey(e.target.value)} placeholder="group_key" - className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} /> - setGrpSub(e.target.value)} placeholder="sub" - className="w-24 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} /> - setGrpMmsi(e.target.value)} placeholder="excluded MMSI" - className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} /> - setGrpReason(e.target.value)} placeholder="사유" - className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGroup} /> - +
      @@ -164,24 +168,27 @@ export function ParentExclusion() {
      GLOBAL 제외 (모든 그룹 영구 차단, 관리자 권한) - {!canCreateGlobal && 권한 없음} + {!canCreateGlobal && 권한 없음}
      - setGlbMmsi(e.target.value)} placeholder="excluded MMSI" - className="w-40 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGlobal} /> - setGlbReason(e.target.value)} placeholder="사유" - className="flex-1 bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs" disabled={!canCreateGlobal} /> - +
      - {error &&
      {tc('error.errorPrefix', { msg: error })}
      } + {error &&
      {tc('error.errorPrefix', { msg: error })}
      } {loading && (
      @@ -220,13 +227,13 @@ export function ParentExclusion() {
      {it.groupKey || '-'} {it.subClusterId ?? '-'}{it.excludedMmsi}{it.excludedMmsi} {it.reason || '-'} {it.actorAcnt || '-'} {formatDateTime(it.createdAt)} {it.selectedParentMmsi || '-'}{it.selectedParentMmsi || '-'} {formatDateTime(it.updatedAt)}
      - - - + aria-label="리셋" + className="text-blue-600 dark:text-blue-400 hover:bg-blue-500/20" + icon={} + />