feat(db): Detection Model Registry 스키마 (V034, Phase 1-1) #87
@ -0,0 +1,226 @@
|
||||
-- V034: Detection Model Registry 기반 스키마 (Phase 1-1)
|
||||
--
|
||||
-- prediction 모듈의 17 탐지 알고리즘을 "명시적 모델 단위" 로 분리하고,
|
||||
-- 운영자가 프론트엔드에서 파라미터·버전을 관리할 수 있도록 하는 기반 인프라.
|
||||
-- 핵심 개념:
|
||||
-- * Model — 독립 탐지 단위 (dark_suspicion / gear_violation_g01_g06 등)
|
||||
-- * Version — 같은 model 의 파라미터 스냅샷. 라이프사이클 DRAFT→ACTIVE→ARCHIVED
|
||||
-- * Role — PRIMARY(운영 반영, 최대 1개) / SHADOW·CHALLENGER(관측용, N개)
|
||||
-- * DAG — model_id 간 선행·후행 의존성 (선행 PRIMARY 결과만 후행에 주입)
|
||||
--
|
||||
-- 기존 V014 correlation_param_models 패턴의 일반화 — JSONB params + is_active 를
|
||||
-- model_id × version × role 차원으로 확장. V014 는 Phase 2 에서 이 스키마로
|
||||
-- 이주 후 2~3 릴리즈 후 deprecate 예정.
|
||||
--
|
||||
-- 참고: docs/prediction-analysis.md §7 P1/P2 + .claude/plans/vast-tinkering-knuth.md
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 1. detection_models — 모델 카탈로그 (고정 메타)
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_models (
|
||||
model_id VARCHAR(64) PRIMARY KEY,
|
||||
display_name VARCHAR(200) NOT NULL,
|
||||
tier INT NOT NULL, -- 1(Primitive) ~ 5(Meta)
|
||||
category VARCHAR(40), -- DARK_VESSEL/GEAR/PATTERN/TRANSSHIP/META
|
||||
description TEXT,
|
||||
entry_module VARCHAR(200) NOT NULL, -- 'prediction.algorithms.dark_vessel'
|
||||
entry_callable VARCHAR(100) NOT NULL, -- 'compute_dark_suspicion'
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 전역 ON/OFF 킬스위치
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
CHECK (tier BETWEEN 1 AND 5)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_models_tier
|
||||
ON kcg.detection_models(tier, category);
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_models_enabled
|
||||
ON kcg.detection_models(is_enabled) WHERE is_enabled = TRUE;
|
||||
|
||||
COMMENT ON TABLE kcg.detection_models IS
|
||||
'탐지 모델 카탈로그 — prediction Model 인터페이스의 단위 정의';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 2. detection_model_dependencies — 모델 간 DAG 엣지
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_dependencies (
|
||||
model_id VARCHAR(64) NOT NULL
|
||||
REFERENCES kcg.detection_models(model_id) ON DELETE CASCADE,
|
||||
depends_on VARCHAR(64) NOT NULL
|
||||
REFERENCES kcg.detection_models(model_id) ON DELETE RESTRICT,
|
||||
input_key VARCHAR(64) NOT NULL, -- 'gap_info' / 'pair_result' 등
|
||||
PRIMARY KEY (model_id, depends_on, input_key),
|
||||
CHECK (model_id <> depends_on) -- self-loop 금지
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_model_deps_reverse
|
||||
ON kcg.detection_model_dependencies(depends_on);
|
||||
|
||||
COMMENT ON TABLE kcg.detection_model_dependencies IS
|
||||
'모델 실행 DAG — 선행 model PRIMARY 결과의 어떤 key 를 후행이 입력으로 쓰는지';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 3. detection_model_versions — 파라미터 스냅샷 + 라이프사이클 + role
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_id VARCHAR(64) NOT NULL
|
||||
REFERENCES kcg.detection_models(model_id) ON DELETE CASCADE,
|
||||
version VARCHAR(32) NOT NULL, -- SemVer 권장 '1.0.0', 자유도 허용
|
||||
status VARCHAR(20) NOT NULL, -- DRAFT / TESTING / ACTIVE / ARCHIVED
|
||||
role VARCHAR(20), -- PRIMARY / SHADOW / CHALLENGER (ACTIVE 일 때만)
|
||||
params JSONB NOT NULL, -- 임계값·가중치·상수
|
||||
notes TEXT,
|
||||
traffic_weight INT DEFAULT 0, -- CHALLENGER split (0~100, 후속 기능)
|
||||
parent_version_id BIGINT
|
||||
REFERENCES kcg.detection_model_versions(id) ON DELETE SET NULL,
|
||||
created_by UUID,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
activated_at TIMESTAMPTZ,
|
||||
archived_at TIMESTAMPTZ,
|
||||
UNIQUE (model_id, version),
|
||||
CHECK (status IN ('DRAFT','TESTING','ACTIVE','ARCHIVED')),
|
||||
CHECK (role IS NULL OR role IN ('PRIMARY','SHADOW','CHALLENGER')),
|
||||
CHECK (status <> 'ACTIVE' OR role IS NOT NULL), -- ACTIVE → role 필수
|
||||
CHECK (traffic_weight BETWEEN 0 AND 100)
|
||||
);
|
||||
|
||||
-- 한 model_id 에 PRIMARY×ACTIVE 는 최대 1건 (운영 반영 보호)
|
||||
-- SHADOW/CHALLENGER×ACTIVE 는 N 건 허용
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_detection_model_primary
|
||||
ON kcg.detection_model_versions(model_id)
|
||||
WHERE status = 'ACTIVE' AND role = 'PRIMARY';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_model_versions_active
|
||||
ON kcg.detection_model_versions(model_id, status)
|
||||
WHERE status = 'ACTIVE';
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_model_versions_status
|
||||
ON kcg.detection_model_versions(status, model_id);
|
||||
|
||||
COMMENT ON TABLE kcg.detection_model_versions IS
|
||||
'모델 버전 + 파라미터 + 라이프사이클. ACTIVE 는 role=PRIMARY 1개 + SHADOW/CHALLENGER N개';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 4. detection_model_run_outputs — 버전별 입력·출력 비교 (파티션)
|
||||
-- 같은 (cycle, model_id, input_ref) 로 PRIMARY vs SHADOW JOIN 비교
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_run_outputs (
|
||||
id BIGSERIAL,
|
||||
cycle_started_at TIMESTAMPTZ NOT NULL, -- 같은 사이클 식별 (모든 버전 공유)
|
||||
model_id VARCHAR(64) NOT NULL,
|
||||
version_id BIGINT NOT NULL,
|
||||
role VARCHAR(20) NOT NULL, -- PRIMARY / SHADOW / CHALLENGER
|
||||
input_ref JSONB NOT NULL, -- {mmsi, analyzed_at} 등
|
||||
outputs JSONB NOT NULL, -- 모델 반환값 snapshot
|
||||
cycle_duration_ms INT,
|
||||
recorded_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (id, cycle_started_at),
|
||||
CHECK (role IN ('PRIMARY','SHADOW','CHALLENGER'))
|
||||
) PARTITION BY RANGE (cycle_started_at);
|
||||
|
||||
-- 2026-04 파티션 (이후는 partition_manager 가 월별 자동 생성)
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_run_outputs_2026_04
|
||||
PARTITION OF kcg.detection_model_run_outputs
|
||||
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_run_outputs_2026_05
|
||||
PARTITION OF kcg.detection_model_run_outputs
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_run_outputs_compare
|
||||
ON kcg.detection_model_run_outputs(model_id, cycle_started_at DESC, role);
|
||||
CREATE INDEX IF NOT EXISTS idx_run_outputs_version
|
||||
ON kcg.detection_model_run_outputs(version_id, cycle_started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_run_outputs_input
|
||||
ON kcg.detection_model_run_outputs USING GIN (input_ref jsonb_path_ops);
|
||||
|
||||
COMMENT ON TABLE kcg.detection_model_run_outputs IS
|
||||
'버전별 실행 결과 원시 snapshot — PRIMARY vs SHADOW diff 분석용. 월별 파티션, 기본 7일 retention';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 5. detection_model_metrics — 사이클 단위 집계 메트릭
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS kcg.detection_model_metrics (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_id VARCHAR(64) NOT NULL,
|
||||
version_id BIGINT NOT NULL
|
||||
REFERENCES kcg.detection_model_versions(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL,
|
||||
metric_key VARCHAR(64) NOT NULL, -- cycle_duration_ms/detected_count/tier_critical_count
|
||||
metric_value NUMERIC,
|
||||
cycle_started_at TIMESTAMPTZ NOT NULL,
|
||||
recorded_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_model_metrics_lookup
|
||||
ON kcg.detection_model_metrics(model_id, version_id, cycle_started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_detection_model_metrics_cycle
|
||||
ON kcg.detection_model_metrics(cycle_started_at DESC, model_id);
|
||||
|
||||
COMMENT ON TABLE kcg.detection_model_metrics IS
|
||||
'모델 실행 집계 메트릭 — dashboard/compare API 의 관측 소스';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 6. Compare VIEW — PRIMARY vs SHADOW 결과를 같은 입력 단위로 JOIN
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
CREATE OR REPLACE VIEW kcg.v_detection_model_comparison AS
|
||||
SELECT
|
||||
p.cycle_started_at,
|
||||
p.model_id,
|
||||
p.input_ref,
|
||||
p.outputs AS primary_outputs,
|
||||
s.outputs AS shadow_outputs,
|
||||
p.version_id AS primary_version_id,
|
||||
s.version_id AS shadow_version_id,
|
||||
s.role AS shadow_role
|
||||
FROM kcg.detection_model_run_outputs p
|
||||
JOIN kcg.detection_model_run_outputs s
|
||||
ON p.cycle_started_at = s.cycle_started_at
|
||||
AND p.model_id = s.model_id
|
||||
AND p.input_ref = s.input_ref
|
||||
WHERE p.role = 'PRIMARY'
|
||||
AND s.role IN ('SHADOW', 'CHALLENGER');
|
||||
|
||||
COMMENT ON VIEW kcg.v_detection_model_comparison IS
|
||||
'PRIMARY × SHADOW/CHALLENGER 동일 입력 결과 비교 — backend Compare API 의 집계 소스';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 7. 권한 트리 / 메뉴 슬롯
|
||||
-- ai-operations:detection-models — AI 모델관리 하위 혹은 별도 노드.
|
||||
-- 기존 ai-operations:ai-model(nav=200) 과 구분하기 위해 별도 nav_sort=250.
|
||||
-- parent_cd='admin' (AI 운영 그룹 평탄화 패턴 따름)
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
INSERT INTO kcg.auth_perm_tree
|
||||
(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord,
|
||||
url_path, label_key, component_key, nav_sort, labels)
|
||||
VALUES
|
||||
('ai-operations:detection-models', 'admin', '탐지 모델 관리', 1, 25,
|
||||
'/detection-models', 'nav.detectionModels',
|
||||
'features/ai-operations/DetectionModelManagement', 250,
|
||||
'{"ko":"탐지 모델 관리","en":"Detection Models"}'::jsonb)
|
||||
ON CONFLICT (rsrc_cd) DO NOTHING;
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
-- 8. 권한 부여
|
||||
-- ADMIN : 5 ops 전부 (모델 카탈로그 관리)
|
||||
-- OPERATOR : READ + UPDATE (SHADOW activate 정도. promote-primary 는 ADMIN 만)
|
||||
-- ANALYST/VIEWER: READ (파라미터 조회 + Compare 분석)
|
||||
-- FIELD : (생략 — 현장 단속 담당, 모델 관리 불필요)
|
||||
-- ══════════════════════════════════════════════════════════════════
|
||||
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
|
||||
SELECT r.role_sn, 'ai-operations:detection-models', 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;
|
||||
|
||||
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
|
||||
SELECT r.role_sn, 'ai-operations:detection-models', op.oper_cd, 'Y'
|
||||
FROM kcg.auth_role r
|
||||
CROSS JOIN (VALUES ('READ'), ('UPDATE')) AS op(oper_cd)
|
||||
WHERE r.role_cd = 'OPERATOR'
|
||||
ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING;
|
||||
|
||||
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
|
||||
SELECT r.role_sn, 'ai-operations:detection-models', 'READ', 'Y'
|
||||
FROM kcg.auth_role r
|
||||
WHERE r.role_cd IN ('ANALYST', 'VIEWER')
|
||||
ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING;
|
||||
@ -7,6 +7,9 @@
|
||||
### 수정
|
||||
- **모니터링/디자인시스템 런타임 에러 해소** — `/monitoring` 의 `SystemStatusPanel` 에서 `stats.total.toLocaleString()` 호출이 백엔드 응답 shape 이슈로 `stats.total` 이 undefined 일 때 Uncaught TypeError 로 크래시하던 문제 null-safe 로 해소(`stats?.total != null`). `/design-system.html` 의 `CatalogBadges` 가 `PERFORMANCE_STATUS_META` 의 `label: {ko, en}` 객체를 그대로 Badge children 으로 주입해 "Objects are not valid as a React child" 를 던지고 `code` 필드 부재로 key 중복 경고가 함께 뜨던 문제 해소 — `Object.entries` 순회 + `AnyMeta.label` 을 `string | {ko,en}` 로 확장 + getKoLabel/getEnLabel 에 label 객체 케이스 추가
|
||||
|
||||
### 추가
|
||||
- **Detection Model Registry DB 스키마 (V034, Phase 1-1)** — prediction 17 탐지 알고리즘을 "명시적 모델 단위" 로 분리하고 프론트엔드에서 파라미터·버전·가중치를 관리할 수 있는 기반 인프라. 테이블 4종(`detection_models` 카탈로그 / `detection_model_dependencies` DAG / `detection_model_versions` 파라미터 스냅샷·라이프사이클·role / `detection_model_run_outputs` 월별 파티션) + 뷰 1개(`v_detection_model_comparison` PRIMARY×SHADOW JOIN). 한 모델을 서로 다른 파라미터로 **동시 실행**(PRIMARY 1 + SHADOW/CHALLENGER N) 지원, ACTIVE×PRIMARY 는 UNIQUE partial index 로 1건 보호. 권한 트리 `ai-operations:detection-models`(nav_sort=250) + ADMIN 5 ops / OPERATOR READ+UPDATE / ANALYST·VIEWER READ. (후속: Phase 1-2 Model Registry + DAG Executor, Phase 2 PoC 5 모델 마이그레이션)
|
||||
|
||||
### 추가
|
||||
- **환적 의심 전용 탐지 페이지 신설 (Phase 0-3)** — `/transshipment` 경로에 READ 전용 대시보드 추가. prediction `algorithms/transshipment.py` 의 5단계 필터 파이프라인 결과(is_transship_suspect=true)를 전체 목록·집계·상세 수준으로 조회. KPI 5장(Total + Transship tier CRITICAL/HIGH/MEDIUM + 종합 위험 CRITICAL) + DataTable 8컬럼 + 필터(hours/level/mmsi) + features JSON 상세. 기존 `/api/analysis/transship` + `getTransshipSuspects` 재사용해 backend 변경 없음. V033 마이그레이션으로 `detection:transshipment` 권한 트리 + 전 역할 READ 부여. (docs/prediction-analysis.md P1 UI 미노출 탐지 해소 — 2/2)
|
||||
- **불법 조업 이벤트 전용 페이지 신설 (Phase 0-2)** — `/illegal-fishing` 경로에 READ 전용 대시보드 추가. event_generator 가 생산하는 `GEAR_ILLEGAL`(G-01/G-05/G-06) + `EEZ_INTRUSION`(영해·접속수역) + `ZONE_DEPARTURE`(특정수역 진입) 3 카테고리를 한 화면에서 통합 조회. 심각도 KPI 5장 + 카테고리별 3장 + DataTable(7컬럼) + 필터(category/level/mmsi) + JSON features 상세 패널 + EventList 네비게이션. 기존 `/api/events` 를 category 다중 병렬 조회로 래핑하여 backend 변경 없이 구현. V032 마이그레이션으로 `detection:illegal-fishing` 권한 트리 + 전 역할 READ 부여 (운영자 처리 액션은 EventList 경유)
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user