Codex Lab 환경(iran-airstrike-replay-codex)에서 검증 완료된 어구 모선 자동 추론 + 검토 워크플로우 전체를 이식. ## Python (prediction/) - gear_parent_inference(1,428줄): 다층 점수 모델 (correlation + name + track + prior bonus) - gear_parent_episode(631줄): Episode 연속성 (Jaccard + 공간거리) - gear_name_rules: 모선 이름 정규화 + 4자 미만 필터 - scheduler: 추론 호출 단계 추가 (4.8) - fleet_tracker/kcgdb: SQL qualified_table() 동적화 - gear_correlation: timestamp 필드 추가 ## DB (database/migration/ 012~015) - 후보 스냅샷, resolution, episode, 라벨 세션, 제외 관리 테이블 9개 + VIEW 2개 ## Backend (Java) - 12개 DTO/Controller (ParentInferenceWorkflowController 등) - GroupPolygonService: parent_resolution LEFT JOIN + 15개 API 메서드 ## Frontend - ParentReviewPanel: 모선 검토 대시보드 - vesselAnalysis: 10개 신규 API 함수 + 6개 타입 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
177 lines
6.6 KiB
PL/PgSQL
177 lines
6.6 KiB
PL/PgSQL
-- 012: 어구 그룹 모선 추론 저장소 + sub_cluster/resolution 스키마 정합성
|
|
|
|
SET search_path TO kcg, public;
|
|
|
|
-- ── live lab과 repo 마이그레이션 정합성 맞추기 ─────────────────────
|
|
|
|
ALTER TABLE kcg.group_polygon_snapshots
|
|
ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0;
|
|
|
|
ALTER TABLE kcg.group_polygon_snapshots
|
|
ADD COLUMN IF NOT EXISTS resolution VARCHAR(20) NOT NULL DEFAULT '6h';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gps_type_res_time
|
|
ON kcg.group_polygon_snapshots(group_type, resolution, snapshot_time DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gps_key_res_time
|
|
ON kcg.group_polygon_snapshots(group_key, resolution, snapshot_time DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gps_key_sub_time
|
|
ON kcg.group_polygon_snapshots(group_key, sub_cluster_id, snapshot_time DESC);
|
|
|
|
ALTER TABLE kcg.gear_correlation_raw_metrics
|
|
ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_raw_metrics_group_sub_time
|
|
ON kcg.gear_correlation_raw_metrics(group_key, sub_cluster_id, observed_at DESC);
|
|
|
|
ALTER TABLE kcg.gear_correlation_scores
|
|
ADD COLUMN IF NOT EXISTS sub_cluster_id SMALLINT NOT NULL DEFAULT 0;
|
|
|
|
ALTER TABLE kcg.gear_correlation_scores
|
|
DROP CONSTRAINT IF EXISTS gear_correlation_scores_model_id_group_key_target_mmsi_key;
|
|
|
|
DROP INDEX IF EXISTS kcg.gear_correlation_scores_model_id_group_key_target_mmsi_key;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint
|
|
WHERE connamespace = 'kcg'::regnamespace
|
|
AND conrelid = 'kcg.gear_correlation_scores'::regclass
|
|
AND conname = 'gear_correlation_scores_unique'
|
|
) THEN
|
|
ALTER TABLE kcg.gear_correlation_scores
|
|
ADD CONSTRAINT gear_correlation_scores_unique
|
|
UNIQUE (model_id, group_key, sub_cluster_id, target_mmsi);
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gc_model_group_sub
|
|
ON kcg.gear_correlation_scores(model_id, group_key, sub_cluster_id, current_score DESC);
|
|
|
|
-- ── 그룹 단위 모선 추론 저장소 ─────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_candidate_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
group_key VARCHAR(100) NOT NULL,
|
|
sub_cluster_id SMALLINT NOT NULL DEFAULT 0,
|
|
parent_name TEXT NOT NULL,
|
|
candidate_mmsi VARCHAR(20) NOT NULL,
|
|
candidate_name VARCHAR(200),
|
|
candidate_vessel_id INT REFERENCES kcg.fleet_vessels(id) ON DELETE SET NULL,
|
|
rank INT NOT NULL,
|
|
candidate_source VARCHAR(100) NOT NULL,
|
|
model_id INT REFERENCES kcg.correlation_param_models(id) ON DELETE SET NULL,
|
|
model_name VARCHAR(100),
|
|
base_corr_score DOUBLE PRECISION DEFAULT 0,
|
|
name_match_score DOUBLE PRECISION DEFAULT 0,
|
|
track_similarity_score DOUBLE PRECISION DEFAULT 0,
|
|
visit_score_6h DOUBLE PRECISION DEFAULT 0,
|
|
proximity_score_6h DOUBLE PRECISION DEFAULT 0,
|
|
activity_sync_score_6h DOUBLE PRECISION DEFAULT 0,
|
|
stability_score DOUBLE PRECISION DEFAULT 0,
|
|
registry_bonus DOUBLE PRECISION DEFAULT 0,
|
|
final_score DOUBLE PRECISION DEFAULT 0,
|
|
margin_from_top DOUBLE PRECISION DEFAULT 0,
|
|
evidence JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (observed_at, group_key, sub_cluster_id, candidate_mmsi)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpcs_group_time
|
|
ON kcg.gear_group_parent_candidate_snapshots(group_key, sub_cluster_id, observed_at DESC, rank ASC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpcs_candidate
|
|
ON kcg.gear_group_parent_candidate_snapshots(candidate_mmsi, observed_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_resolution (
|
|
group_key VARCHAR(100) NOT NULL,
|
|
sub_cluster_id SMALLINT NOT NULL DEFAULT 0,
|
|
parent_name TEXT NOT NULL,
|
|
normalized_parent_name VARCHAR(200),
|
|
status VARCHAR(40) NOT NULL,
|
|
selected_parent_mmsi VARCHAR(20),
|
|
selected_parent_name VARCHAR(200),
|
|
selected_vessel_id INT REFERENCES kcg.fleet_vessels(id) ON DELETE SET NULL,
|
|
confidence DOUBLE PRECISION,
|
|
decision_source VARCHAR(40),
|
|
top_score DOUBLE PRECISION DEFAULT 0,
|
|
second_score DOUBLE PRECISION DEFAULT 0,
|
|
score_margin DOUBLE PRECISION DEFAULT 0,
|
|
stable_cycles INT DEFAULT 0,
|
|
last_evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_promoted_at TIMESTAMPTZ,
|
|
approved_by VARCHAR(100),
|
|
approved_at TIMESTAMPTZ,
|
|
manual_comment TEXT,
|
|
rejected_candidate_mmsi VARCHAR(20),
|
|
rejected_at TIMESTAMPTZ,
|
|
evidence_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (group_key, sub_cluster_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpr_status
|
|
ON kcg.gear_group_parent_resolution(status, last_evaluated_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpr_parent
|
|
ON kcg.gear_group_parent_resolution(selected_parent_mmsi);
|
|
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_review_log (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
group_key VARCHAR(100) NOT NULL,
|
|
sub_cluster_id SMALLINT NOT NULL DEFAULT 0,
|
|
action VARCHAR(20) NOT NULL,
|
|
selected_parent_mmsi VARCHAR(20),
|
|
actor VARCHAR(100) NOT NULL,
|
|
comment TEXT,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggprl_group_time
|
|
ON kcg.gear_group_parent_review_log(group_key, sub_cluster_id, created_at DESC);
|
|
|
|
-- ── copied schema 환경의 sequence 정렬 ─────────────────────────────
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.fleet_companies', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.fleet_companies), 1),
|
|
TRUE
|
|
);
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.fleet_vessels', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.fleet_vessels), 1),
|
|
TRUE
|
|
);
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.gear_identity_log', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.gear_identity_log), 1),
|
|
TRUE
|
|
);
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.fleet_tracking_snapshot', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.fleet_tracking_snapshot), 1),
|
|
TRUE
|
|
);
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.group_polygon_snapshots', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.group_polygon_snapshots), 1),
|
|
TRUE
|
|
);
|
|
|
|
SELECT setval(
|
|
pg_get_serial_sequence('kcg.gear_correlation_scores', 'id'),
|
|
COALESCE((SELECT MAX(id) FROM kcg.gear_correlation_scores), 1),
|
|
TRUE
|
|
);
|