From 256152f7fc60c232c9fb6faec9c58fcf16a55169 Mon Sep 17 00:00:00 2001 From: Nan Kyung Lee Date: Tue, 14 Apr 2026 14:55:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20>=20=EA=B0=90=EC=82=AC=C2=B7=EB=B3=B4=EC=95=88?= =?UTF-8?q?=EC=97=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D(DAR-11)=20=EB=A9=94=EB=89=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5탭 구성: 검증 현황 / 논리 모델 검증 / 물리 모델 검증 / 중복·정합성 점검 / 검증 결과 이력 - 4단계 검증 절차 (계획 수립→논리 검증→물리 검증→결과 보고) - 논리 모델 8항목 (완전성·정합성·정규화·표준), 물리 모델 10항목 (구조·타입·인덱스·제약·성능) - 중복·정합성 점검 6항목 + 8개 주제영역 48테이블 매핑 - V027 마이그레이션: admin:data-model-verification 권한 트리 + ADMIN 역할 권한 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../V027__data_model_verification_menu.sql | 28 ++ frontend/src/app/componentRegistry.ts | 3 + .../features/admin/DataModelVerification.tsx | 378 ++++++++++++++++++ frontend/src/features/admin/index.ts | 1 + frontend/src/lib/i18n/locales/en/common.json | 3 +- frontend/src/lib/i18n/locales/ko/common.json | 3 +- 6 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V027__data_model_verification_menu.sql create mode 100644 frontend/src/features/admin/DataModelVerification.tsx 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/src/app/componentRegistry.ts b/frontend/src/app/componentRegistry.ts index d712205..204ef79 100644 --- a/frontend/src/app/componentRegistry.ts +++ b/frontend/src/app/componentRegistry.ts @@ -125,6 +125,9 @@ export const COMPONENT_REGISTRY: Record = { '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/index.ts b/frontend/src/features/admin/index.ts index d7386dd..d435fec 100644 --- a/frontend/src/features/admin/index.ts +++ b/frontend/src/features/admin/index.ts @@ -6,3 +6,4 @@ 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/lib/i18n/locales/en/common.json b/frontend/src/lib/i18n/locales/en/common.json index 9ec2062..10d35c7 100644 --- a/frontend/src/lib/i18n/locales/en/common.json +++ b/frontend/src/lib/i18n/locales/en/common.json @@ -37,7 +37,8 @@ "loginHistory": "Login History", "aiSecurity": "AI Security", "aiAgentSecurity": "AI Agent Security", - "dataRetentionPolicy": "Data Retention" + "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 2ac6039..74b7d9b 100644 --- a/frontend/src/lib/i18n/locales/ko/common.json +++ b/frontend/src/lib/i18n/locales/ko/common.json @@ -37,7 +37,8 @@ "loginHistory": "로그인 이력", "aiSecurity": "AI 보안", "aiAgentSecurity": "AI Agent 보안", - "dataRetentionPolicy": "데이터 보관·파기" + "dataRetentionPolicy": "데이터 보관·파기", + "dataModelVerification": "데이터 모델 검증" }, "status": { "active": "활성",