diff --git a/backend/pom.xml b/backend/pom.xml index 7a0d549..7384f52 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -110,6 +110,11 @@ spring-security-test test + + + org.hibernate.orm + hibernate-spatial + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9fdcbd0..af78fe1 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -19,6 +19,7 @@ spring: hibernate: default_schema: kcg format_sql: true + dialect: org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect jdbc: time_zone: Asia/Seoul open-in-view: false diff --git a/backend/src/main/resources/db/migration/V008__code_master.sql b/backend/src/main/resources/db/migration/V008__code_master.sql new file mode 100644 index 0000000..d981603 --- /dev/null +++ b/backend/src/main/resources/db/migration/V008__code_master.sql @@ -0,0 +1,163 @@ +-- ============================================================ +-- V008: code_master (계층형 트리 코드 테이블 + 시드) +-- auth_perm_tree와 동일한 self-referencing 패턴 +-- ============================================================ + +CREATE TABLE kcg.code_master ( + code_id VARCHAR(100) PRIMARY KEY, + parent_id VARCHAR(100) REFERENCES kcg.code_master(code_id), + group_code VARCHAR(50) NOT NULL, -- 루트 그룹 (빠른 필터용 비정규화) + code VARCHAR(50) NOT NULL, -- 이 레벨의 코드값 + depth INT NOT NULL DEFAULT 0, + name_ko VARCHAR(100) NOT NULL, + name_en VARCHAR(100), + sort_order INT DEFAULT 0, + color_hex VARCHAR(10), + icon VARCHAR(30), + metadata JSONB, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_code_group ON kcg.code_master(group_code, depth); +CREATE INDEX idx_code_parent ON kcg.code_master(parent_id); + +-- ============================================================ +-- 위반 유형 (VIOLATION_TYPE) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('VIOLATION_TYPE', NULL, 'VIOLATION_TYPE', 'VIOLATION_TYPE', 0, '위반 유형', 'Violation Type', 10, NULL), +('VIOLATION_TYPE:EEZ_VIOLATION', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'EEZ_VIOLATION', 1, 'EEZ 침범', 'EEZ Violation', 10, '#EF4444'), +('VIOLATION_TYPE:DARK_VESSEL', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'DARK_VESSEL', 1, '다크베셀', 'Dark Vessel', 20, '#7C3AED'), +('VIOLATION_TYPE:MMSI_TAMPERING', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'MMSI_TAMPERING', 1, 'MMSI 변조', 'MMSI Tampering', 30, '#F59E0B'), +('VIOLATION_TYPE:ILLEGAL_TRANSSHIP', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'ILLEGAL_TRANSSHIP',1,'불법환적', 'Illegal Transship', 40, '#EC4899'), +('VIOLATION_TYPE:ILLEGAL_GEAR', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'ILLEGAL_GEAR', 1, '어구 불법', 'Illegal Gear', 50, '#F97316'), +('VIOLATION_TYPE:ZONE_DEPARTURE', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'ZONE_DEPARTURE', 1, '조업구역 이탈', 'Zone Departure', 60, '#06B6D4'), +('VIOLATION_TYPE:RISK_BEHAVIOR', 'VIOLATION_TYPE', 'VIOLATION_TYPE', 'RISK_BEHAVIOR', 1, '위험 행동', 'Risk Behavior', 70, '#64748B'); + +-- ============================================================ +-- 이벤트 레벨 (EVENT_LEVEL) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('EVENT_LEVEL', NULL, 'EVENT_LEVEL', 'EVENT_LEVEL', 0, '이벤트 심각도', 'Event Level', 20, NULL), +('EVENT_LEVEL:CRITICAL', 'EVENT_LEVEL', 'EVENT_LEVEL', 'CRITICAL', 1, '심각', 'Critical', 10, '#EF4444'), +('EVENT_LEVEL:HIGH', 'EVENT_LEVEL', 'EVENT_LEVEL', 'HIGH', 1, '높음', 'High', 20, '#F59E0B'), +('EVENT_LEVEL:MEDIUM', 'EVENT_LEVEL', 'EVENT_LEVEL', 'MEDIUM', 1, '보통', 'Medium', 30, '#3B82F6'), +('EVENT_LEVEL:LOW', 'EVENT_LEVEL', 'EVENT_LEVEL', 'LOW', 1, '낮음', 'Low', 40, '#6B7280'); + +-- ============================================================ +-- 이벤트 상태 (EVENT_STATUS) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('EVENT_STATUS', NULL, 'EVENT_STATUS', 'EVENT_STATUS', 0, '이벤트 상태', 'Event Status', 30, NULL), +('EVENT_STATUS:NEW', 'EVENT_STATUS', 'EVENT_STATUS', 'NEW', 1, '신규', 'New', 10, '#EF4444'), +('EVENT_STATUS:ACK', 'EVENT_STATUS', 'EVENT_STATUS', 'ACK', 1, '확인', 'Acknowledged', 20, '#F59E0B'), +('EVENT_STATUS:IN_PROGRESS', 'EVENT_STATUS', 'EVENT_STATUS', 'IN_PROGRESS', 1, '처리중', 'In Progress', 30, '#3B82F6'), +('EVENT_STATUS:RESOLVED', 'EVENT_STATUS', 'EVENT_STATUS', 'RESOLVED', 1, '완료', 'Resolved', 40, '#22C55E'), +('EVENT_STATUS:FALSE_POSITIVE', 'EVENT_STATUS', 'EVENT_STATUS', 'FALSE_POSITIVE',1, '오탐', 'False Positive', 50, '#6B7280'); + +-- ============================================================ +-- 이벤트 카테고리 (EVENT_CATEGORY) — 이벤트 유형 세분화 +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('EVENT_CATEGORY', NULL, 'EVENT_CATEGORY', 'EVENT_CATEGORY', 0, '이벤트 유형', 'Event Category', 35, NULL), +('EVENT_CATEGORY:EEZ_INTRUSION', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'EEZ_INTRUSION', 1, 'EEZ 침범', 'EEZ Intrusion', 10, '#EF4444'), +('EVENT_CATEGORY:DARK_VESSEL', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'DARK_VESSEL', 1, '다크베셀', 'Dark Vessel', 20, '#7C3AED'), +('EVENT_CATEGORY:FLEET_CLUSTER', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'FLEET_CLUSTER', 1, '선단밀집', 'Fleet Cluster', 30, '#F97316'), +('EVENT_CATEGORY:ILLEGAL_TRANSSHIP', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'ILLEGAL_TRANSSHIP',1, '불법환적', 'Illegal Transship',40, '#EC4899'), +('EVENT_CATEGORY:MMSI_TAMPERING', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'MMSI_TAMPERING', 1, 'MMSI 변조', 'MMSI Tampering', 50, '#F59E0B'), +('EVENT_CATEGORY:AIS_LOSS', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'AIS_LOSS', 1, 'AIS 소실', 'AIS Loss', 60, '#64748B'), +('EVENT_CATEGORY:SPEED_ANOMALY', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'SPEED_ANOMALY', 1, '속력 이상', 'Speed Anomaly', 70, '#06B6D4'), +('EVENT_CATEGORY:ZONE_DEPARTURE', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'ZONE_DEPARTURE', 1, '구역 이탈', 'Zone Departure', 80, '#8B5CF6'), +('EVENT_CATEGORY:GEAR_ILLEGAL', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'GEAR_ILLEGAL', 1, '불법어구', 'Illegal Gear', 90, '#F97316'), +('EVENT_CATEGORY:AIS_RESUME', 'EVENT_CATEGORY', 'EVENT_CATEGORY', 'AIS_RESUME', 1, 'AIS 재송출', 'AIS Resume', 100, '#22C55E'); + +-- ============================================================ +-- 단속 조치 유형 (ENFORCEMENT_ACTION) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('ENFORCEMENT_ACTION', NULL, 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 0, '단속 조치', 'Enforcement Action', 40, NULL), +('ENFORCEMENT_ACTION:CAPTURE', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'CAPTURE', 1, '나포', 'Capture', 10, '#EF4444'), +('ENFORCEMENT_ACTION:INSPECT', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'INSPECT', 1, '검문', 'Inspect', 20, '#F59E0B'), +('ENFORCEMENT_ACTION:WARN', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'WARN', 1, '경고', 'Warn', 30, '#3B82F6'), +('ENFORCEMENT_ACTION:DISPERSE', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'DISPERSE', 1, '퇴거', 'Disperse', 40, '#8B5CF6'), +('ENFORCEMENT_ACTION:TRACK', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'TRACK', 1, '추적', 'Track', 50, '#06B6D4'), +('ENFORCEMENT_ACTION:EVIDENCE', 'ENFORCEMENT_ACTION', 'ENFORCEMENT_ACTION', 'EVIDENCE', 1, '증거수집', 'Evidence', 60, '#64748B'); + +-- ============================================================ +-- 단속 결과 (ENFORCEMENT_RESULT) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('ENFORCEMENT_RESULT', NULL, 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 0, '단속 결과', 'Enforcement Result', 50, NULL), +('ENFORCEMENT_RESULT:PUNISHED', 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 'PUNISHED', 1, '처벌', 'Punished', 10, '#EF4444'), +('ENFORCEMENT_RESULT:WARNED', 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 'WARNED', 1, '경고', 'Warned', 20, '#F59E0B'), +('ENFORCEMENT_RESULT:RELEASED', 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 'RELEASED', 1, '훈방', 'Released', 30, '#22C55E'), +('ENFORCEMENT_RESULT:REFERRED', 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 'REFERRED', 1, '수사의뢰', 'Referred', 40, '#7C3AED'), +('ENFORCEMENT_RESULT:FALSE_POSITIVE', 'ENFORCEMENT_RESULT', 'ENFORCEMENT_RESULT', 'FALSE_POSITIVE', 1, '오탐(정상)', 'False Positive', 50, '#6B7280'); + +-- ============================================================ +-- AI 일치도 (AI_MATCH) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('AI_MATCH', NULL, 'AI_MATCH', 'AI_MATCH', 0, 'AI 일치도', 'AI Match', 60, NULL), +('AI_MATCH:MATCH', 'AI_MATCH', 'AI_MATCH', 'MATCH', 1, '일치', 'Match', 10, '#22C55E'), +('AI_MATCH:PARTIAL', 'AI_MATCH', 'AI_MATCH', 'PARTIAL', 1, '부분일치', 'Partial', 20, '#F59E0B'), +('AI_MATCH:MISMATCH', 'AI_MATCH', 'AI_MATCH', 'MISMATCH', 1, '불일치', 'Mismatch', 30, '#EF4444'); + +-- ============================================================ +-- 다크베셀 패턴 (DARK_PATTERN) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('DARK_PATTERN', NULL, 'DARK_PATTERN', 'DARK_PATTERN', 0, '다크베셀 패턴', 'Dark Pattern', 70, NULL), +('DARK_PATTERN:COMPLETE_BLACKOUT', 'DARK_PATTERN', 'DARK_PATTERN', 'COMPLETE_BLACKOUT', 1, 'AIS 완전차단', 'Complete Blackout', 10, '#EF4444'), +('DARK_PATTERN:INTERMITTENT', 'DARK_PATTERN', 'DARK_PATTERN', 'INTERMITTENT', 1, '간헐 송출', 'Intermittent', 20, '#F59E0B'), +('DARK_PATTERN:MMSI_SPOOFING', 'DARK_PATTERN', 'DARK_PATTERN', 'MMSI_SPOOFING', 1, 'MMSI 변경 의심', 'MMSI Spoofing', 30, '#F97316'), +('DARK_PATTERN:ZONE_BOUNDARY_DISAPPEAR', 'DARK_PATTERN', 'DARK_PATTERN', 'ZONE_BOUNDARY_DISAPPEAR',1,'수역 경계 소실', 'Zone Boundary Disappear',40, '#7C3AED'), +('DARK_PATTERN:RAPID_REAPPEAR', 'DARK_PATTERN', 'DARK_PATTERN', 'RAPID_REAPPEAR', 1, '급재출현', 'Rapid Reappear', 50, '#EC4899'), +('DARK_PATTERN:SCHEDULED_BLACKOUT', 'DARK_PATTERN', 'DARK_PATTERN', 'SCHEDULED_BLACKOUT', 1, '정기 차단(저위험)','Scheduled Blackout', 60, '#6B7280'); + +-- ============================================================ +-- 허가 상태 (PERMIT_STATUS) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('PERMIT_STATUS', NULL, 'PERMIT_STATUS', 'PERMIT_STATUS', 0, '허가 상태', 'Permit Status', 80, NULL), +('PERMIT_STATUS:PERMITTED', 'PERMIT_STATUS', 'PERMIT_STATUS', 'PERMITTED', 1, '유효', 'Permitted', 10, '#22C55E'), +('PERMIT_STATUS:EXPIRED', 'PERMIT_STATUS', 'PERMIT_STATUS', 'EXPIRED', 1, '기간 초과', 'Expired', 20, '#F59E0B'), +('PERMIT_STATUS:NONE', 'PERMIT_STATUS', 'PERMIT_STATUS', 'NONE', 1, '무허가', 'None', 30, '#EF4444'), +('PERMIT_STATUS:REVOKED', 'PERMIT_STATUS', 'PERMIT_STATUS', 'REVOKED', 1, '취소', 'Revoked', 40, '#7C3AED'), +('PERMIT_STATUS:UNKNOWN', 'PERMIT_STATUS', 'PERMIT_STATUS', 'UNKNOWN', 1, '미상', 'Unknown', 50, '#6B7280'); + +-- ============================================================ +-- 어구 판정 (GEAR_JUDGMENT) — illegal_gear_classifier 산출 +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('GEAR_JUDGMENT', NULL, 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 0, '어구 판정', 'Gear Judgment', 90, NULL), +('GEAR_JUDGMENT:LEGAL', 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 'LEGAL', 1, '합법', 'Legal', 10, '#22C55E'), +('GEAR_JUDGMENT:NO_PERMIT', 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 'NO_PERMIT', 1, '무허가', 'No Permit', 20, '#EF4444'), +('GEAR_JUDGMENT:GEAR_MISMATCH', 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 'GEAR_MISMATCH', 1, '어구 불일치', 'Gear Mismatch', 30, '#F59E0B'), +('GEAR_JUDGMENT:ZONE_VIOLATION', 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 'ZONE_VIOLATION', 1, '해역 위반', 'Zone Violation',40, '#F97316'), +('GEAR_JUDGMENT:SEASON_VIOLATION', 'GEAR_JUDGMENT', 'GEAR_JUDGMENT', 'SEASON_VIOLATION',1, '금어기 위반', 'Season Violation',50,'#7C3AED'); + +-- ============================================================ +-- 함정 상태 (PATROL_STATUS) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('PATROL_STATUS', NULL, 'PATROL_STATUS', 'PATROL_STATUS', 0, '함정 상태', 'Patrol Status', 100, NULL), +('PATROL_STATUS:AVAILABLE', 'PATROL_STATUS', 'PATROL_STATUS', 'AVAILABLE', 1, '가용', 'Available', 10, '#22C55E'), +('PATROL_STATUS:ON_PATROL', 'PATROL_STATUS', 'PATROL_STATUS', 'ON_PATROL', 1, '초계중', 'On Patrol', 20, '#3B82F6'), +('PATROL_STATUS:IN_PURSUIT', 'PATROL_STATUS', 'PATROL_STATUS', 'IN_PURSUIT', 1, '추적중', 'In Pursuit', 30, '#EF4444'), +('PATROL_STATUS:INSPECTING', 'PATROL_STATUS', 'PATROL_STATUS', 'INSPECTING', 1, '검문중', 'Inspecting', 40, '#F59E0B'), +('PATROL_STATUS:RETURNING', 'PATROL_STATUS', 'PATROL_STATUS', 'RETURNING', 1, '귀항중', 'Returning', 50, '#8B5CF6'), +('PATROL_STATUS:STANDBY', 'PATROL_STATUS', 'PATROL_STATUS', 'STANDBY', 1, '대기', 'Standby', 60, '#64748B'), +('PATROL_STATUS:MAINTENANCE', 'PATROL_STATUS', 'PATROL_STATUS', 'MAINTENANCE', 1, '정비중', 'Maintenance', 70, '#6B7280'); + +-- ============================================================ +-- 선박 국적 (VESSEL_FLAG) +-- ============================================================ +INSERT INTO kcg.code_master (code_id, parent_id, group_code, code, depth, name_ko, name_en, sort_order, color_hex) VALUES +('VESSEL_FLAG', NULL, 'VESSEL_FLAG', 'VESSEL_FLAG', 0, '선박 국적', 'Vessel Flag', 110, NULL), +('VESSEL_FLAG:CN', 'VESSEL_FLAG', 'VESSEL_FLAG', 'CN', 1, '중국', 'China', 10, '#EF4444'), +('VESSEL_FLAG:KR', 'VESSEL_FLAG', 'VESSEL_FLAG', 'KR', 1, '한국', 'Korea', 20, '#3B82F6'), +('VESSEL_FLAG:JP', 'VESSEL_FLAG', 'VESSEL_FLAG', 'JP', 1, '일본', 'Japan', 30, '#22C55E'), +('VESSEL_FLAG:RU', 'VESSEL_FLAG', 'VESSEL_FLAG', 'RU', 1, '러시아', 'Russia', 40, '#F59E0B'), +('VESSEL_FLAG:UNKNOWN', 'VESSEL_FLAG', 'VESSEL_FLAG', 'UNKNOWN', 1, '미상', 'Unknown', 50, '#6B7280'); diff --git a/backend/src/main/resources/db/migration/V009__gear_type_master.sql b/backend/src/main/resources/db/migration/V009__gear_type_master.sql new file mode 100644 index 0000000..22b8aeb --- /dev/null +++ b/backend/src/main/resources/db/migration/V009__gear_type_master.sql @@ -0,0 +1,60 @@ +-- ============================================================ +-- V009: gear_type_master (어구 유형 마스터) +-- prediction classifier 출력값과 1:1 매칭 +-- GearIdentification 화면에서 관리자가 룰 편집 +-- ============================================================ + +CREATE TABLE kcg.gear_type_master ( + gear_code VARCHAR(20) PRIMARY KEY, + gear_name_ko VARCHAR(50) NOT NULL, + gear_name_en VARCHAR(50), + category VARCHAR(20), -- NET, TRAP, LINE + -- 분류 룰 (prediction classifier가 사용) + speed_min_kn NUMERIC(5,2), -- 조업 속도 범위 + speed_max_kn NUMERIC(5,2), + duration_min_minutes INT, -- 최소 지속시간 + pattern_signature JSONB, -- 추가 분류 규칙 (확장용) + polygon_shape_hint VARCHAR(20), -- LINEAR, CIRCULAR, CLUSTERED + -- 합법성 기준 + legal_zones TEXT[], -- zone_polygon_master.zone_code 참조 + legal_seasons JSONB, -- [{"start":"03-01","end":"06-30"}] + permit_required BOOLEAN DEFAULT true, + -- 표시 + display_color VARCHAR(7), + display_icon VARCHAR(30), + display_order INT DEFAULT 0, + description TEXT, + is_active BOOLEAN DEFAULT true, + created_by UUID, + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 시드: 프론트엔드 더미 기반 6종 +-- ============================================================ +INSERT INTO kcg.gear_type_master (gear_code, gear_name_ko, gear_name_en, category, + speed_min_kn, speed_max_kn, duration_min_minutes, polygon_shape_hint, + permit_required, display_color, display_order, description) VALUES +('TRAWL_BOTTOM', '저층트롤', 'Bottom Trawl', 'NET', + 2.0, 5.0, 60, 'LINEAR', + true, '#EF4444', 10, '저층을 끌어 조업하는 그물. 해저 생태계 영향 크고 대부분의 수역에서 규제됨'), + +('GILLNET', '유자망', 'Gill Net', 'NET', + 0.0, 2.0, 120, 'LINEAR', + true, '#F59E0B', 20, '그물을 설치하고 기다리는 수동 어구. 부유/저층 구분'), + +('TRAP', '통발', 'Trap/Pot', 'TRAP', + 0.0, 1.0, 240, 'CLUSTERED', + true, '#3B82F6', 30, '바닥에 설치하는 덫 방식 어구. 특정 어종 대상'), + +('PURSE_SEINE', '선망', 'Purse Seine', 'NET', + 3.0, 8.0, 30, 'CIRCULAR', + true, '#22C55E', 40, '원형으로 그물을 던져 감싸는 대규모 어법. 선단 규모 필요'), + +('LONGLINE', '연승', 'Long Line', 'LINE', + 1.0, 4.0, 180, 'LINEAR', + true, '#8B5CF6', 50, '긴 줄에 낚시바늘을 다수 부착하는 어법. 궤적이 선형'), + +('UNKNOWN', '미분류', 'Unknown', NULL, + NULL, NULL, NULL, NULL, + false, '#6B7280', 99, '분류 불가능한 어구 또는 어구 미사용 선박'); diff --git a/backend/src/main/resources/db/migration/V010__zone_polygon_master.sql b/backend/src/main/resources/db/migration/V010__zone_polygon_master.sql new file mode 100644 index 0000000..c1a0b9a --- /dev/null +++ b/backend/src/main/resources/db/migration/V010__zone_polygon_master.sql @@ -0,0 +1,80 @@ +-- ============================================================ +-- V010: zone_polygon_master (해역 폴리곤 마스터) +-- PostGIS GEOMETRY 사용, MapControl 화면에서 관리자가 편집 +-- prediction location.py가 매 사이클 참조 +-- ============================================================ + +CREATE TABLE kcg.zone_polygon_master ( + zone_code VARCHAR(30) PRIMARY KEY, + zone_name_ko VARCHAR(100) NOT NULL, + zone_name_en VARCHAR(100), + zone_type VARCHAR(30) NOT NULL, -- TERRITORIAL, EEZ, SPECIAL_FISHING, NLL, BUFFER, PROHIBITED, PATROL_SECTOR + parent_zone_code VARCHAR(30) REFERENCES kcg.zone_polygon_master(zone_code), + polygon_geom GEOMETRY(MULTIPOLYGON, 4326), + -- 단속 정책 + baseline_distance_nm NUMERIC(8,2), + enforcement_priority INT DEFAULT 5, -- 1=최우선 + default_risk_level VARCHAR(20), -- 진입만으로 부여되는 기본 위험 레벨 + -- 어구 정책 + allowed_gear_codes TEXT[], + prohibited_gear_codes TEXT[], + -- 표시 + display_color VARCHAR(7), + display_opacity NUMERIC(3,2) DEFAULT 0.3, + display_order INT DEFAULT 0, + description TEXT, + metadata JSONB, -- 관할서, 면적, 기타 + is_active BOOLEAN DEFAULT true, + created_by UUID, + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_zone_geom ON kcg.zone_polygon_master USING GIST(polygon_geom); +CREATE INDEX idx_zone_type ON kcg.zone_polygon_master(zone_type); +CREATE INDEX idx_zone_parent ON kcg.zone_polygon_master(parent_zone_code); + +-- ============================================================ +-- 시드: 주요 한국 해역 (간략 폴리곤 — 향후 정밀 GeoJSON import) +-- prediction의 data/zones/ GeoJSON이 정밀 데이터 소스 +-- ============================================================ + +-- 한국 영해 (12해리, 간략 바운딩) +INSERT INTO kcg.zone_polygon_master (zone_code, zone_name_ko, zone_name_en, zone_type, + enforcement_priority, default_risk_level, display_color, display_opacity, display_order, description) +VALUES +('TERRITORIAL_SEA', '영해', 'Territorial Sea', 'TERRITORIAL', + 3, NULL, '#3B82F6', 0.15, 10, '기선으로부터 12해리 이내'); + +-- 한국 EEZ +INSERT INTO kcg.zone_polygon_master (zone_code, zone_name_ko, zone_name_en, zone_type, + enforcement_priority, default_risk_level, display_color, display_opacity, display_order, description) +VALUES +('EEZ_KR', '한국 EEZ', 'Korea EEZ', 'EEZ', + 2, 'LOW', '#06B6D4', 0.1, 20, '한국 배타적 경제수역'); + +-- NLL +INSERT INTO kcg.zone_polygon_master (zone_code, zone_name_ko, zone_name_en, zone_type, + enforcement_priority, default_risk_level, display_color, display_opacity, display_order, description) +VALUES +('NLL', '북방한계선', 'Northern Limit Line', 'NLL', + 1, 'CRITICAL', '#EF4444', 0.3, 5, '서해 북방한계선. 최우선 감시 구역'); + +-- 특정어업수역 Ⅰ~Ⅳ (iran prediction의 zones/ GeoJSON 참조) +INSERT INTO kcg.zone_polygon_master (zone_code, zone_name_ko, zone_name_en, zone_type, + parent_zone_code, enforcement_priority, default_risk_level, + display_color, display_opacity, display_order, description) VALUES +('SPECIAL_FISHING_1', '특정어업수역 Ⅰ', 'Special Fishing Zone 1', 'SPECIAL_FISHING', + 'EEZ_KR', 2, 'HIGH', '#F59E0B', 0.25, 30, '한중 잠정조치수역 인접'), +('SPECIAL_FISHING_2', '특정어업수역 Ⅱ', 'Special Fishing Zone 2', 'SPECIAL_FISHING', + 'EEZ_KR', 2, 'HIGH', '#F97316', 0.25, 31, '서해 중부 어업 수역'), +('SPECIAL_FISHING_3', '특정어업수역 Ⅲ', 'Special Fishing Zone 3', 'SPECIAL_FISHING', + 'EEZ_KR', 3, 'MEDIUM', '#8B5CF6', 0.2, 32, '남해 어업 수역'), +('SPECIAL_FISHING_4', '특정어업수역 Ⅳ', 'Special Fishing Zone 4', 'SPECIAL_FISHING', + 'EEZ_KR', 3, 'MEDIUM', '#EC4899', 0.2, 33, '동해 어업 수역'); + +-- 서해 5도 (patrol store 더미 참조) +INSERT INTO kcg.zone_polygon_master (zone_code, zone_name_ko, zone_name_en, zone_type, + enforcement_priority, default_risk_level, display_color, display_opacity, display_order, description) +VALUES +('WEST_5_ISLANDS', '서해 5도', 'West Sea 5 Islands', 'PATROL_SECTOR', + 1, 'HIGH', '#EF4444', 0.2, 6, '백령도/대청도/소청도/연평도/우도 인근'); diff --git a/backend/src/main/resources/db/migration/V011__vessel_permit_patrol.sql b/backend/src/main/resources/db/migration/V011__vessel_permit_patrol.sql new file mode 100644 index 0000000..b0496b3 --- /dev/null +++ b/backend/src/main/resources/db/migration/V011__vessel_permit_patrol.sql @@ -0,0 +1,104 @@ +-- ============================================================ +-- V011: vessel_permit_master + patrol_ship_master + fleet_companies + 시드 +-- ============================================================ + +-- ============================================================ +-- 선단 회사 (중국 선단 회사 레지스트리) +-- ============================================================ +CREATE TABLE kcg.fleet_companies ( + id BIGSERIAL PRIMARY KEY, + name_cn VARCHAR(200), + name_en VARCHAR(200), + name_ko VARCHAR(200), + country VARCHAR(10) DEFAULT 'CN', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 어선 허가/등록 마스터 +-- ============================================================ +CREATE TABLE kcg.vessel_permit_master ( + mmsi VARCHAR(20) PRIMARY KEY, + vessel_name VARCHAR(100), + vessel_name_cn VARCHAR(100), + flag_country VARCHAR(10), -- KR, CN, JP, RU, UNKNOWN + vessel_type VARCHAR(30), -- FISHING, CARGO, TANKER, PATROL, UNKNOWN + tonnage NUMERIC(10,2), + length_m NUMERIC(6,2), + build_year INT, + -- 허가 상태 + permit_status VARCHAR(20) DEFAULT 'UNKNOWN', -- PERMITTED, EXPIRED, NONE, REVOKED, UNKNOWN + permit_no VARCHAR(50), + permitted_gear_codes TEXT[], -- gear_type_master.gear_code 참조 + permitted_zones TEXT[], -- zone_polygon_master.zone_code 참조 + permit_valid_from DATE, + permit_valid_to DATE, + -- 소속 + company_id BIGINT REFERENCES kcg.fleet_companies(id), + -- 메타 + data_source VARCHAR(50) DEFAULT 'MANUAL', -- KR_FISHERIES, CHINA_REGISTRY, IRAN_REGISTRY, MANUAL + last_synced_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_vessel_flag ON kcg.vessel_permit_master(flag_country); +CREATE INDEX idx_vessel_permit ON kcg.vessel_permit_master(permit_status); +CREATE INDEX idx_vessel_company ON kcg.vessel_permit_master(company_id); + +-- ============================================================ +-- 함정 마스터 +-- ============================================================ +CREATE TABLE kcg.patrol_ship_master ( + ship_id BIGSERIAL PRIMARY KEY, + ship_code VARCHAR(20) UNIQUE NOT NULL, -- '3001함' + ship_name VARCHAR(100), + ship_class VARCHAR(50), -- 태극급, 참수리급, 삼봉급 + tonnage NUMERIC(10,2), + max_speed_kn NUMERIC(5,2), + fuel_capacity_l NUMERIC(10,2), + base_port VARCHAR(50), + -- 현재 상태 (실시간 갱신) + current_status VARCHAR(20) DEFAULT 'STANDBY', -- code_master PATROL_STATUS 참조 + current_lat DOUBLE PRECISION, + current_lon DOUBLE PRECISION, + current_zone_code VARCHAR(30), -- zone_polygon_master FK + fuel_pct INT, + crew_count INT, + -- 메타 + is_active BOOLEAN DEFAULT true, + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 시드: 프론트엔드 더미 기반 선박 (9척) +-- ============================================================ +INSERT INTO kcg.fleet_companies (id, name_cn, name_en, name_ko, country) VALUES +(1, '荣成远洋渔业', 'Rongcheng Ocean Fishery', '영성원양어업', 'CN'), +(2, '大连海洋', 'Dalian Ocean', '대련해양', 'CN'); +SELECT setval('kcg.fleet_companies_id_seq', 10); + +INSERT INTO kcg.vessel_permit_master (mmsi, vessel_name, vessel_name_cn, flag_country, vessel_type, + tonnage, permit_status, permitted_gear_codes, company_id, data_source) VALUES +('412345678', '鲁荣渔56555', '鲁荣渔56555', 'CN', 'FISHING', 450.0, 'NONE', '{TRAWL_BOTTOM}', 1, 'IRAN_REGISTRY'), +('412345679', '鲁荣渔56556', '鲁荣渔56556', 'CN', 'FISHING', 380.0, 'EXPIRED', '{GILLNET}', 1, 'IRAN_REGISTRY'), +('412345680', '辽大渔42881', '辽大渔42881', 'CN', 'FISHING', 520.0, 'NONE', '{PURSE_SEINE}', 2, 'IRAN_REGISTRY'), +('412345681', '浙象渔23166', '浙象渔23166', 'CN', 'FISHING', 290.0, 'NONE', '{LONGLINE}', NULL, 'IRAN_REGISTRY'), +('412345682', '闽霞渔09876', '闽霞渔09876', 'CN', 'FISHING', 410.0, 'NONE', '{TRAP}', NULL, 'IRAN_REGISTRY'), +('412345683', '苏赣渔05512', '苏赣渔05512', 'CN', 'FISHING', 350.0, 'NONE', '{GILLNET}', NULL, 'IRAN_REGISTRY'), +('440012345', '제주해양호', NULL, 'KR', 'FISHING', 85.0, 'PERMITTED','{LONGLINE,TRAP}',NULL, 'KR_FISHERIES'), +('440012346', '통영수산호', NULL, 'KR', 'FISHING', 120.0, 'PERMITTED','{GILLNET}', NULL, 'KR_FISHERIES'), +('000000001', 'Unknown-001', NULL, 'UNKNOWN','UNKNOWN',NULL,'UNKNOWN', NULL, NULL, 'MANUAL'); + +-- ============================================================ +-- 시드: 함정 6척 (프론트엔드 patrolStore 기반) +-- ============================================================ +INSERT INTO kcg.patrol_ship_master (ship_code, ship_name, ship_class, tonnage, max_speed_kn, + fuel_capacity_l, base_port, current_status, current_lat, current_lon, + current_zone_code, fuel_pct, crew_count) VALUES +('3001', '3001함', '태극급', 3000.0, 30.0, 500000, '인천', 'IN_PURSUIT', 37.52, 124.78, 'NLL', 78, 120), +('3005', '3005함', '삼봉급', 1500.0, 25.0, 300000, '목포', 'ON_PATROL', 34.85, 125.42, 'SPECIAL_FISHING_1', 92, 80), +('3009', '3009함', '참수리급', 500.0, 35.0, 100000, '속초', 'AVAILABLE', 38.12, 128.95, NULL, 100, 35), +('5001', '5001함', '태극급', 3000.0, 30.0, 500000, '부산', 'ON_PATROL', 34.42, 129.38, 'SPECIAL_FISHING_3', 65, 120), +('1502', '1502함', '참수리급', 500.0, 35.0, 100000, '인천', 'INSPECTING', 37.38, 124.55, 'WEST_5_ISLANDS', 45, 35), +('2003', '2003함', '삼봉급', 1500.0, 25.0, 300000, '서귀포', 'STANDBY', 33.15, 126.58, NULL, 88, 80); diff --git a/backend/src/main/resources/db/migration/V012__prediction_events_stats.sql b/backend/src/main/resources/db/migration/V012__prediction_events_stats.sql new file mode 100644 index 0000000..b82c849 --- /dev/null +++ b/backend/src/main/resources/db/migration/V012__prediction_events_stats.sql @@ -0,0 +1,252 @@ +-- ============================================================ +-- V012: prediction 산출 테이블 (이벤트 허브 + 알림 + 통계) +-- prediction이 write, backend가 상태만 update +-- ============================================================ + +-- ============================================================ +-- 분석 결과 확장 (vessel_analysis_results) +-- prediction이 직접 INSERT. 기존 iran 28컬럼 + 확장 +-- ============================================================ +CREATE TABLE kcg.vessel_analysis_results ( + id BIGSERIAL NOT NULL, + mmsi VARCHAR(20) NOT NULL, + analyzed_at TIMESTAMPTZ NOT NULL, + -- 분류 + vessel_type VARCHAR(30), + confidence NUMERIC(5,4), + fishing_pct NUMERIC(5,4), + cluster_id INT, + season VARCHAR(20), + -- 위치 + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + zone_code VARCHAR(30), -- zone_polygon_master FK + dist_to_baseline_nm NUMERIC(8,2), + -- 행동 분석 + activity_state VARCHAR(20), -- STATIONARY, FISHING, SAILING + ucaf_score NUMERIC(5,4), + ucft_score NUMERIC(5,4), + -- 위협 탐지 + is_dark BOOLEAN DEFAULT false, + gap_duration_min INT, + dark_pattern VARCHAR(30), -- code_master DARK_PATTERN 참조 + spoofing_score NUMERIC(5,4), + bd09_offset_m NUMERIC(8,2), + speed_jump_count INT, + -- 환적 + transship_suspect BOOLEAN DEFAULT false, + transship_pair_mmsi VARCHAR(20), + transship_duration_min INT, + -- 선단 + fleet_cluster_id INT, + fleet_role VARCHAR(20), -- LEADER, FOLLOWER, NOISE + fleet_is_leader BOOLEAN DEFAULT false, + -- 위험도 + risk_score INT, -- 0~100 + risk_level VARCHAR(20), -- CRITICAL, HIGH, MEDIUM, LOW + -- ★ 확장 컬럼 + gear_code VARCHAR(20), -- gear_type_master FK (분류 결과) + violation_categories TEXT[], -- code_master VIOLATION_TYPE 참조 + gear_judgment VARCHAR(30), -- code_master GEAR_JUDGMENT 참조 + permit_status VARCHAR(20), -- 분석 시점 허가 상태 스냅샷 + -- 특징 벡터 (선택) + features JSONB, + -- 메타 + created_at TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (id, analyzed_at) +) PARTITION BY RANGE (analyzed_at); + +-- 파티션 (prediction partition_manager가 자동 생성하지만, 초기 1개 생성) +CREATE TABLE kcg.vessel_analysis_results_default PARTITION OF kcg.vessel_analysis_results DEFAULT; + +CREATE INDEX idx_var_mmsi ON kcg.vessel_analysis_results(mmsi, analyzed_at DESC); +CREATE INDEX idx_var_risk ON kcg.vessel_analysis_results(risk_level, analyzed_at DESC); +CREATE INDEX idx_var_dark ON kcg.vessel_analysis_results(is_dark) WHERE is_dark = true; +CREATE INDEX idx_var_zone ON kcg.vessel_analysis_results(zone_code, analyzed_at DESC); +CREATE INDEX idx_var_gear ON kcg.vessel_analysis_results(gear_code) WHERE gear_code IS NOT NULL; +CREATE INDEX idx_var_violation ON kcg.vessel_analysis_results USING GIN(violation_categories); + +-- ============================================================ +-- 이벤트 허브 (★ 모든 운영 흐름의 단일 진입점) +-- prediction event_generator가 INSERT, backend가 status만 UPDATE +-- ============================================================ +CREATE TABLE kcg.prediction_events ( + id BIGSERIAL PRIMARY KEY, + event_uid VARCHAR(50) UNIQUE NOT NULL, -- EVT-YYYYMMDD-NNNN + occurred_at TIMESTAMPTZ NOT NULL, + level VARCHAR(20) NOT NULL, -- CRITICAL, HIGH, MEDIUM, LOW + category VARCHAR(50) NOT NULL, -- code_master EVENT_CATEGORY 참조 + title VARCHAR(200) NOT NULL, + detail TEXT, + -- 대상 선박 + vessel_mmsi VARCHAR(20), + vessel_name VARCHAR(100), + -- 위치 + area_name VARCHAR(100), + zone_code VARCHAR(30), + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + speed_kn NUMERIC(5,2), + -- 분석 출처 + source_type VARCHAR(50), -- VESSEL_ANALYSIS, GEAR_GROUP, TRANSSHIP, FLEET + source_ref_id BIGINT, -- 원본 분석결과 PK + ai_confidence NUMERIC(5,4), + -- 운영 상태 (backend가 갱신) + status VARCHAR(20) DEFAULT 'NEW', -- code_master EVENT_STATUS 참조 + assignee_id UUID, + assignee_name VARCHAR(100), + acked_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + resolution_note TEXT, + -- dedup + dedup_key VARCHAR(200), -- mmsi + category + window + -- 메타 + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_event_status ON kcg.prediction_events(status, occurred_at DESC); +CREATE INDEX idx_event_level ON kcg.prediction_events(level, occurred_at DESC); +CREATE INDEX idx_event_category ON kcg.prediction_events(category, occurred_at DESC); +CREATE INDEX idx_event_mmsi ON kcg.prediction_events(vessel_mmsi, occurred_at DESC); +CREATE INDEX idx_event_dedup ON kcg.prediction_events(dedup_key, occurred_at DESC); + +-- ============================================================ +-- 이벤트 처리 워크플로우 (상태 변경 이력) +-- ============================================================ +CREATE TABLE kcg.event_workflow ( + id BIGSERIAL PRIMARY KEY, + event_id BIGINT NOT NULL REFERENCES kcg.prediction_events(id), + prev_status VARCHAR(20), + new_status VARCHAR(20) NOT NULL, + actor_id UUID, + actor_name VARCHAR(100), + comment TEXT, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_ew_event ON kcg.event_workflow(event_id, created_at DESC); + +-- ============================================================ +-- AI 알림 발송 이력 +-- ============================================================ +CREATE TABLE kcg.prediction_alerts ( + id BIGSERIAL PRIMARY KEY, + event_id BIGINT NOT NULL REFERENCES kcg.prediction_events(id), + channel VARCHAR(20) NOT NULL, -- DASHBOARD, MOBILE, SMS, EMAIL + recipient VARCHAR(200), + sent_at TIMESTAMPTZ DEFAULT now(), + delivery_status VARCHAR(20) DEFAULT 'SENT', -- SENT, DELIVERED, READ, FAILED + ai_confidence NUMERIC(5,4), + metadata JSONB +); + +CREATE INDEX idx_alert_event ON kcg.prediction_alerts(event_id); + +-- ============================================================ +-- 사전 집계 통계 — 시간별 (최근 48h 보존) +-- ============================================================ +CREATE TABLE kcg.prediction_stats_hourly ( + stat_hour TIMESTAMPTZ PRIMARY KEY, + total_detections INT DEFAULT 0, + by_category JSONB, -- {"EEZ_VIOLATION": 3, "DARK_VESSEL": 1, ...} + by_zone JSONB, -- {"NLL": 5, "EEZ_KR": 8, ...} + by_risk_level JSONB, -- {"CRITICAL": 2, "HIGH": 5, ...} + event_count INT DEFAULT 0, + critical_count INT DEFAULT 0, + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 사전 집계 통계 — 일별 +-- ============================================================ +CREATE TABLE kcg.prediction_stats_daily ( + stat_date DATE PRIMARY KEY, + total_detections INT DEFAULT 0, + by_category JSONB, + by_zone JSONB, + by_risk_level JSONB, + by_gear_type JSONB, -- {"TRAWL_BOTTOM": 12, "GILLNET": 8, ...} + by_violation_type JSONB, -- {"EEZ_VIOLATION": 15, ...} + event_count INT DEFAULT 0, + critical_event_count INT DEFAULT 0, + enforcement_count INT DEFAULT 0, -- 단속 건수 (backend가 주입) + false_positive_count INT DEFAULT 0, + ai_accuracy_pct NUMERIC(5,2), + manual_confirmed_parents INT DEFAULT 0, -- 운영자 모선 확정 건수 + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 사전 집계 통계 — 월별 +-- ============================================================ +CREATE TABLE kcg.prediction_stats_monthly ( + stat_month CHAR(7) PRIMARY KEY, -- 'YYYY-MM' + total_detections INT DEFAULT 0, + total_enforcements INT DEFAULT 0, + by_category JSONB, + by_zone JSONB, + by_risk_level JSONB, + by_gear_type JSONB, + by_violation_type JSONB, + event_count INT DEFAULT 0, + critical_event_count INT DEFAULT 0, + false_positive_count INT DEFAULT 0, + ai_accuracy_pct NUMERIC(5,2), + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- ============================================================ +-- 실시간 KPI (프론트엔드 더미 kpiStore 대체) +-- ============================================================ +CREATE TABLE kcg.prediction_kpi_realtime ( + kpi_key VARCHAR(50) PRIMARY KEY, + kpi_label VARCHAR(100) NOT NULL, + value INT DEFAULT 0, + trend VARCHAR(10), -- up, down, flat + delta_pct NUMERIC(5,2), + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- 시드: 프론트엔드 kpiStore 기반 6개 KPI +INSERT INTO kcg.prediction_kpi_realtime (kpi_key, kpi_label, value, trend, delta_pct) VALUES +('realtime_detection', '실시간 탐지', 47, 'up', 8.2), +('eez_violation', 'EEZ 침범', 18, 'up', 12.5), +('dark_vessel', '다크베셀', 12, 'down', -5.3), +('illegal_transship', '불법환적 의심',8, 'flat', 0.0), +('tracking_active', '추적 중', 15, 'up', 3.1), +('captured_inspected', '나포/검문', 3, 'flat', 0.0); + +-- ============================================================ +-- 위험도 격자 (RiskMap용, 1시간 단위) +-- ============================================================ +CREATE TABLE kcg.prediction_risk_grid ( + cell_id VARCHAR(20) NOT NULL, -- 'lat_lon' 형식 (e.g., '3400_12500') + stat_hour TIMESTAMPTZ NOT NULL, + avg_risk NUMERIC(5,2), + max_risk INT, + vessel_count INT DEFAULT 0, + critical_count INT DEFAULT 0, + metadata JSONB, + PRIMARY KEY (cell_id, stat_hour) +); + +CREATE INDEX idx_grid_hour ON kcg.prediction_risk_grid(stat_hour DESC); + +-- ============================================================ +-- prediction 학습 피드백 입력 (backend가 write, prediction이 read) +-- ============================================================ +CREATE TABLE kcg.prediction_label_input ( + id BIGSERIAL PRIMARY KEY, + input_type VARCHAR(30) NOT NULL, -- PARENT_CONFIRM, PARENT_REJECT, FALSE_POSITIVE, GEAR_CORRECTION + group_key VARCHAR(255), + sub_cluster_id INT, + mmsi VARCHAR(20), + label_value VARCHAR(100), -- 확정된 모선 MMSI, 수정된 어구 코드 등 + confidence NUMERIC(5,4), + actor_id UUID, + consumed_at TIMESTAMPTZ, -- prediction이 사용하면 timestamp 기록 + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_label_unconsumed ON kcg.prediction_label_input(consumed_at) WHERE consumed_at IS NULL; diff --git a/backend/src/main/resources/db/migration/V013__enforcement_operations.sql b/backend/src/main/resources/db/migration/V013__enforcement_operations.sql new file mode 100644 index 0000000..4a4a8ba --- /dev/null +++ b/backend/src/main/resources/db/migration/V013__enforcement_operations.sql @@ -0,0 +1,272 @@ +-- ============================================================ +-- V013: 운영 도메인 테이블 (단속/계획/배치/AI모델) +-- 백엔드 전용 — 운영자 의사결정 데이터 +-- ============================================================ + +-- ============================================================ +-- 단속 이력 (EnforcementHistory 화면) +-- enforcement_records.event_id → prediction_events.id (자동 매칭 + 수동 변경) +-- ============================================================ +CREATE TABLE kcg.enforcement_records ( + id BIGSERIAL PRIMARY KEY, + enf_uid VARCHAR(50) UNIQUE NOT NULL, -- ENF-YYYYMMDD-NNNN + event_id BIGINT REFERENCES kcg.prediction_events(id), + -- 시간/위치 + enforced_at TIMESTAMPTZ NOT NULL, + zone_code VARCHAR(30), -- zone_polygon_master FK + area_name VARCHAR(100), + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + -- 대상 선박 + vessel_mmsi VARCHAR(20), + vessel_name VARCHAR(100), + flag_country VARCHAR(10), + -- 단속 내용 + violation_type VARCHAR(50), -- code_master VIOLATION_TYPE 참조 + action VARCHAR(50) NOT NULL, -- code_master ENFORCEMENT_ACTION 참조 + result VARCHAR(50), -- code_master ENFORCEMENT_RESULT 참조 + -- AI 일치도 + ai_match_status VARCHAR(20), -- code_master AI_MATCH 참조 + ai_confidence NUMERIC(5,4), + -- 수행 함정 + patrol_ship_id BIGINT REFERENCES kcg.patrol_ship_master(ship_id), + -- 담당 + enforced_by UUID, + enforced_by_name VARCHAR(100), + remarks TEXT, + -- 메타 + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_enf_date ON kcg.enforcement_records(enforced_at DESC); +CREATE INDEX idx_enf_event ON kcg.enforcement_records(event_id); +CREATE INDEX idx_enf_mmsi ON kcg.enforcement_records(vessel_mmsi); +CREATE INDEX idx_enf_type ON kcg.enforcement_records(violation_type); + +-- ============================================================ +-- 단속 계획 (EnforcementPlan 화면) +-- ============================================================ +CREATE TABLE kcg.enforcement_plans ( + id BIGSERIAL PRIMARY KEY, + plan_uid VARCHAR(50) UNIQUE NOT NULL, -- PLN-YYYYMMDD-NNNN + title VARCHAR(200) NOT NULL, + zone_code VARCHAR(30), + area_name VARCHAR(100), + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + -- 일정 + planned_date DATE NOT NULL, + planned_from TIMESTAMPTZ, + planned_to TIMESTAMPTZ, + -- 위험도 평가 + risk_level VARCHAR(20), + risk_score INT, + -- 배치 + assigned_ship_count INT DEFAULT 0, + assigned_crew INT DEFAULT 0, + -- 상태 + status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT, APPROVED, IN_PROGRESS, COMPLETED, CANCELLED + alert_status VARCHAR(20), -- 경보 발령 여부 + -- 담당 + created_by UUID, + approved_by UUID, + remarks TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_plan_date ON kcg.enforcement_plans(planned_date DESC); +CREATE INDEX idx_plan_status ON kcg.enforcement_plans(status); + +-- ============================================================ +-- 함정 배치 (PatrolRoute, FleetOptimization) +-- ============================================================ +CREATE TABLE kcg.patrol_assignments ( + id BIGSERIAL PRIMARY KEY, + ship_id BIGINT NOT NULL REFERENCES kcg.patrol_ship_master(ship_id), + plan_id BIGINT REFERENCES kcg.enforcement_plans(id), + -- 배치 해역 + zone_code VARCHAR(30), + -- 기간 + assigned_at TIMESTAMPTZ DEFAULT now(), + completed_at TIMESTAMPTZ, + -- 경로 + waypoints JSONB, -- [{lat, lon, name, eta}] + route_distance_nm NUMERIC(8,2), + estimated_hours NUMERIC(5,2), + fuel_estimate_l NUMERIC(10,2), + -- 상태 + status VARCHAR(20) DEFAULT 'ASSIGNED',-- ASSIGNED, EN_ROUTE, ON_STATION, COMPLETED, CANCELLED + -- 담당 + assigned_by UUID, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_pa_ship ON kcg.patrol_assignments(ship_id, assigned_at DESC); +CREATE INDEX idx_pa_plan ON kcg.patrol_assignments(plan_id); +CREATE INDEX idx_pa_status ON kcg.patrol_assignments(status); + +-- ============================================================ +-- AI 모델 버전 (AIModelManagement) +-- ============================================================ +CREATE TABLE kcg.ai_model_versions ( + id BIGSERIAL PRIMARY KEY, + model_name VARCHAR(100) NOT NULL, -- 'gear_classifier', 'risk_scorer', 'parent_inference' + version VARCHAR(50) NOT NULL, + description TEXT, + -- 성능 메트릭 + accuracy_pct NUMERIC(5,2), + precision_pct NUMERIC(5,2), + recall_pct NUMERIC(5,2), + f1_score NUMERIC(5,4), + -- 상태 + status VARCHAR(20) DEFAULT 'TRAINING', -- TRAINING, EVALUATING, DEPLOYED, ARCHIVED + deployed_at TIMESTAMPTZ, + -- 메타 + train_config JSONB, + eval_metrics JSONB, + created_by UUID, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE kcg.ai_model_metrics ( + id BIGSERIAL PRIMARY KEY, + model_id BIGINT NOT NULL REFERENCES kcg.ai_model_versions(id), + metric_date DATE NOT NULL, + metric_name VARCHAR(50) NOT NULL, -- accuracy, precision, recall, f1, latency_ms + metric_value NUMERIC(10,4), + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_amm_model ON kcg.ai_model_metrics(model_id, metric_date DESC); + +-- ============================================================ +-- 시드: 단속 이력 6건 (프론트 enforcementStore 기반) +-- ============================================================ +INSERT INTO kcg.enforcement_records (enf_uid, enforced_at, zone_code, area_name, + vessel_mmsi, vessel_name, flag_country, violation_type, action, result, + ai_match_status, ai_confidence, patrol_ship_id, remarks) VALUES +('ENF-20260403-0001', '2026-04-03 14:30:00+09', 'NLL', '서해 NLL 인근', + '412345678', '鲁荣渔56555', 'CN', 'EEZ_VIOLATION', 'CAPTURE', 'PUNISHED', + 'MATCH', 0.95, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='3001'), + 'NLL 인근 불법조업 현행범'), +('ENF-20260402-0001', '2026-04-02 09:15:00+09', 'SPECIAL_FISHING_1', '서해 중부', + '412345680', '辽大渔42881', 'CN', 'ILLEGAL_GEAR', 'INSPECT', 'WARNED', + 'PARTIAL', 0.72, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='3005'), + '무허가 선망 사용'), +('ENF-20260401-0001', '2026-04-01 16:45:00+09', 'SPECIAL_FISHING_2', '서해 5도 인근', + '412345679', '鲁荣渔56556', 'CN', 'DARK_VESSEL', 'TRACK', 'REFERRED', + 'MATCH', 0.88, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='1502'), + 'AIS 차단 후 도주, 수사의뢰'), +('ENF-20260331-0001', '2026-03-31 11:20:00+09', 'EEZ_KR', 'EEZ 남부', + '412345682', '闽霞渔09876', 'CN', 'ILLEGAL_TRANSSHIP', 'EVIDENCE', 'PUNISHED', + 'MATCH', 0.91, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='5001'), + '해상 환적 현장 증거 확보'), +('ENF-20260330-0001', '2026-03-30 08:00:00+09', 'WEST_5_ISLANDS', '연평도 서방', + '412345681', '浙象渔23166', 'CN', 'ZONE_DEPARTURE', 'DISPERSE', 'RELEASED', + 'MISMATCH', 0.45, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='1502'), + '조업구역 이탈, 퇴거 조치 (오탐 가능성)'), +('ENF-20260329-0001', '2026-03-29 22:30:00+09', 'NLL', 'NLL 동부', + '412345683', '苏赣渔05512', 'CN', 'EEZ_VIOLATION', 'CAPTURE', 'PUNISHED', + 'MATCH', 0.97, (SELECT ship_id FROM kcg.patrol_ship_master WHERE ship_code='3001'), + '야간 EEZ 침범, 고속 도주 후 나포'); + +-- ============================================================ +-- 시드: 이벤트 15건 (프론트 eventStore 기반) +-- ============================================================ +INSERT INTO kcg.prediction_events (event_uid, occurred_at, level, category, title, detail, + vessel_mmsi, vessel_name, area_name, zone_code, lat, lon, speed_kn, + source_type, ai_confidence, status) VALUES +('EVT-20260407-0001', '2026-04-07 06:12:00+09', 'CRITICAL', 'EEZ_INTRUSION', + 'EEZ 침범 탐지', '중국 어선 鲁荣渔56555 EEZ 침범, 위험도 96', + '412345678', '鲁荣渔56555', '서해 NLL', 'NLL', 37.52, 124.78, 8.5, + 'VESSEL_ANALYSIS', 0.96, 'IN_PROGRESS'), +('EVT-20260407-0002', '2026-04-07 05:48:00+09', 'HIGH', 'DARK_VESSEL', + '다크베셀 장기 소실', 'AIS 신호 180분 소실, 완전차단 패턴', + '412345680', '辽大渔42881', '서해 중부', 'SPECIAL_FISHING_1', 34.85, 125.42, 0.0, + 'VESSEL_ANALYSIS', 0.88, 'ACK'), +('EVT-20260407-0003', '2026-04-07 05:30:00+09', 'HIGH', 'FLEET_CLUSTER', + '선단 밀집 감지', '중국어선 12척 선단 밀집, 리더 선박 식별', + '412345681', '浙象渔23166', 'EEZ 북부', 'EEZ_KR', 36.90, 124.20, 6.2, + 'VESSEL_ANALYSIS', 0.82, 'NEW'), +('EVT-20260407-0004', '2026-04-07 04:55:00+09', 'HIGH', 'ILLEGAL_TRANSSHIP', + '불법환적 의심', '2척 60분 이상 근접 정박, 환적 의심', + '412345682', '闽霞渔09876', '남해', 'SPECIAL_FISHING_3', 34.42, 129.38, 0.5, + 'VESSEL_ANALYSIS', 0.79, 'NEW'), +('EVT-20260407-0005', '2026-04-07 04:30:00+09', 'CRITICAL', 'MMSI_TAMPERING', + 'MMSI 3회 변경', 'MMSI 변경 3회 탐지, GPS 스푸핑 의심', + '412345683', '苏赣渔05512', '서해 5도', 'WEST_5_ISLANDS', 37.38, 124.55, 12.3, + 'VESSEL_ANALYSIS', 0.93, 'IN_PROGRESS'), +('EVT-20260407-0006', '2026-04-07 04:10:00+09', 'MEDIUM', 'ZONE_DEPARTURE', + '구역 이탈', '허가 해역 이탈 감지', + '440012345', '제주해양호', 'EEZ 남부', 'EEZ_KR', 33.15, 126.58, 5.1, + 'VESSEL_ANALYSIS', 0.65, 'NEW'), +('EVT-20260407-0007', '2026-04-07 03:45:00+09', 'LOW', 'AIS_RESUME', + 'AIS 재송출', 'AIS 신호 회복, 120분 소실 후', + '412345679', '鲁荣渔56556', '서해 NLL', 'NLL', 37.45, 124.65, 3.2, + 'VESSEL_ANALYSIS', 0.55, 'RESOLVED'), +('EVT-20260407-0008', '2026-04-07 03:20:00+09', 'MEDIUM', 'SPEED_ANOMALY', + '속력 이상', '급격한 속력 변화 탐지 (2kn → 18kn)', + '412345678', '鲁荣渔56555', '서해 NLL', 'NLL', 37.50, 124.80, 18.0, + 'VESSEL_ANALYSIS', 0.71, 'ACK'), +('EVT-20260407-0009', '2026-04-07 02:55:00+09', 'HIGH', 'AIS_LOSS', + 'AIS 소실', '45분간 AIS 신호 없음, 간헐송출 패턴', + '412345681', '浙象渔23166', 'EEZ 북부', 'EEZ_KR', 36.88, 124.18, 0.0, + 'VESSEL_ANALYSIS', 0.75, 'NEW'), +('EVT-20260407-0010', '2026-04-07 02:30:00+09', 'MEDIUM', 'GEAR_ILLEGAL', + 'EEZ 내 어구 설치', '무허가 저층트롤 어구 그룹 탐지', + '412345680', '辽大渔42881', '서해 중부', 'SPECIAL_FISHING_1', 34.90, 125.40, 2.8, + 'GEAR_GROUP', 0.68, 'NEW'), +('EVT-20260407-0011', '2026-04-07 02:00:00+09', 'MEDIUM', 'FLEET_CLUSTER', + '어구 그룹 신규 탐지', '4척 어구 그룹 신규 형성', + NULL, NULL, '서해 5도', 'WEST_5_ISLANDS', 37.40, 124.50, 0.0, + 'GEAR_GROUP', 0.62, 'NEW'), +('EVT-20260407-0012', '2026-04-07 01:30:00+09', 'LOW', 'ZONE_DEPARTURE', + '접안 후 출항', '검문 대상 선박 출항', + '412345679', '鲁荣渔56556', '인천항', NULL, 37.45, 126.60, 5.0, + 'VESSEL_ANALYSIS', 0.40, 'RESOLVED'), +('EVT-20260407-0013', '2026-04-07 01:00:00+09', 'CRITICAL', 'EEZ_INTRUSION', + 'NLL 근접 EEZ 침범', '위험도 최상위, 야간 침입', + '412345683', '苏赣渔05512', 'NLL 동부', 'NLL', 37.55, 124.90, 11.0, + 'VESSEL_ANALYSIS', 0.98, 'RESOLVED'), +('EVT-20260406-0001', '2026-04-06 22:00:00+09', 'HIGH', 'DARK_VESSEL', + '다크베셀 탐지', 'MMSI 변조 후 AIS 차단', + '412345682', '闽霞渔09876', '남해', 'SPECIAL_FISHING_3', 34.40, 129.35, 0.0, + 'VESSEL_ANALYSIS', 0.85, 'RESOLVED'), +('EVT-20260406-0002', '2026-04-06 20:30:00+09', 'MEDIUM', 'ILLEGAL_TRANSSHIP', + '환적 의심', '야간 근접 정박 90분', + '412345681', '浙象渔23166', 'EEZ 서부', 'EEZ_KR', 35.20, 124.80, 0.3, + 'VESSEL_ANALYSIS', 0.70, 'FALSE_POSITIVE'); + +-- ============================================================ +-- 시드: 단속 계획 5건 (프론트 enforcementStore 기반) +-- ============================================================ +INSERT INTO kcg.enforcement_plans (plan_uid, title, zone_code, area_name, + lat, lon, planned_date, risk_level, risk_score, + assigned_ship_count, assigned_crew, status, alert_status) VALUES +('PLN-20260408-0001', 'NLL 집중 단속', 'NLL', '서해 NLL', 37.52, 124.78, + '2026-04-08', 'CRITICAL', 92, 3, 180, 'APPROVED', '경보 발령'), +('PLN-20260409-0001', 'EEZ 북부 순찰', 'EEZ_KR', 'EEZ 북부', 36.90, 124.20, + '2026-04-09', 'HIGH', 78, 2, 120, 'DRAFT', NULL), +('PLN-20260410-0001', '서해 5도 초계', 'WEST_5_ISLANDS', '서해 5도', 37.38, 124.55, + '2026-04-10', 'HIGH', 85, 2, 100, 'APPROVED', '주의'), +('PLN-20260411-0001', '남해 환적 감시', 'SPECIAL_FISHING_3', '남해', 34.42, 129.38, + '2026-04-11', 'MEDIUM', 65, 1, 60, 'DRAFT', NULL), +('PLN-20260412-0001', '서해 중부 야간 작전', 'SPECIAL_FISHING_1', '서해 중부', 34.85, 125.42, + '2026-04-12', 'HIGH', 80, 2, 140, 'APPROVED', '경보 발령'); + +-- ============================================================ +-- 시드: 일별/월별 통계 (프론트 kpiStore/monthlyTrends 기반) +-- ============================================================ +INSERT INTO kcg.prediction_stats_monthly (stat_month, total_detections, total_enforcements, + by_violation_type, event_count, critical_event_count, false_positive_count, ai_accuracy_pct) VALUES +('2025-10', 128, 42, '{"EEZ_VIOLATION":45,"DARK_VESSEL":32,"MMSI_TAMPERING":23,"ILLEGAL_TRANSSHIP":15,"ILLEGAL_GEAR":13}', 85, 12, 16, 81.0), +('2025-11', 145, 38, '{"EEZ_VIOLATION":51,"DARK_VESSEL":36,"MMSI_TAMPERING":26,"ILLEGAL_TRANSSHIP":17,"ILLEGAL_GEAR":15}', 97, 15, 14, 84.0), +('2025-12', 167, 55, '{"EEZ_VIOLATION":59,"DARK_VESSEL":42,"MMSI_TAMPERING":30,"ILLEGAL_TRANSSHIP":20,"ILLEGAL_GEAR":16}', 112, 18, 12, 86.0), +('2026-01', 189, 61, '{"EEZ_VIOLATION":66,"DARK_VESSEL":47,"MMSI_TAMPERING":34,"ILLEGAL_TRANSSHIP":23,"ILLEGAL_GEAR":19}', 126, 22, 10, 88.0), +('2026-02', 156, 48, '{"EEZ_VIOLATION":55,"DARK_VESSEL":39,"MMSI_TAMPERING":28,"ILLEGAL_TRANSSHIP":19,"ILLEGAL_GEAR":15}', 104, 17, 9, 89.0), +('2026-03', 172, 52, '{"EEZ_VIOLATION":60,"DARK_VESSEL":43,"MMSI_TAMPERING":31,"ILLEGAL_TRANSSHIP":21,"ILLEGAL_GEAR":17}', 115, 19, 8, 90.0), +('2026-04', 67, 15, '{"EEZ_VIOLATION":24,"DARK_VESSEL":17,"MMSI_TAMPERING":12,"ILLEGAL_TRANSSHIP":8,"ILLEGAL_GEAR":6}', 45, 8, 2, 93.0); diff --git a/database/migration/README.md b/database/migration/README.md index 98f608e..c3df544 100644 --- a/database/migration/README.md +++ b/database/migration/README.md @@ -1,30 +1,73 @@ # Database Migrations -PostgreSQL 마이그레이션 (Flyway 형식). +> ⚠️ **실제 SQL 파일 위치**: [`backend/src/main/resources/db/migration/`](../../backend/src/main/resources/db/migration/) +> +> Spring Boot Flyway 표준 위치를 따르므로 SQL 파일은 백엔드 모듈 안에 있습니다. +> Spring Boot 기동 시 Flyway가 자동으로 적용합니다. ## DB 정보 -- DB Name: `kcgaidb` -- User: `kcg-app` -- Schema: `kcg` +- **DB Name**: `kcgaidb` +- **User**: `kcg-app` +- **Schema**: `kcg` +- **Host**: `211.208.115.83:5432` -## 마이그레이션 파일 (Phase 2에서 작성) +## 적용된 마이그레이션 (V001~V013) + +### Phase 1~8: 인증/권한/감사 (V001~V007) | 파일 | 내용 | |---|---| -| `V001__auth_init.sql` | 사용자, 조직, 역할, 로그인 이력 | +| `V001__auth_init.sql` | 인증/조직/역할/사용자-역할/로그인 이력 | | `V002__perm_tree.sql` | 권한 트리 + 권한 매트릭스 | -| `V003__perm_seed.sql` | 초기 역할 + 트리 노드 시드 | -| `V004__access_logs.sql` | 감사로그, 접근 이력 | -| `V005__parent_workflow.sql` | 모선 워크플로우 (운영자 결정/제외/학습 세션) | +| `V003__perm_seed.sql` | 초기 역할 5종 + 트리 노드 45개 + 권한 매트릭스 시드 | +| `V004__access_logs.sql` | 감사로그/접근이력 | +| `V005__parent_workflow.sql` | 모선 워크플로우 (resolution/review_log/exclusions/label_sessions) | +| `V006__demo_accounts.sql` | 데모 계정 5종 | +| `V007__perm_tree_label_align.sql` | 트리 노드 명칭을 사이드바 i18n 라벨과 일치 | + +### S1: 마스터 데이터 + Prediction 기반 (V008~V013) + +| 파일 | 내용 | +|---|---| +| `V008__code_master.sql` | 계층형 코드 마스터 (12그룹, 72코드: 위반유형/이벤트/단속/허가/함정 등) | +| `V009__gear_type_master.sql` | 어구 유형 마스터 6종 (분류 룰 + 합법성 기준) | +| `V010__zone_polygon_master.sql` | 해역 폴리곤 마스터 (PostGIS GEOMETRY, 8개 해역 시드) | +| `V011__vessel_permit_patrol.sql` | 어선 허가 마스터 + 함정 마스터 + fleet_companies (선박 9척, 함정 6척) | +| `V012__prediction_events_stats.sql` | vessel_analysis_results(파티션) + 이벤트 허브 + 알림 + 통계(시/일/월) + KPI + 위험격자 + 학습피드백 | +| `V013__enforcement_operations.sql` | 단속 이력/계획 + 함정 배치 + AI모델 버전/메트릭 (시드 포함) | ## 실행 방법 -```bash -# DB 생성 (1회) -psql -U postgres -c "CREATE DATABASE kcgaidb;" -psql -U postgres -c "CREATE USER \"kcg-app\" WITH PASSWORD 'Kcg2026ai';" -psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE kcgaidb TO \"kcg-app\";" +### 최초 1회 - DB/사용자 생성 (관리자 권한 필요) +```sql +-- snp 관리자 계정으로 접속 +psql -h 211.208.115.83 -U snp -d postgres -# 마이그레이션은 backend Spring Boot가 기동 시 자동 실행 (Flyway) +CREATE DATABASE kcgaidb; +CREATE USER "kcg-app" WITH PASSWORD 'Kcg2026ai'; +GRANT ALL PRIVILEGES ON DATABASE kcgaidb TO "kcg-app"; + +\c kcgaidb +CREATE SCHEMA IF NOT EXISTS kcg AUTHORIZATION "kcg-app"; +GRANT ALL ON SCHEMA kcg TO "kcg-app"; +ALTER DATABASE kcgaidb OWNER TO "kcg-app"; +``` + +### 마이그레이션 실행 (자동) +백엔드 기동 시 Flyway가 자동 적용: +```bash cd backend && ./mvnw spring-boot:run ``` + +### 수동 적용 +```bash +cd backend && ./mvnw flyway:migrate -Dflyway.url=jdbc:postgresql://211.208.115.83:5432/kcgaidb -Dflyway.user=kcg-app -Dflyway.password=Kcg2026ai -Dflyway.schemas=kcg +``` + +### Checksum 불일치 시 (마이그레이션 파일 수정 후) +```bash +cd backend && ./mvnw flyway:repair -Dflyway.url=... (위와 동일) +``` + +## 신규 마이그레이션 추가 +[`backend/src/main/resources/db/migration/`](../../backend/src/main/resources/db/migration/)에 `V00N__설명.sql` 형식으로 추가하면 다음 기동 시 자동 적용됩니다.