diff --git a/backend/src/main/resources/db/migration/V026__data_retention_policy_menu.sql b/backend/src/main/resources/db/migration/V026__data_retention_policy_menu.sql new file mode 100644 index 0000000..000534c --- /dev/null +++ b/backend/src/main/resources/db/migration/V026__data_retention_policy_menu.sql @@ -0,0 +1,28 @@ +-- ============================================================ +-- V026: 데이터 보관기간 및 파기 정책 (DAR-10) 메뉴 추가 +-- 시스템관리 > 감사·보안 서브그룹 +-- ============================================================ + +-- 1. 권한 트리 노드 등록 +INSERT INTO kcg.auth_perm_tree(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord) +VALUES ('admin:data-retention', 'admin', '데이터 보관·파기', 1, 57) +ON CONFLICT (rsrc_cd) DO NOTHING; + +-- 2. 메뉴 메타데이터 갱신 +UPDATE kcg.auth_perm_tree +SET url_path = '/admin/data-retention', + label_key = 'nav.dataRetentionPolicy', + component_key = 'features/admin/DataRetentionPolicy', + nav_group = 'admin', + nav_sub_group = '감사·보안', + nav_sort = 2000, + labels = '{"ko":"데이터 보관·파기","en":"Data Retention"}' +WHERE rsrc_cd = 'admin:data-retention'; + +-- 3. ADMIN 역할에 전체 권한 부여 +INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn) +SELECT r.role_sn, 'admin:data-retention', op.oper_cd, 'Y' +FROM kcg.auth_role r +CROSS JOIN (VALUES ('READ'), ('CREATE'), ('UPDATE'), ('DELETE'), ('EXPORT')) AS op(oper_cd) +WHERE r.role_cd = 'ADMIN' +ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING; diff --git a/backend/src/main/resources/db/migration/V027__data_model_verification_menu.sql b/backend/src/main/resources/db/migration/V027__data_model_verification_menu.sql new file mode 100644 index 0000000..d4c268a --- /dev/null +++ b/backend/src/main/resources/db/migration/V027__data_model_verification_menu.sql @@ -0,0 +1,28 @@ +-- ============================================================ +-- V027: 데이터 모델 검증 (DAR-11) 메뉴 추가 +-- 시스템관리 > 감사·보안 서브그룹 +-- ============================================================ + +-- 1. 권한 트리 노드 등록 +INSERT INTO kcg.auth_perm_tree(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord) +VALUES ('admin:data-model-verification', 'admin', '데이터 모델 검증', 1, 58) +ON CONFLICT (rsrc_cd) DO NOTHING; + +-- 2. 메뉴 메타데이터 갱신 +UPDATE kcg.auth_perm_tree +SET url_path = '/admin/data-model-verification', + label_key = 'nav.dataModelVerification', + component_key = 'features/admin/DataModelVerification', + nav_group = 'admin', + nav_sub_group = '감사·보안', + nav_sort = 2100, + labels = '{"ko":"데이터 모델 검증","en":"Model Verification"}' +WHERE rsrc_cd = 'admin:data-model-verification'; + +-- 3. ADMIN 역할에 전체 권한 부여 +INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn) +SELECT r.role_sn, 'admin:data-model-verification', op.oper_cd, 'Y' +FROM kcg.auth_role r +CROSS JOIN (VALUES ('READ'), ('CREATE'), ('UPDATE'), ('DELETE'), ('EXPORT')) AS op(oper_cd) +WHERE r.role_cd = 'ADMIN' +ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING; diff --git a/frontend/public/dar03/bottom-trawl.png b/frontend/public/dar03/bottom-trawl.png new file mode 100644 index 0000000..54d0a09 Binary files /dev/null and b/frontend/public/dar03/bottom-trawl.png differ diff --git a/frontend/public/dar03/gillnet.png b/frontend/public/dar03/gillnet.png new file mode 100644 index 0000000..e00b6e9 Binary files /dev/null and b/frontend/public/dar03/gillnet.png differ diff --git a/frontend/public/dar03/pair-trawl.png b/frontend/public/dar03/pair-trawl.png new file mode 100644 index 0000000..1640710 Binary files /dev/null and b/frontend/public/dar03/pair-trawl.png differ diff --git a/frontend/public/dar03/pot-trap.png b/frontend/public/dar03/pot-trap.png new file mode 100644 index 0000000..e552661 Binary files /dev/null and b/frontend/public/dar03/pot-trap.png differ diff --git a/frontend/public/dar03/stow-net.png b/frontend/public/dar03/stow-net.png new file mode 100644 index 0000000..4e31f9e Binary files /dev/null and b/frontend/public/dar03/stow-net.png differ diff --git a/frontend/src/app/componentRegistry.ts b/frontend/src/app/componentRegistry.ts index caed5d4..204ef79 100644 --- a/frontend/src/app/componentRegistry.ts +++ b/frontend/src/app/componentRegistry.ts @@ -122,6 +122,12 @@ export const COMPONENT_REGISTRY: Record = { 'features/admin/AIAgentSecurityPage': lazy(() => import('@features/admin').then((m) => ({ default: m.AIAgentSecurityPage })), ), + 'features/admin/DataRetentionPolicy': lazy(() => + import('@features/admin').then((m) => ({ default: m.DataRetentionPolicy })), + ), + 'features/admin/DataModelVerification': lazy(() => + import('@features/admin').then((m) => ({ default: m.DataModelVerification })), + ), // ── 모선 워크플로우 ── 'features/parent-inference/ParentReview': lazy(() => import('@features/parent-inference/ParentReview').then((m) => ({ diff --git a/frontend/src/features/admin/DataModelVerification.tsx b/frontend/src/features/admin/DataModelVerification.tsx new file mode 100644 index 0000000..ef31c08 --- /dev/null +++ b/frontend/src/features/admin/DataModelVerification.tsx @@ -0,0 +1,378 @@ +import { useState } from 'react'; +import { Card, CardContent } from '@shared/components/ui/card'; +import { Badge } from '@shared/components/ui/badge'; +import { PageContainer, PageHeader } from '@shared/components/layout'; +import { getStatusIntent } from '@shared/constants/statusIntent'; +import { + Database, CheckCircle, AlertTriangle, FileText, Users, + Layers, Table2, Search, ChevronRight, GitBranch, + Shield, Eye, ListChecks, ClipboardCheck, +} from 'lucide-react'; + +/* + * DAR-11: 데이터 모델 검증 + * + * 설계된 데이터 모델을 설계자·개발자·해양경찰청·전문가 등이 참여하여 + * 각 단계별 데이터 모델 검증 시 기준을 정의·실시한다. + * + * ① 검증 현황 ② 논리 모델 검증 ③ 물리 모델 검증 ④ 중복·정합성 점검 ⑤ 검증 결과 이력 + */ + +type Tab = 'overview' | 'logical' | 'physical' | 'duplication' | 'history'; + +// ─── 검증 현황 KPI ────────────────── +const VERIFICATION_KPI = [ + { label: '전체 테이블', value: '48개', icon: Table2, color: '#3b82f6' }, + { label: '논리 모델 검증', value: '완료', icon: GitBranch, color: '#10b981' }, + { label: '물리 모델 검증', value: '완료', icon: Database, color: '#8b5cf6' }, + { label: '중복 테이블', value: '0건', icon: Search, color: '#06b6d4' }, + { label: '미해결 이슈', value: '1건', icon: AlertTriangle, color: '#f59e0b' }, +]; + +// ─── 검증 참여자 ────────────────── +const PARTICIPANTS = [ + { role: '데이터 설계자', name: '정해진 (주무관)', responsibility: '논리·물리 모델 설계, ERD 작성', badge: 'critical' as const }, + { role: '백엔드 개발자', name: '이상호 (경위)', responsibility: '마이그레이션 구현, 인덱스·성능 최적화', badge: 'info' as const }, + { role: '해양경찰청 담당관', name: '김영수 (사무관)', responsibility: '요구사항 대비 완전성 검토·승인', badge: 'warning' as const }, + { role: 'DB 전문가', name: '외부 자문위원', responsibility: '정규화·반정규화 타당성, 성능 검증', badge: 'success' as const }, +]; + +// ─── 검증 절차 (4단계) ────────────────── +const VERIFICATION_PHASES = [ + { phase: '① 검증 계획 수립', actions: ['검증 범위·기준·일정 확정', '참여자 역할 분담', '체크리스트 작성'], responsible: '데이터 설계자', icon: FileText }, + { phase: '② 논리 모델 검증', actions: ['요구사항 대비 완전성 확인', '엔티티·속성·관계 정합성', '정규화 수준 적정성 검토'], responsible: '설계자 + 담당관', icon: GitBranch }, + { phase: '③ 물리 모델 검증', actions: ['테이블·컬럼·인덱스 구조 확인', '데이터 타입·제약조건 적정성', '성능 관점 반정규화 타당성'], responsible: '개발자 + DB전문가', icon: Database }, + { phase: '④ 결과 보고·조치', actions: ['검증 결과서 작성', '미해결 이슈 추적·조치', '최종 승인 및 이력 등록'], responsible: '전체 참여자', icon: ClipboardCheck }, +]; + +// ─── 논리 모델 검증 항목 ────────────────── +const LOGICAL_CHECKS = [ + { category: '완전성', item: '요구사항 커버리지', desc: '48개 테이블이 SFR/DAR 전체 요구사항을 충족하는지 매핑 확인', result: '100% (48/48)', status: '통과' }, + { category: '완전성', item: '엔티티 누락 여부', desc: '식별된 비즈니스 영역(인증·탐지·단속·통계·관리) 대비 누락 엔티티 점검', result: '누락 0건', status: '통과' }, + { category: '정합성', item: '엔티티 관계 정의', desc: '외래키 관계, 참조 무결성, 카디널리티(1:N, M:N) 적정성', result: '78개 관계 확인', status: '통과' }, + { category: '정합성', item: '속성 정의 명확성', desc: '속성명 명명규칙, 도메인 정의, NULL 허용 정책 준수', result: '전체 준수', status: '통과' }, + { category: '정규화', item: '제3정규형 준수', desc: '함수 종속성 분석, 이행 종속 제거, 제3정규형(3NF) 이상 달성', result: '48/48 테이블', status: '통과' }, + { category: '정규화', item: '반정규화 타당성', desc: '성능 목적 반정규화 항목의 타당성 및 정합성 관리 방안', result: '3건 (타당)', status: '통과' }, + { category: '표준', item: '명명규칙 준수', desc: '테이블·컬럼·인덱스 명명규칙 (snake_case, kcg 스키마 접두어)', result: '전체 준수', status: '통과' }, + { category: '표준', item: '코드성 데이터 표준화', desc: '상태코드·유형코드 등 코드 마스터 테이블 일원화', result: 'code_master 통합', status: '통과' }, +]; + +// ─── 물리 모델 검증 항목 ────────────────── +const PHYSICAL_CHECKS = [ + { category: '구조', item: '테이블 스페이스 배치', desc: 'PostgreSQL kcg 스키마 내 48개 테이블 배치 적정성', result: '적정', status: '통과' }, + { category: '구조', item: '파티셔닝 전략', desc: 'AIS 위치 로그 등 대용량 테이블 파티셔닝 적용 여부', result: 'TimescaleDB hypertable', status: '통과' }, + { category: '데이터 타입', item: '컬럼 타입 적정성', desc: 'VARCHAR 길이, NUMERIC 정밀도, TIMESTAMP 타임존 설정', result: '전체 적정', status: '통과' }, + { category: '데이터 타입', item: 'JSON vs 정규 컬럼', desc: 'JSONB 사용 항목의 타당성 (스키마 유연성 vs 쿼리 성능)', result: '3개 테이블 JSONB (타당)', status: '통과' }, + { category: '인덱스', item: '기본키·외래키 인덱스', desc: 'PK/FK 인덱스 자동 생성 확인, 복합키 순서 적정성', result: '전체 생성 확인', status: '통과' }, + { category: '인덱스', item: '조회 성능 인덱스', desc: '고빈도 조회 패턴 분석 기반 추가 인덱스 설계', result: '12개 추가 인덱스', status: '통과' }, + { category: '제약조건', item: 'NOT NULL·CHECK·UNIQUE', desc: '필수값 제약, 범위 체크, 유일성 제약 적용 현황', result: '전체 적용', status: '통과' }, + { category: '제약조건', item: '참조 무결성 (FK)', desc: '외래키 ON DELETE/UPDATE 정책 (CASCADE/RESTRICT)', result: '정책 수립 완료', status: '통과' }, + { category: '성능', item: '쿼리 실행 계획 검증', desc: '주요 조회 쿼리 EXPLAIN ANALYZE 검증', result: 'P95 < 100ms', status: '통과' }, + { category: '성능', item: '대량 INSERT 성능', desc: 'AIS 5분 주기 배치 INSERT 성능 검증 (bulk insert)', result: '10K rows/sec', status: '주의' }, +]; + +// ─── 중복·정합성 점검 ────────────────── +const DUPLICATION_CHECKS = [ + { target: '테이블 중복', desc: '동일 목적의 테이블이 중복 존재하는지 점검', scope: '48개 테이블', result: '중복 0건', status: '통과' }, + { target: '컬럼 중복', desc: '동일 비즈니스 의미의 컬럼이 다른 이름으로 존재하는지 점검', scope: '전체 컬럼', result: '중복 0건', status: '통과' }, + { target: '반정규화 정합성', desc: '반정규화로 인한 중복 데이터의 동기화 방안 확인', scope: '3개 반정규화 항목', result: '트리거 기반 동기화', status: '통과' }, + { target: '코드값 일관성', desc: '상태코드·유형코드가 code_master 통해 일원 관리되는지 확인', scope: '19개 코드 카탈로그', result: '전체 일원화', status: '통과' }, + { target: '외래키 정합성', desc: '참조 대상 레코드 삭제 시 고아 레코드 발생 여부 점검', scope: '78개 FK 관계', result: '고아 0건', status: '통과' }, + { target: '스키마 간 정합성', desc: 'prediction 분석 결과 → kcgaidb 연계 시 데이터 타입 일치', scope: 'SNPDB ↔ kcgaidb', result: '타입 일치 확인', status: '통과' }, +]; + +// ─── 데이터 주제영역 ────────────────── +const SUBJECT_AREAS = [ + { area: '인증·권한', tables: 'auth_user, auth_role, auth_perm_tree, auth_perm, auth_user_role', count: 5, desc: '사용자·역할·권한 트리 기반 RBAC' }, + { area: 'AIS·선박', tables: 'vessel_master, ais_position, vessel_permit', count: 3, desc: 'AIS 수신 데이터, 선박 기본정보, 허가 현황' }, + { area: '탐지·분석', tables: 'vessel_analysis, dark_vessel, gear_detection, transship_detection, ...', count: 12, desc: '14개 알고리즘 분석 결과 저장' }, + { area: '단속·이벤트', tables: 'prediction_event, enforcement_plan, enforcement_record, ...', count: 8, desc: '이벤트 발생·단속 계획·단속 이력' }, + { area: '모선 워크플로우', tables: 'parent_review, parent_exclusion, label_session, ...', count: 5, desc: '모선 확정·제외·학습 워크플로우' }, + { area: '순찰·함정', tables: 'patrol_ship, patrol_route, fleet_optimization, ...', count: 6, desc: '함정 관리·순찰 경로·최적화' }, + { area: '통계·감사', tables: 'statistics_daily, audit_log, access_log, login_history, ...', count: 5, desc: '통계 집계·감사 로그·접근 이력' }, + { area: '시스템·관리', tables: 'code_master, zone_polygon, gear_type_master, notice, ...', count: 4, desc: '코드 마스터·구역 정보·공지사항' }, +]; + +// ─── 검증 결과 이력 ────────────────── +const VERIFICATION_HISTORY = [ + { id: 'VER-2026-005', date: '2026-04-10', phase: '물리 모델 검증', reviewer: '이상호, 외부 자문', target: 'V015 NUMERIC 정밀도 조정', issues: 0, result: '통과' }, + { id: 'VER-2026-004', date: '2026-04-07', phase: '중복·정합성 점검', reviewer: '정해진', target: '반정규화 3건 정합성', issues: 0, result: '통과' }, + { id: 'VER-2026-003', date: '2026-04-03', phase: '논리 모델 검증', reviewer: '김영수, 정해진', target: 'V014 함정·예측 테이블 추가', issues: 1, result: '조건부 통과' }, + { id: 'VER-2026-002', date: '2026-03-28', phase: '물리 모델 검증', reviewer: '이상호', target: 'V012~V013 이벤트·단속 테이블', issues: 0, result: '통과' }, + { id: 'VER-2026-001', date: '2026-03-20', phase: '논리 모델 검증', reviewer: '전체 참여', target: '초기 V001~V011 전체 구조', issues: 3, result: '조건부 통과' }, + { id: 'VER-2025-012', date: '2025-12-15', phase: '검증 계획 수립', reviewer: '김영수', target: '검증 계획서 v1.0 확정', issues: 0, result: '승인' }, +]; + +export function DataModelVerification() { + const [tab, setTab] = useState('overview'); + + return ( + + + + {/* 탭 */} +
+ {([ + { key: 'overview' as Tab, icon: Eye, label: '검증 현황' }, + { key: 'logical' as Tab, icon: GitBranch, label: '논리 모델 검증' }, + { key: 'physical' as Tab, icon: Database, label: '물리 모델 검증' }, + { key: 'duplication' as Tab, icon: Search, label: '중복·정합성 점검' }, + { key: 'history' as Tab, icon: FileText, label: '검증 결과 이력' }, + ]).map(t => ( + + ))} +
+ + {/* ── ① 검증 현황 ── */} + {tab === 'overview' && ( +
+
+ {VERIFICATION_KPI.map(k => ( +
+ +
+
{k.value}
+
{k.label}
+
+
+ ))} +
+ + {/* 검증 절차 */} + +
+ + 검증 절차 (4단계) +
+
+ {VERIFICATION_PHASES.map((s, i) => ( +
+ {i < VERIFICATION_PHASES.length - 1 && ( + + )} +
+
+ + {s.phase} +
+
{s.responsible}
+
    + {s.actions.map(a => ( +
  • + + {a} +
  • + ))} +
+
+
+ ))} +
+
+ + {/* 참여자 + 주제영역 */} +
+ +
+ + 검증 참여자 +
+
+ {PARTICIPANTS.map(p => ( +
+ {p.role} +
+
{p.name}
+
{p.responsibility}
+
+
+ ))} +
+
+ +
+ + 데이터 주제영역 ({SUBJECT_AREAS.reduce((s, a) => s + a.count, 0)}개 테이블) +
+
+ {SUBJECT_AREAS.map(a => ( +
+ {a.count}개 + {a.area} + {a.desc} +
+ ))} +
+
+
+
+ )} + + {/* ── ② 논리 모델 검증 ── */} + {tab === 'logical' && ( +
+ +
+ + 논리 데이터 모델 검증 기준 및 결과 + {LOGICAL_CHECKS.filter(c => c.status === '통과').length}/{LOGICAL_CHECKS.length} 통과 +
+ + + + + + + + + + + + {LOGICAL_CHECKS.map(c => ( + + + + + + + + ))} + +
분류검증 항목상세 기준검증 결과판정
{c.category}{c.item}{c.desc}{c.result}{c.status}
+
+ + +
데이터 주제영역별 엔티티 구성
+
+ {SUBJECT_AREAS.map(a => ( +
+
+ {a.area} + {a.count}개 +
+
{a.desc}
+
{a.tables}
+
+ ))} +
+
+
+ )} + + {/* ── ③ 물리 모델 검증 ── */} + {tab === 'physical' && ( + +
+ + 물리 데이터 모델 검증 기준 및 결과 + {PHYSICAL_CHECKS.filter(c => c.status === '통과').length}/{PHYSICAL_CHECKS.length} 통과 + {PHYSICAL_CHECKS.some(c => c.status === '주의') && ( + {PHYSICAL_CHECKS.filter(c => c.status === '주의').length}건 주의 + )} +
+ + + + + + + + + + + + {PHYSICAL_CHECKS.map(c => ( + + + + + + + + ))} + +
분류검증 항목상세 기준검증 결과판정
{c.category}{c.item}{c.desc}{c.result}{c.status}
+
+ )} + + {/* ── ④ 중복·정합성 점검 ── */} + {tab === 'duplication' && ( + +
+ + 중복 테이블·컬럼 및 데이터 정합성 점검 + {DUPLICATION_CHECKS.filter(c => c.status === '통과').length}/{DUPLICATION_CHECKS.length} 통과 +
+ + + + + + + + + + + + {DUPLICATION_CHECKS.map(c => ( + + + + + + + + ))} + +
점검 항목상세 내용점검 범위결과판정
{c.target}{c.desc}{c.scope}{c.result}{c.status}
+
+ )} + + {/* ── ⑤ 검증 결과 이력 ── */} + {tab === 'history' && ( + +
+ + 검증 결과 이력 + {VERIFICATION_HISTORY.length}건 +
+ + + + + + + + + + + + + + {VERIFICATION_HISTORY.map(h => ( + + + + + + + + + + ))} + +
검증 ID일시단계검증자대상이슈결과
{h.id}{h.date}{h.phase}{h.reviewer}{h.target}{h.issues > 0 ? {h.issues}건 : 0건}{h.result}
+
+ )} +
+ ); +} diff --git a/frontend/src/features/admin/DataRetentionPolicy.tsx b/frontend/src/features/admin/DataRetentionPolicy.tsx new file mode 100644 index 0000000..3858b00 --- /dev/null +++ b/frontend/src/features/admin/DataRetentionPolicy.tsx @@ -0,0 +1,432 @@ +import { useState } from 'react'; +import { Card, CardContent } from '@shared/components/ui/card'; +import { Badge } from '@shared/components/ui/badge'; +import { PageContainer, PageHeader } from '@shared/components/layout'; +import { getStatusIntent } from '@shared/constants/statusIntent'; +import { + Database, Clock, Trash2, ShieldCheck, FileText, AlertTriangle, + CheckCircle, Archive, CalendarClock, UserCheck, Search, + ChevronRight, Lock, Eye, Settings, +} from 'lucide-react'; + +/* + * DAR-10: 데이터 보관기간 및 파기 정책 + * + * 법령 내 규정 및 유관기관 협의에 따라 데이터 유형별 보관기간 및 + * 파기 절차를 수립하고 체계적으로 관리하는 정책 페이지. + * + * ① 보관 현황 ② 유형별 보관기간 ③ 파기 절차 ④ 예외·연장 ⑤ 파기 감사 대장 + */ + +type Tab = 'overview' | 'retention' | 'disposal' | 'exception' | 'audit'; + +// ─── 보관 현황 KPI ────────────────── +const RETENTION_KPI = [ + { label: '관리 데이터 유형', value: '6종', icon: Database, color: '#3b82f6' }, + { label: '보관기간 초과', value: '0건', icon: Clock, color: '#10b981' }, + { label: '파기 대기', value: '3건', icon: Trash2, color: '#f59e0b' }, + { label: '보존 연장 중', value: '1건', icon: ShieldCheck, color: '#8b5cf6' }, + { label: '금월 파기 완료', value: '12건', icon: CheckCircle, color: '#06b6d4' }, +]; + +// ─── 유형별 보관기간 기준표 ────────────────── +const RETENTION_TABLE = [ + { type: '선박 위치 로그 (AIS)', category: '운항 데이터', basis: '해사안전법 시행규칙 제42조', period: '5년', format: 'PostgreSQL + TimescaleDB', volume: '약 2.1TB/년', status: '정상' }, + { type: '단속 자료', category: '법집행 기록', basis: '해양경비법 제18조, 공공기록물법', period: '10년 (영구 가능)', format: 'PostgreSQL + 파일 스토리지', volume: '약 150GB/년', status: '정상' }, + { type: '수사 관련 자료', category: '수사 기록', basis: '형사소송법 제198조, 수사기록 보존규칙', period: '영구 (종결 후 30년)', format: '암호화 스토리지', volume: '약 50GB/년', status: '정상' }, + { type: 'AI 학습용 임시 데이터', category: 'AI 학습 데이터', basis: '개인정보보호법 제21조', period: '학습 완료 후 90일', format: 'S3 + DVC', volume: '약 500GB/주기', status: '정상' }, + { type: 'CCTV·영상 증거', category: '영상 데이터', basis: '개인정보보호법 제25조', period: '30일 (증거 채택 시 영구)', format: 'NAS + HLS', volume: '약 3TB/월', status: '주의' }, + { type: '시스템 접근 로그', category: '감사 로그', basis: '정보통신망법 제45조, 전자금융감독규정', period: '5년', format: 'Elasticsearch', volume: '약 80GB/년', status: '정상' }, +]; + +// ─── 파기 방식 정의 ────────────────── +const DISPOSAL_METHODS = [ + { method: '완전 삭제 (Secure Erase)', desc: 'DoD 5220.22-M 기준 3회 덮어쓰기 후 삭제', target: 'DB 레코드, 파일', encryption: '해당 없음', recovery: '복구 불가능', status: '적용' }, + { method: '암호화 키 폐기', desc: '암호화된 데이터의 복호화 키를 영구 폐기하여 접근 차단', target: '암호화 스토리지', encryption: 'AES-256 키 폐기', recovery: '복구 불가능', status: '적용' }, + { method: '논리적 삭제 + 만료', desc: 'soft-delete 마킹 후 보관기간 만료 시 물리 삭제 전환', target: '운영 DB', encryption: '-', recovery: '만료 전 복구 가능', status: '적용' }, + { method: '물리적 파기', desc: '디가우저(Degausser) 또는 물리적 파쇄로 매체 파기', target: '이동식 매체, 하드디스크', encryption: '해당 없음', recovery: '복구 불가능', status: '적용' }, +]; + +// ─── 파기 승인 절차 ────────────────── +const DISPOSAL_WORKFLOW = [ + { phase: '① 파기 대상 선별', actions: ['보관기간 만료 데이터 자동 탐색', '유형별 파기 대상 목록 생성', '백업 데이터 포함 여부 확인'], responsible: '시스템 자동', icon: Search }, + { phase: '② 파기 신청', actions: ['파기 대상 목록 검토 및 승인 요청', '수사·소송 보존 연장 대상 제외 확인', '파기 방식 지정 (완전삭제/키폐기/물리파기)'], responsible: '데이터 관리자', icon: FileText }, + { phase: '③ 승인 및 집행', actions: ['보안담당관 파기 승인', '파기 실행 (이중 확인 절차)', '백업 데이터 동시 파기'], responsible: '보안담당관', icon: UserCheck }, + { phase: '④ 결과 기록', actions: ['파기 결과 로그 자동 기록', '파기 대장 등록 (대상·일시·담당자·방식)', '감사 보고서 생성 및 보관'], responsible: '시스템 자동', icon: Archive }, +]; + +// ─── 보존 연장 예외 현황 ────────────────── +const EXCEPTIONS = [ + { id: 'EXC-2026-001', dataType: '단속 자료 #2024-1892', reason: '수사 진행 중 (인천해경)', originalExpiry: '2026-03-15', extendedTo: '수사 종결 시까지', approver: '수사과장', status: '연장 중' }, + { id: 'EXC-2026-002', dataType: 'AIS 로그 (2021-Q2)', reason: '재판 증거 제출 (서울중앙지법)', originalExpiry: '2026-06-30', extendedTo: '판결 확정 시까지', approver: '법무담당관', status: '연장 중' }, + { id: 'EXC-2025-015', dataType: 'CCTV 영상 #V-2025-0342', reason: '감사원 감사 대상', originalExpiry: '2025-12-01', extendedTo: '2026-06-30', approver: '감사담당관', status: '해제 예정' }, +]; + +const EXCEPTION_RULES = [ + { rule: '수사·소송 보존', desc: '수사 개시 또는 소송 진행 중인 데이터는 종결 시까지 파기 유예', authority: '수사과장 / 법무담당관' }, + { rule: '감사 보존', desc: '내부·외부 감사 대상 데이터는 감사 완료 후 6개월까지 보존 연장', authority: '감사담당관' }, + { rule: '재난·사고 보존', desc: '해양 사고 관련 데이터는 사고 조사 종결 시까지 보존', authority: '안전관리관' }, + { rule: '정보공개 청구', desc: '정보공개 청구 접수된 데이터는 처리 완료 시까지 보존', authority: '정보공개담당관' }, +]; + +// ─── 파기 감사 대장 ────────────────── +const DISPOSAL_AUDIT_LOG = [ + { id: 'DSP-2026-012', date: '2026-04-10', target: 'AI 학습 임시 데이터 (배치 #B-0392)', type: 'AI 학습 데이터', method: '완전 삭제', volume: '48.2GB', operator: '정해진', approver: '김영수', result: '완료' }, + { id: 'DSP-2026-011', date: '2026-04-08', target: 'CCTV 영상 2026-03월분 (미채택)', type: '영상 데이터', method: '완전 삭제', volume: '2.8TB', operator: '시스템', approver: '김영수', result: '완료' }, + { id: 'DSP-2026-010', date: '2026-04-05', target: '시스템 접근 로그 (2021-Q1)', type: '감사 로그', method: '완전 삭제', volume: '12.5GB', operator: '시스템', approver: '김영수', result: '완료' }, + { id: 'DSP-2026-009', date: '2026-04-03', target: 'AI 학습 임시 데이터 (배치 #B-0391)', type: 'AI 학습 데이터', method: '암호화 키 폐기', volume: '51.7GB', operator: '정해진', approver: '김영수', result: '완료' }, + { id: 'DSP-2026-008', date: '2026-04-01', target: 'AIS 위치 로그 (2021-03)', type: '운항 데이터', method: '완전 삭제', volume: '180GB', operator: '시스템', approver: '이상호', result: '완료' }, + { id: 'DSP-2026-007', date: '2026-03-28', target: '이동식 매체 (USB-0021~0025)', type: '물리 매체', method: '물리적 파기', volume: '5개', operator: '박민수', approver: '김영수', result: '완료' }, +]; + +// ─── 보관 구조 요약 ────────────────── +const STORAGE_ARCHITECTURE = [ + { tier: '운영 스토리지', desc: '실시간 조회·분석 대상 (최근 1년)', tech: 'PostgreSQL + TimescaleDB', encryption: 'TDE (AES-256)', backup: '일일 증분 + 주간 전체', icon: Database }, + { tier: '아카이브 스토리지', desc: '장기 보관 대상 (1~10년)', tech: 'S3 Compatible (Glacier 등급)', encryption: 'SSE-KMS', backup: '월간 무결성 검증', icon: Archive }, + { tier: '백업 스토리지', desc: '재해 복구용 (이중화)', tech: '원격지 NAS + 테이프', encryption: 'AES-256', backup: '분기별 복구 테스트', icon: Lock }, + { tier: '파기 대기 영역', desc: 'soft-delete 후 파기 승인 대기', tech: '격리 스토리지 (접근 제한)', encryption: 'AES-256', backup: '미백업 (파기 예정)', icon: Trash2 }, +]; + +export function DataRetentionPolicy() { + const [tab, setTab] = useState('overview'); + + return ( + + + + {/* 탭 */} +
+ {([ + { key: 'overview' as Tab, icon: Eye, label: '보관 현황' }, + { key: 'retention' as Tab, icon: CalendarClock, label: '유형별 보관기간' }, + { key: 'disposal' as Tab, icon: Trash2, label: '파기 절차' }, + { key: 'exception' as Tab, icon: ShieldCheck, label: '예외·연장' }, + { key: 'audit' as Tab, icon: FileText, label: '파기 감사 대장' }, + ]).map(t => ( + + ))} +
+ + {/* ── ① 보관 현황 ── */} + {tab === 'overview' && ( +
+
+ {RETENTION_KPI.map(k => ( +
+ +
+
{k.value}
+
{k.label}
+
+
+ ))} +
+ + {/* 보관 구조 */} + +
+ + 전체 보관 구조 (4-Tier) +
+
+ {STORAGE_ARCHITECTURE.map(s => ( +
+
+ + {s.tier} +
+

{s.desc}

+
+ {[ + ['기술', s.tech], + ['암호화', s.encryption], + ['백업', s.backup], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+ ))} +
+
+ + {/* 정책 준수 현황 */} +
+ +
정책 준수 점검
+
+ {[ + ['유형별 보관기간 기준표', '6/6종 수립 완료', '완료'], + ['파기 방식 정의', '4/4 방식 적용', '완료'], + ['파기 승인 절차', '4단계 절차 운영 중', '완료'], + ['보존 연장 예외 관리', '3건 관리 중 (1건 해제 예정)', '정상'], + ['백업 데이터 동시 파기', '파기 시 백업 포함 확인', '완료'], + ['파기 감사 대장', '12건 기록 (금월)', '완료'], + ['CCTV 30일 보관 준수', '미채택 영상 30일 초과 1건', '주의'], + ].map(([k, v, s]) => ( +
+ {s === '완료' || s === '정상' ? : } + {k} + {v} +
+ ))} +
+
+ +
최근 파기 이력
+
+ {DISPOSAL_AUDIT_LOG.slice(0, 6).map(d => ( +
+ {d.date} + {d.target} + {d.method} + {d.volume} +
+ ))} +
+
+
+
+ )} + + {/* ── ② 유형별 보관기간 ── */} + {tab === 'retention' && ( +
+ +
+ + 데이터 유형별 보관기간 기준표 + {RETENTION_TABLE.length}종 관리 +
+ + + + + + + + + + + + + + {RETENTION_TABLE.map(r => ( + + + + + + + + + + ))} + +
데이터 유형분류법적 근거보관기간저장 형식연간 용량상태
{r.type}{r.category}{r.basis}{r.period}{r.format}{r.volume}{r.status}
+
+ + +
관련 법령 요약
+
+ {[ + { law: '해사안전법 시행규칙', article: '제42조', content: 'AIS 장치 기록 보존 의무 (5년)' }, + { law: '해양경비법', article: '제18조', content: '단속 기록 작성·보존 의무' }, + { law: '공공기록물 관리에 관한 법률', article: '제19조', content: '기록물 보존기간 준수 의무' }, + { law: '개인정보보호법', article: '제21조', content: '목적 달성 후 지체 없이 파기' }, + { law: '정보통신망법', article: '제45조', content: '접속기록 5년 보관 의무' }, + { law: '형사소송법', article: '제198조', content: '수사기록 보존 의무' }, + ].map(l => ( +
+
{l.law}
+ {l.article} +
{l.content}
+
+ ))} +
+
+
+ )} + + {/* ── ③ 파기 절차 ── */} + {tab === 'disposal' && ( +
+ {/* 파기 승인 워크플로우 */} + +
+ + 파기 승인 절차 (4단계) +
+
+ {DISPOSAL_WORKFLOW.map((s, i) => ( +
+ {i < DISPOSAL_WORKFLOW.length - 1 && ( + + )} +
+
+ + {s.phase} +
+
{s.responsible}
+
    + {s.actions.map(a => ( +
  • + + {a} +
  • + ))} +
+
+
+ ))} +
+
+ + {/* 파기 방식 */} + +
+ + 파기 방식 정의 +
+ + + + + + + + + + + + + {DISPOSAL_METHODS.map(m => ( + + + + + + + + + ))} + +
파기 방식설명적용 대상암호화복구 가능성상태
{m.method}{m.desc}{m.target}{m.encryption}{m.recovery}{m.status}
+
+
+ )} + + {/* ── ④ 예외·연장 ── */} + {tab === 'exception' && ( +
+ +
+ + 보존 연장 예외 현황 + {EXCEPTIONS.filter(e => e.status === '연장 중').length}건 연장 중 +
+ + + + + + + + + + + + + + {EXCEPTIONS.map(e => ( + + + + + + + + + + ))} + +
예외 ID대상 데이터사유원래 만료연장 기한승인자상태
{e.id}{e.dataType}{e.reason}{e.originalExpiry}{e.extendedTo}{e.approver}{e.status}
+
+ + +
+ + 보존 연장 사유 유형 +
+
+ {EXCEPTION_RULES.map(r => ( +
+ +
+
{r.rule}
+
{r.desc}
+
+ {r.authority} +
+ ))} +
+
+
+ )} + + {/* ── ⑤ 파기 감사 대장 ── */} + {tab === 'audit' && ( + +
+ + 파기 감사 대장 + {DISPOSAL_AUDIT_LOG.length}건 +
+ + + + + + + + + + + + + + + + {DISPOSAL_AUDIT_LOG.map(d => ( + + + + + + + + + + + + ))} + +
파기 ID일시파기 대상유형방식용량수행자승인자결과
{d.id}{d.date}{d.target}{d.type}{d.method}{d.volume}{d.operator}{d.approver}{d.result}
+
+ )} +
+ ); +} diff --git a/frontend/src/features/admin/index.ts b/frontend/src/features/admin/index.ts index 9eb51ae..d435fec 100644 --- a/frontend/src/features/admin/index.ts +++ b/frontend/src/features/admin/index.ts @@ -5,3 +5,5 @@ export { AdminPanel } from './AdminPanel'; export { DataHub } from './DataHub'; export { AISecurityPage } from './AISecurityPage'; export { AIAgentSecurityPage } from './AIAgentSecurityPage'; +export { DataRetentionPolicy } from './DataRetentionPolicy'; +export { DataModelVerification } from './DataModelVerification'; diff --git a/frontend/src/features/ai-operations/AIModelManagement.tsx b/frontend/src/features/ai-operations/AIModelManagement.tsx index c28ce2b..29a6a82 100644 --- a/frontend/src/features/ai-operations/AIModelManagement.tsx +++ b/frontend/src/features/ai-operations/AIModelManagement.tsx @@ -197,6 +197,149 @@ const GEAR_PROFILES = [ features: [{ k: 'High Speed', v: '>7 kt' }, { k: 'Circularity', v: 'High' }, { k: 'Speed Trans.', v: 'High' }, { k: 'Fleet', v: 'High' }] }, ]; +// ─── DAR-03 5종 어구 구조 비교 (FAO ISSCFG) ────────── + +interface DAR03GearSummary { + no: string; + name: string; + faoCode: string; + mesh: string; + iuuRisk: '매우 높음' | '높음' | '중간' | '낮음~중간'; + aisType: string; + gCodes: string; +} + +const DAR03_GEAR_SUMMARY: DAR03GearSummary[] = [ + { no: '①', name: '저층 트롤', faoCode: 'OTB/TBB', mesh: '≥60mm', iuuRisk: '높음', aisType: '어선 AIS', gCodes: 'G-01, G-03' }, + { no: '②', name: '쌍끌이 트롤', faoCode: 'PTM', mesh: '≥56mm', iuuRisk: '매우 높음', aisType: '어선 AIS 2척', gCodes: 'G-02, G-06' }, + { no: '③', name: '스토우넷', faoCode: 'FYK', mesh: '≥55mm', iuuRisk: '중간', aisType: '어구 AIS 부표', gCodes: 'G-01, G-04, G-05' }, + { no: '④', name: '자망', faoCode: 'GNS/GND', mesh: '55~144mm', iuuRisk: '낮음~중간', aisType: '어구 AIS 부표', gCodes: 'G-03, G-05' }, + { no: '⑤', name: '통발·함정', faoCode: 'FPO', mesh: '탈출구 Ø≥8cm', iuuRisk: '중간', aisType: '어구 AIS 부표', gCodes: 'G-01, G-04' }, +]; + +const DAR03_IUU_INTENT: Record = { + '매우 높음': 'critical', + '높음': 'high', + '중간': 'warning', + '낮음~중간': 'info', +}; + +interface DAR03GearDetail { + no: string; + name: string; + nameEn: string; + image: string; + specs: { k: string; v: string }[]; + gCodes: { code: string; desc: string }[]; +} + +const DAR03_GEAR_DETAILS: DAR03GearDetail[] = [ + { + no: '①', name: '저층 트롤', nameEn: 'Bottom Trawl (OTB/TBB)', image: '/dar03/bottom-trawl.png', + specs: [ + { k: 'FAO 코드', v: 'OTB / TBB' }, + { k: '최소 망목', v: '≥ 60mm (마름모형)' }, + { k: '주요 어종', v: '참조기 · 갈치' }, + { k: '조업 속력', v: '2.5~4.5 knot' }, + { k: '항적 패턴', v: 'U형 회전 · 직선 왕복' }, + { k: 'AIS', v: '어선 AIS (어구 AIS 없음)' }, + ], + gCodes: [ + { code: 'G-01', desc: '허가 해역 외 트롤 → GIS 교차' }, + { code: 'G-03', desc: '미등록 어구 → label=1' }, + ], + }, + { + no: '②', name: '쌍끌이 중층 트롤', nameEn: 'Pair Midwater Trawl (PTM)', image: '/dar03/pair-trawl.png', + specs: [ + { k: 'FAO 코드', v: 'PTM' }, + { k: '최소 망목', v: '≥ 56mm' }, + { k: '주요 어종', v: '전갱이 · 고등어 · 참조기' }, + { k: '선박 간격', v: '300~500m 유지' }, + { k: '조업 속력', v: '2~4 knot (2척 동기화)' }, + { k: 'AIS', v: '2척 어선 AIS 동기화' }, + ], + gCodes: [ + { code: 'G-02', desc: '금어기 내 공조 조업 탐지' }, + { code: 'G-06', desc: '2척 동기화 2시간+ → 공조' }, + ], + }, + { + no: '③', name: '스토우넷 (안강망)', nameEn: 'Stow Net (FYK)', image: '/dar03/stow-net.png', + specs: [ + { k: 'FAO 코드', v: 'FYK' }, + { k: '최소 망목', v: '≥ 55mm (캔버스형)' }, + { k: '주요 어종', v: '참조기 · 갈치 · 실치' }, + { k: '설치 방식', v: '말뚝·닻으로 고정' }, + { k: 'AIS', v: '어구 AIS 부표 부착 의무' }, + { k: '탐지 지표', v: '위치 이탈·출현·소실 주기' }, + ], + gCodes: [ + { code: 'G-01', desc: '위치 편차 200m+ → 구역 외' }, + { code: 'G-04', desc: '신호 30분 내 반복 → MMSI 조작' }, + { code: 'G-05', desc: '이동 500m+ → 인위적 이동' }, + ], + }, + { + no: '④', name: '자망', nameEn: 'Gillnet (GNS/GND)', image: '/dar03/gillnet.png', + specs: [ + { k: 'FAO 코드', v: 'GNS / GND' }, + { k: '최소 망목', v: '55~144mm (어종별 상이)' }, + { k: '참조기 기준', v: '55mm (황해)' }, + { k: '은돔 기준', v: '100mm' }, + { k: 'AIS', v: '어구 AIS 부표 부착' }, + { k: '탐지 지표', v: '미등록 여부·기간 이탈' }, + ], + gCodes: [ + { code: 'G-02', desc: '금어기 내 신호 출현 → label=1' }, + { code: 'G-03', desc: '등록DB 미매칭 → 불법 자망' }, + { code: 'G-05', desc: '조류 보정 후 500m+ → 이동' }, + ], + }, + { + no: '⑤', name: '통발 · 함정', nameEn: 'Pot / Trap (FPO)', image: '/dar03/pot-trap.png', + specs: [ + { k: 'FAO 코드', v: 'FPO' }, + { k: '탈출구 (꽃게)', v: 'Ø ≥ 8cm 또는 높이 33mm' }, + { k: '탈출구 (참게)', v: '측면 30mm + 말단 7cm' }, + { k: '주요 어종', v: '꽃게 · 참게 · 장어' }, + { k: '미성어 방류율', v: '95% 이상 (탈출구 적용 시)' }, + { k: 'AIS', v: '어구 AIS 부표 부착' }, + ], + gCodes: [ + { code: 'G-01', desc: '허가 구역 외 설치 → GIS 교차' }, + { code: 'G-04', desc: '어선-어구 출현·소실 60분+ 불일치' }, + ], + }, +]; + +interface DAR03AisSignal { + no: string; + name: string; + aisType: string; + normal: string[]; + threshold: string[]; + gCodes: string; +} + +const DAR03_AIS_SIGNALS: DAR03AisSignal[] = [ + { no: '①', name: '저층 트롤', aisType: '어선 AIS (Class-A)', + normal: ['2.5~4.5 knot', 'U형 항적 반복'], + threshold: ['5 knot 이상 급가속', '금지 해역 진입'], gCodes: 'G-01, G-03' }, + { no: '②', name: '쌍끌이 트롤', aisType: '어선 AIS 2척', + normal: ['2~4 knot 동기화', '500m 간격 유지'], + threshold: ['동기화 2시간 이상', '동시 AIS 차단 30분+'], gCodes: 'G-02, G-06' }, + { no: '③', name: '스토우넷', aisType: '어구 AIS (Class-B)', + normal: ['위치 완전 고정', '신호 지속 출현'], + threshold: ['위치 편차 200m+', '출현·소실 30분 이내 반복'], gCodes: 'G-01, G-04, G-05' }, + { no: '④', name: '자망', aisType: '어구 AIS (Class-B)', + normal: ['위치 반고정', '조류에 따라 완만이동'], + threshold: ['등록 DB 미매칭', '금어기 내 신호 출현'], gCodes: 'G-02, G-03' }, + { no: '⑤', name: '통발', aisType: '어구 AIS (Class-B)', + normal: ['위치 완전 고정', '신호 지속'], + threshold: ['어선 접근·이탈 불일치 60분+', '구역 외 위치'], gCodes: 'G-01, G-04' }, +]; + // ─── ⑦ 7대 탐지 엔진 (불법조업 감시 알고리즘 v4.0) ─── interface DetectionEngine { @@ -601,6 +744,166 @@ export function AIModelManagement() { ))} + + {/* ── DAR-03 5종 어구 구조 비교 ── */} +
+ +
+
DAR-03 · 5종 어구 구조 비교 (FAO ISSCFG)
+
+ 불법 어망·어구 탐지 참고자료 — FAO 국제 어구 분류 기준 · Wang et al.(2022) 논문 기반 · G-01~G-06 탐지 코드 연계 +
+
+ 참고자료 +
+ + {/* 5종 어구 특성 비교 요약 */} + + + + + 5종 어구 특성 비교 요약 + + + + + + + + + + + + + + + + {DAR03_GEAR_SUMMARY.map((g) => ( + + + + + + + + + ))} + +
어구FAO 코드최소 망목IUU 위험도AIS 부착주요 G코드
+ {g.no} + {g.name} + {g.faoCode}{g.mesh} + {g.iuuRisk} + {g.aisType}{g.gCodes}
+
+
+ + {/* 어구별 구조 도식 */} + + + + + 어구별 구조 도식 비교 + +

+ ※ FAO 어구 분류 기준 및 Wang et al.(2022) 논문 기반 개념도. 임계값은 사업 착수 후 해양경찰청 실무 데이터 분석으로 최종 확정. +

+
+ +
+ {DAR03_GEAR_DETAILS.map((g) => ( + + +
+ {g.no} +
+
{g.name}
+
{g.nameEn}
+
+
+
+ {g.nameEn} +
+
+ {g.specs.map((s) => ( +
+ {s.k} + {s.v} +
+ ))} +
+
+
G코드 연계
+
+ {g.gCodes.map((gc) => ( +
+ {gc.code} + {gc.desc} +
+ ))} +
+
+
+
+ ))} +
+
+
+ + {/* AIS 신호 특성 및 이상 판정 기준 */} + + + + + 어구별 AIS 신호 특성 및 이상 판정 기준 + + + + + + + + + + + + + + + {DAR03_AIS_SIGNALS.map((s) => ( + + + + + + + + ))} + +
어구AIS 유형정상 신호 특성이상 탐지 임계값G코드
+ {s.no} + {s.name} + {s.aisType} +
    + {s.normal.map((n) => ( +
  • + + {n} +
  • + ))} +
+
+
    + {s.threshold.map((th) => ( +
  • + + {th} +
  • + ))} +
+
{s.gCodes}
+
+
)} diff --git a/frontend/src/features/risk-assessment/EnforcementPlan.tsx b/frontend/src/features/risk-assessment/EnforcementPlan.tsx index b6bae6c..4211262 100644 --- a/frontend/src/features/risk-assessment/EnforcementPlan.tsx +++ b/frontend/src/features/risk-assessment/EnforcementPlan.tsx @@ -7,7 +7,11 @@ import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { getRiskIntent, getStatusIntent } from '@shared/constants/statusIntent'; import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels'; -import { Shield, AlertTriangle, Ship, Plus, Calendar, Users, Loader2 } from 'lucide-react'; +import { + Shield, AlertTriangle, Ship, Plus, Calendar, Users, Loader2, + Navigation, Anchor, Radio, Target, Clock, Compass, Eye, + MapPin, Zap, ChevronRight, CheckCircle, Activity, +} from 'lucide-react'; import { BaseMap, STATIC_LAYERS, createMarkerLayer, createRadiusLayer, useMapLayers, type MapHandle } from '@lib/map'; import type { MarkerData } from '@lib/map'; import { getEnforcementPlans, type EnforcementPlan as EnforcementPlanApi } from '@/services/enforcement'; @@ -17,6 +21,64 @@ import { useSettingsStore } from '@stores/settingsStore'; /* SFR-06: 단속 계획·경보 연계(단속 우선지역 예보) */ +type PlanTab = 'overview' | 'single' | 'multi'; + +// ─── 단일 함정 작전 데이터 ────────────────── + +const SINGLE_OP_TYPES = [ + { type: '정찰 순찰', desc: '특정 구역 내 불법조업 의심 선박 탐색·확인', duration: '4~8시간', crew: '8~12명', risk: '중간', icon: Eye }, + { type: '긴급 출동', desc: 'CRITICAL 경보 발생 시 즉시 현장 투입', duration: '1~3시간', crew: '10~15명', risk: '높음', icon: Zap }, + { type: '감시 초계', desc: '고위험 해역 정기 순찰 및 AIS 모니터링', duration: '6~12시간', crew: '8~10명', risk: '낮음', icon: Compass }, + { type: '근접 차단', desc: '도주 의심 선박 접근·정선명령·검문검색', duration: '2~4시간', crew: '12~18명', risk: '높음', icon: Target }, +]; + +const SINGLE_SCENARIOS = [ + { phase: '① 출항 전', actions: ['작전 브리핑 (위협 분석·해상상태)', '장비 점검 (레이더·AIS·카메라·무선)', '인력 배치 및 역할 분담'], time: '출항 60분 전' }, + { phase: '② 이동·접근', actions: ['최적 경로 항해 (연료·시간 최적화)', 'AIS 실시간 추적 + 레이더 교차확인', '본부 상황실 위치 보고 (15분 주기)'], time: '이동 중' }, + { phase: '③ 현장 작전', actions: ['정선명령 → 임검 또는 감시 유지', '증거 수집 (영상·사진·AIS 로그)', '위반 확인 시 나포 절차 진행'], time: '현장 도착 후' }, + { phase: '④ 복귀·보고', actions: ['작전 결과 보고 (상황실 실시간 전송)', '증거물 봉인 및 인계', '작전 후 회고 (AAR) 기록'], time: '작전 종료 후' }, +]; + +const AVAILABLE_SHIPS = [ + { name: '3009함', type: '1,000톤급', speed: '25kt', equip: '레이더·AIS·FLIR', status: '가용', crew: 18, zone: 'II구역' }, + { name: '3012함', type: '500톤급', speed: '30kt', equip: '레이더·AIS·EO/IR', status: '가용', crew: 12, zone: 'III구역' }, + { name: '1502함', type: '250톤급', speed: '28kt', equip: '레이더·AIS', status: '초계중', crew: 10, zone: 'I구역' }, + { name: '1507함', type: '250톤급', speed: '28kt', equip: '레이더·AIS·소나', status: '가용', crew: 10, zone: 'IV구역' }, + { name: '523정', type: '100톤급', speed: '32kt', equip: '레이더·AIS', status: '정비중', crew: 8, zone: '-' }, + { name: '527정', type: '100톤급', speed: '33kt', equip: '레이더·AIS·드론', status: '가용', crew: 8, zone: 'II구역' }, +]; + +// ─── 다함정 작전 데이터 ────────────────── + +const MULTI_OP_TYPES = [ + { type: '포위 차단 작전', desc: '2~4척이 불법 선단을 포위하여 도주로 차단', ships: '3~4척', formation: '삼각·사각 포위', command: '지휘함 1 + 차단함 2~3', icon: Target }, + { type: '광역 초계 작전', desc: '넓은 해역을 분할하여 동시 순찰', ships: '4~6척', formation: '구역 분할 병렬', command: '지휘함 1 + 순찰함 3~5', icon: Compass }, + { type: '합동 단속 작전', desc: '항공·함정 협동으로 고위험 해역 집중 단속', ships: '3~5척 + 항공 1', formation: '항공 유도 + 수상 차단', command: '합동지휘소', icon: Shield }, + { type: '호위 작전', desc: '아국 어선 보호 및 불법어선 접근 억제', ships: '2~3척', formation: '호위 종대', command: '지휘함 1 + 호위함 1~2', icon: Ship }, +]; + +const MULTI_ROLES = [ + { role: '지휘함', duty: '작전 총괄·의사결정·상황실 보고', requirement: '500톤급 이상', comm: 'VHF Ch.16 + 보안채널', badge: 'critical' as const }, + { role: '차단함', duty: '도주로 차단·정선명령 집행', requirement: '250톤급 이상', comm: 'VHF + AIS 공유', badge: 'high' as const }, + { role: '감시함', duty: '원거리 레이더·FLIR 감시, 증거 촬영', requirement: '100톤급 이상', comm: 'VHF + 영상 전송', badge: 'info' as const }, + { role: '기동함', duty: '고속 접근·소형선박 추적·인력 투입', requirement: '100톤급 고속정', comm: 'VHF + 전술채널', badge: 'warning' as const }, +]; + +const MULTI_COMM_PROTOCOL = [ + { channel: 'VHF Ch.16', purpose: '국제 조난·호출 (공용)', encryption: '비암호', usage: '초기 접촉·정선명령' }, + { channel: 'VHF Ch.22', purpose: '함정 간 전술 통신', encryption: '비암호', usage: '작전 기동·위치 보고' }, + { channel: '보안 채널 (HF)', purpose: '지휘함-상황실 암호 통신', encryption: 'AES-256', usage: '작전 지시·기밀 보고' }, + { channel: 'AIS 공유', purpose: '실시간 위치·속도 동기화', encryption: '-', usage: '함정 간 위치 인식' }, + { channel: '위성 데이터링크', purpose: '원거리 영상·데이터 전송', encryption: 'TLS 1.3', usage: '상황실 실시간 전송' }, +]; + +const MULTI_SCENARIOS = [ + { phase: '① 작전 계획', tasks: ['AI 위험도 분석 기반 작전 구역 선정', '함정 배치 및 역할 분담', '통신 채널·보고 주기 확정', '기상·해황·조류 분석'], commander: '작전지휘관' }, + { phase: '② 전개·배치', tasks: ['각 함정 지정 위치로 이동', '포위망 형성 또는 구역 분할 완료', '지휘함 위치 확인 보고', 'AIS 상호 추적 개시'], commander: '지휘함장' }, + { phase: '③ 작전 수행', tasks: ['감시함 탐지 → 지휘함 판단 → 차단함 투입', '정선명령·임검·나포 절차', '도주 시 기동함 추적 + 차단함 우회', '증거 수집·실시간 상황실 전송'], commander: '현장지휘관' }, + { phase: '④ 철수·평가', tasks: ['나포 선박 호송 또는 감시 해제', '전 함정 귀항 또는 재배치', '합동 작전 보고서(AAR) 작성', '작전 데이터 DB 기록 (AI 학습용)'], commander: '작전지휘관' }, +]; + interface Plan { id: string; zone: string; lat: number; lng: number; risk: number; period: string; ships: string; crew: number; status: string; alert: string; [key: string]: unknown; } /** API 응답 → 화면용 Plan 변환 */ @@ -54,6 +116,7 @@ export function EnforcementPlan() { const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); + const [tab, setTab] = useState('overview'); const [plans, setPlans] = useState([]); const [criticalEvents, setCriticalEvents] = useState([]); const [loading, setLoading] = useState(false); @@ -145,89 +208,333 @@ export function EnforcementPlan() { } /> - {/* 로딩/에러 상태 */} - {loading && ( -
단속 계획을 불러오는 중...
- )} - {error && ( -
로드 실패: {error}
- )} - -
- {[ - { l: '오늘 계획', v: `${todayCount}건`, c: 'text-heading', i: Calendar }, - { l: '경보 발령', v: `${alertCount}건`, c: 'text-red-400', i: AlertTriangle }, - { l: '투입 함정', v: `${totalShips}척`, c: 'text-cyan-400', i: Ship }, - { l: '투입 인력', v: `${totalCrew}명`, c: 'text-green-400', i: Users }, - ].map(k => ( -
- {k.v}{k.l} -
+ {/* 탭 */} +
+ {([ + { key: 'overview' as PlanTab, icon: Calendar, label: '단속 계획' }, + { key: 'single' as PlanTab, icon: Navigation, label: '단일 함정 순찰 작전' }, + { key: 'multi' as PlanTab, icon: Anchor, label: '다함정 순찰 작전' }, + ]).map((t) => ( + ))}
- {/* 미배정 CRITICAL 이벤트 */} - {criticalEvents.length > 0 && ( - - -
- - 미배정 CRITICAL 이벤트 - {criticalEvents.length}건 -
-
- {criticalEvents.map((evt) => ( -
- - {getAlertLevelLabel(evt.level, tc, lang)} - - {evt.title} - {formatDateTime(evt.occurredAt)} - {evt.vesselMmsi ?? '-'} -
- ))} -
-
-
- )} - - -
경보 임계값 설정
-
- {[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => ( -
- {k} - {v} + {/* ── ① 단속 계획 (기존) ── */} + {tab === 'overview' && ( +
+ {loading && ( +
단속 계획을 불러오는 중...
+ )} + {error && ( +
로드 실패: {error}
+ )} + +
+ {[ + { l: '오늘 계획', v: `${todayCount}건`, c: 'text-heading', i: Calendar }, + { l: '경보 발령', v: `${alertCount}건`, c: 'text-red-400', i: AlertTriangle }, + { l: '투입 함정', v: `${totalShips}척`, c: 'text-cyan-400', i: Ship }, + { l: '투입 인력', v: `${totalCrew}명`, c: 'text-green-400', i: Users }, + ].map(k => ( +
+ {k.v}{k.l}
))}
- - - + {criticalEvents.length > 0 && ( + + +
+ + 미배정 CRITICAL 이벤트 + {criticalEvents.length}건 +
+
+ {criticalEvents.map((evt) => ( +
+ + {getAlertLevelLabel(evt.level, tc, lang)} + + {evt.title} + {formatDateTime(evt.occurredAt)} + {evt.vesselMmsi ?? '-'} +
+ ))} +
+
+
+ )} - {/* 단속 구역 지도 */} - - - - {/* 범례 */} -
-
단속 구역
-
-
위험도 80+
-
위험도 60~80
-
위험도 40~60
-
-
-
확정
-
계획중
-
+ + +
경보 임계값 설정
+
+ {[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ + + + + +
+
단속 구역
+
+
위험도 80+
+
위험도 60~80
+
위험도 40~60
+
+
+
확정
+
계획중
+
+
+
+ {PLANS.length}개 + 단속 구역 배치 +
+ + +
+ )} + + {/* ── ② 단일 함정 순찰 작전 ── */} + {tab === 'single' && ( +
+ {/* 작전 유형 */} +
+ {SINGLE_OP_TYPES.map((op) => ( + + +
+ + {op.type} +
+

{op.desc}

+
+ {[ + ['소요시간', op.duration], + ['필요인력', op.crew], + ['위험수준', op.risk], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ ))}
-
- {PLANS.length}개 - 단속 구역 배치 + + {/* 가용 함정 현황 */} + + +
+ + 가용 함정 현황 + {AVAILABLE_SHIPS.filter(s => s.status === '가용').length}척 가용 +
+ + + + + + + + + + + + + + {AVAILABLE_SHIPS.map((s) => ( + + + + + + + + + + ))} + +
함정명함급최대속력장비승조원배치 구역상태
{s.name}{s.type}{s.speed}{s.equip}{s.crew}명{s.zone}{s.status}
+
+
+ + {/* 단일 함정 작전 절차 */} + + +
+ + 단일 함정 작전 절차 (SOP) +
+
+ {SINGLE_SCENARIOS.map((s, i) => ( +
+ {i < SINGLE_SCENARIOS.length - 1 && ( + + )} +
+
+ {s.phase} +
+
{s.time}
+
    + {s.actions.map((a) => ( +
  • + + {a} +
  • + ))} +
+
+
+ ))} +
+
+
+
+ )} + + {/* ── ③ 다함정 순찰 작전 ── */} + {tab === 'multi' && ( +
+ {/* 작전 유형 */} +
+ {MULTI_OP_TYPES.map((op) => ( + + +
+ + {op.type} +
+

{op.desc}

+
+ {[ + ['투입 함정', op.ships], + ['대형', op.formation], + ['지휘 체계', op.command], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ ))}
- - + + {/* 함정 역할 분담 */} + + +
+ + 함정별 역할 분담 +
+
+ {MULTI_ROLES.map((r) => ( +
+
+ {r.role} +
+
+
{r.duty}
+
+ 요구 사양 + {r.requirement} +
+
+ 통신 + {r.comm} +
+
+
+ ))} +
+
+
+ + {/* 통신 프로토콜 */} + + +
+ + 함정 간 통신 프로토콜 +
+ + + + + + + + + + + {MULTI_COMM_PROTOCOL.map((c) => ( + + + + + + + ))} + +
채널용도암호화사용 시점
{c.channel}{c.purpose} + {c.encryption} + {c.usage}
+
+
+ + {/* 다함정 합동 작전 절차 */} + + +
+ + 다함정 합동 작전 절차 (SOP) +
+
+ {MULTI_SCENARIOS.map((s, i) => ( +
+ {i < MULTI_SCENARIOS.length - 1 && ( + + )} +
+
+ {s.phase} + {s.commander} +
+
    + {s.tasks.map((t) => ( +
  • + + {t} +
  • + ))} +
+
+
+ ))} +
+
+
+
+ )} ); } diff --git a/frontend/src/lib/i18n/locales/en/common.json b/frontend/src/lib/i18n/locales/en/common.json index 1fce46f..10d35c7 100644 --- a/frontend/src/lib/i18n/locales/en/common.json +++ b/frontend/src/lib/i18n/locales/en/common.json @@ -36,7 +36,9 @@ "accessLogs": "Access Logs", "loginHistory": "Login History", "aiSecurity": "AI Security", - "aiAgentSecurity": "AI Agent Security" + "aiAgentSecurity": "AI Agent Security", + "dataRetentionPolicy": "Data Retention", + "dataModelVerification": "Model Verification" }, "status": { "active": "Active", diff --git a/frontend/src/lib/i18n/locales/ko/common.json b/frontend/src/lib/i18n/locales/ko/common.json index f8b7de0..74b7d9b 100644 --- a/frontend/src/lib/i18n/locales/ko/common.json +++ b/frontend/src/lib/i18n/locales/ko/common.json @@ -36,7 +36,9 @@ "accessLogs": "접근 이력", "loginHistory": "로그인 이력", "aiSecurity": "AI 보안", - "aiAgentSecurity": "AI Agent 보안" + "aiAgentSecurity": "AI Agent 보안", + "dataRetentionPolicy": "데이터 보관·파기", + "dataModelVerification": "데이터 모델 검증" }, "status": { "active": "활성",