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>
707 lines
16 KiB
Markdown
707 lines
16 KiB
Markdown
# Gear Parent Inference Workflow V2 Phase 1 Spec
|
|
|
|
## 목적
|
|
|
|
이 문서는 `GEAR-PARENT-INFERENCE-WORKFLOW-V2.md`의 첫 구현 단계를 바로 개발할 수 있는 수준으로 구체화한 명세다.
|
|
|
|
Phase 1 범위는 아래로 제한한다.
|
|
|
|
- DB 마이그레이션
|
|
- backend API 계약
|
|
- prediction exclusion/label read-write 지점
|
|
- 프론트의 최소 계약 변화
|
|
|
|
이번 단계에서는 실제 자동화/LLM 연결은 다루지 않는다.
|
|
|
|
## 범위 요약
|
|
|
|
### 포함
|
|
|
|
- 그룹 단위 후보 제외 `1/3/5일`
|
|
- 전역 후보 제외
|
|
- 정답 라벨 세션 `1/3/5일`
|
|
- 라벨 세션 기간 동안 cycle별 tracking 기록
|
|
- active exclusion을 parent inference 후보 생성에 반영
|
|
- exclusion/label 관리 API
|
|
|
|
### 제외
|
|
|
|
- 운영 `kcg` 스키마 반영
|
|
- 기존 `gear_correlation_scores` 산식 변경
|
|
- LLM reviewer
|
|
- label session의 anchor 기반 재매칭 보강
|
|
- UI 고도화 화면 전부
|
|
|
|
## 구현 원칙
|
|
|
|
1. 기존 자동 추론 저장소는 유지한다.
|
|
2. 새 사람 판단 데이터는 별도 테이블에 저장한다.
|
|
3. Phase 1에서는 `group_key + sub_cluster_id`를 세션 식별 기준으로 고정한다.
|
|
4. 기존 `CONFIRM/REJECT/RESET` API는 삭제하지 않지만, 새 UI에서는 사용하지 않는다.
|
|
5. 새 API와 prediction 로직은 `kcg_lab` 기준으로만 먼저 구현한다.
|
|
|
|
## DB 명세
|
|
|
|
## 1. `gear_parent_candidate_exclusions`
|
|
|
|
### 목적
|
|
|
|
- 그룹 단위 후보 제외와 전역 후보 제외를 단일 저장소에서 관리
|
|
|
|
### DDL 초안
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_parent_candidate_exclusions (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
scope_type VARCHAR(16) NOT NULL,
|
|
group_key VARCHAR(100),
|
|
sub_cluster_id SMALLINT,
|
|
candidate_mmsi VARCHAR(20) NOT NULL,
|
|
reason_type VARCHAR(32) NOT NULL,
|
|
duration_days INT,
|
|
active_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
active_until TIMESTAMPTZ,
|
|
released_at TIMESTAMPTZ,
|
|
released_by VARCHAR(100),
|
|
actor VARCHAR(100) NOT NULL,
|
|
comment TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT chk_gpce_scope CHECK (scope_type IN ('GROUP', 'GLOBAL')),
|
|
CONSTRAINT chk_gpce_reason CHECK (reason_type IN ('GROUP_WRONG_PARENT', 'GLOBAL_NOT_PARENT_TARGET')),
|
|
CONSTRAINT chk_gpce_group_scope CHECK (
|
|
(scope_type = 'GROUP' AND group_key IS NOT NULL AND sub_cluster_id IS NOT NULL AND duration_days IN (1, 3, 5) AND active_until IS NOT NULL)
|
|
OR
|
|
(scope_type = 'GLOBAL' AND duration_days IS NULL)
|
|
)
|
|
);
|
|
```
|
|
|
|
### 인덱스
|
|
|
|
```sql
|
|
CREATE INDEX IF NOT EXISTS idx_gpce_scope_mmsi_active
|
|
ON kcg.gear_parent_candidate_exclusions(scope_type, candidate_mmsi, active_from DESC)
|
|
WHERE released_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpce_group_active
|
|
ON kcg.gear_parent_candidate_exclusions(group_key, sub_cluster_id, active_from DESC)
|
|
WHERE released_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpce_active_until
|
|
ON kcg.gear_parent_candidate_exclusions(active_until);
|
|
```
|
|
|
|
### active 판정 규칙
|
|
|
|
active exclusion은 아래를 만족해야 한다.
|
|
|
|
```sql
|
|
released_at IS NULL
|
|
AND active_from <= NOW()
|
|
AND (active_until IS NULL OR active_until > NOW())
|
|
```
|
|
|
|
### 해석 규칙
|
|
|
|
- `GROUP`
|
|
- 특정 그룹에서만 해당 후보 제거
|
|
- `GLOBAL`
|
|
- 모든 그룹에서 해당 후보 제거
|
|
|
|
## 2. `gear_parent_label_sessions`
|
|
|
|
### 목적
|
|
|
|
- 정답 라벨 세션 저장
|
|
|
|
### DDL 초안
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_parent_label_sessions (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
group_key VARCHAR(100) NOT NULL,
|
|
sub_cluster_id SMALLINT NOT NULL,
|
|
label_parent_mmsi VARCHAR(20) NOT NULL,
|
|
label_parent_name VARCHAR(200),
|
|
label_parent_vessel_id INT REFERENCES kcg.fleet_vessels(id) ON DELETE SET NULL,
|
|
duration_days INT NOT NULL,
|
|
active_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
active_until TIMESTAMPTZ NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
|
actor VARCHAR(100) NOT NULL,
|
|
comment TEXT,
|
|
anchor_snapshot_time TIMESTAMPTZ,
|
|
anchor_center_point geometry(Point, 4326),
|
|
anchor_member_mmsis JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT chk_gpls_duration CHECK (duration_days IN (1, 3, 5)),
|
|
CONSTRAINT chk_gpls_status CHECK (status IN ('ACTIVE', 'EXPIRED', 'CANCELLED'))
|
|
);
|
|
```
|
|
|
|
### 인덱스
|
|
|
|
```sql
|
|
CREATE INDEX IF NOT EXISTS idx_gpls_group_active
|
|
ON kcg.gear_parent_label_sessions(group_key, sub_cluster_id, active_from DESC)
|
|
WHERE status = 'ACTIVE';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpls_mmsi_active
|
|
ON kcg.gear_parent_label_sessions(label_parent_mmsi, active_from DESC)
|
|
WHERE status = 'ACTIVE';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpls_active_until
|
|
ON kcg.gear_parent_label_sessions(active_until);
|
|
```
|
|
|
|
### active 판정 규칙
|
|
|
|
```sql
|
|
status = 'ACTIVE'
|
|
AND active_from <= NOW()
|
|
AND active_until > NOW()
|
|
```
|
|
|
|
### 만료 처리 규칙
|
|
|
|
prediction 또는 backend batch에서 아래를 주기적으로 실행한다.
|
|
|
|
```sql
|
|
UPDATE kcg.gear_parent_label_sessions
|
|
SET status = 'EXPIRED', updated_at = NOW()
|
|
WHERE status = 'ACTIVE'
|
|
AND active_until <= NOW();
|
|
```
|
|
|
|
## 3. `gear_parent_label_tracking_cycles`
|
|
|
|
### 목적
|
|
|
|
- 활성 정답 라벨 세션 동안 cycle별 자동 추론 결과 저장
|
|
|
|
### DDL 초안
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_parent_label_tracking_cycles (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
label_session_id BIGINT NOT NULL REFERENCES kcg.gear_parent_label_sessions(id) ON DELETE CASCADE,
|
|
observed_at TIMESTAMPTZ NOT NULL,
|
|
candidate_snapshot_observed_at TIMESTAMPTZ,
|
|
auto_status VARCHAR(40),
|
|
top_candidate_mmsi VARCHAR(20),
|
|
top_candidate_name VARCHAR(200),
|
|
top_candidate_score DOUBLE PRECISION,
|
|
top_candidate_margin DOUBLE PRECISION,
|
|
candidate_count INT NOT NULL DEFAULT 0,
|
|
labeled_candidate_present BOOLEAN NOT NULL DEFAULT FALSE,
|
|
labeled_candidate_rank INT,
|
|
labeled_candidate_score DOUBLE PRECISION,
|
|
labeled_candidate_pre_bonus_score DOUBLE PRECISION,
|
|
labeled_candidate_margin_from_top DOUBLE PRECISION,
|
|
matched_top1 BOOLEAN NOT NULL DEFAULT FALSE,
|
|
matched_top3 BOOLEAN NOT NULL DEFAULT FALSE,
|
|
evidence_summary JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT uq_gpltc_session_observed UNIQUE (label_session_id, observed_at)
|
|
);
|
|
```
|
|
|
|
### 인덱스
|
|
|
|
```sql
|
|
CREATE INDEX IF NOT EXISTS idx_gpltc_session_observed
|
|
ON kcg.gear_parent_label_tracking_cycles(label_session_id, observed_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpltc_top_candidate
|
|
ON kcg.gear_parent_label_tracking_cycles(top_candidate_mmsi);
|
|
```
|
|
|
|
## 4. 기존 `gear_group_parent_review_log` action 확장
|
|
|
|
### 새 action 목록
|
|
|
|
- `LABEL_PARENT`
|
|
- `EXCLUDE_GROUP`
|
|
- `EXCLUDE_GLOBAL`
|
|
- `RELEASE_EXCLUSION`
|
|
- `CANCEL_LABEL`
|
|
|
|
기존 action과 공존한다.
|
|
|
|
## migration 파일 제안
|
|
|
|
- `014_gear_parent_workflow_v2_phase1.sql`
|
|
|
|
구성 순서:
|
|
|
|
1. 새 테이블 3개 생성
|
|
2. 인덱스 생성
|
|
3. review log action 확장은 schema 변경 불필요
|
|
4. optional helper view 추가
|
|
|
|
## optional view 제안
|
|
|
|
### `vw_active_gear_parent_candidate_exclusions`
|
|
|
|
```sql
|
|
CREATE OR REPLACE VIEW kcg.vw_active_gear_parent_candidate_exclusions AS
|
|
SELECT *
|
|
FROM kcg.gear_parent_candidate_exclusions
|
|
WHERE released_at IS NULL
|
|
AND active_from <= NOW()
|
|
AND (active_until IS NULL OR active_until > NOW());
|
|
```
|
|
|
|
### `vw_active_gear_parent_label_sessions`
|
|
|
|
```sql
|
|
CREATE OR REPLACE VIEW kcg.vw_active_gear_parent_label_sessions AS
|
|
SELECT *
|
|
FROM kcg.gear_parent_label_sessions
|
|
WHERE status = 'ACTIVE'
|
|
AND active_from <= NOW()
|
|
AND active_until > NOW();
|
|
```
|
|
|
|
## backend API 명세
|
|
|
|
## 공통 정책
|
|
|
|
- 모든 write API는 `actor` 필수
|
|
- `group_key`, `sub_cluster_id`, `candidate_mmsi`, `selected_parent_mmsi`는 trim 후 저장
|
|
- 잘못된 기간은 `400 Bad Request`
|
|
- 중복 active session/exclusion 생성 시 `409 Conflict` 대신 동일 active row를 반환해도 됨
|
|
- Phase 1에서는 멱등성을 우선한다
|
|
|
|
## 1. 정답 라벨 세션 생성
|
|
|
|
### endpoint
|
|
|
|
`POST /api/vessel-analysis/groups/{groupKey}/parent-inference/{subClusterId}/label-sessions`
|
|
|
|
### request
|
|
|
|
```json
|
|
{
|
|
"selectedParentMmsi": "412333326",
|
|
"durationDays": 3,
|
|
"actor": "analyst-01",
|
|
"comment": "수동 검토 확정"
|
|
}
|
|
```
|
|
|
|
### validation
|
|
|
|
- `selectedParentMmsi` 필수
|
|
- `durationDays in (1,3,5)`
|
|
- 동일 `groupKey + subClusterId`에 active label session이 이미 있으면 새 row 생성 금지
|
|
|
|
### response
|
|
|
|
```json
|
|
{
|
|
"groupKey": "58399",
|
|
"subClusterId": 0,
|
|
"action": "LABEL_PARENT",
|
|
"labelSession": {
|
|
"id": 12,
|
|
"status": "ACTIVE",
|
|
"labelParentMmsi": "412333326",
|
|
"labelParentName": "UWEIJINGYU51015",
|
|
"durationDays": 3,
|
|
"activeFrom": "2026-04-03T10:00:00+09:00",
|
|
"activeUntil": "2026-04-06T10:00:00+09:00",
|
|
"actor": "analyst-01",
|
|
"comment": "수동 검토 확정"
|
|
}
|
|
}
|
|
```
|
|
|
|
## 2. 그룹 후보 제외 생성
|
|
|
|
### endpoint
|
|
|
|
`POST /api/vessel-analysis/groups/{groupKey}/parent-inference/{subClusterId}/candidate-exclusions`
|
|
|
|
### request
|
|
|
|
```json
|
|
{
|
|
"candidateMmsi": "412333326",
|
|
"durationDays": 3,
|
|
"actor": "analyst-01",
|
|
"comment": "이 그룹에서는 오답"
|
|
}
|
|
```
|
|
|
|
### 생성 규칙
|
|
|
|
- 내부적으로 `scopeType='GROUP'`
|
|
- `reasonType='GROUP_WRONG_PARENT'`
|
|
- 동일 `groupKey + subClusterId + candidateMmsi` active row가 있으면 재사용
|
|
|
|
### response
|
|
|
|
```json
|
|
{
|
|
"groupKey": "58399",
|
|
"subClusterId": 0,
|
|
"action": "EXCLUDE_GROUP",
|
|
"exclusion": {
|
|
"id": 33,
|
|
"scopeType": "GROUP",
|
|
"candidateMmsi": "412333326",
|
|
"durationDays": 3,
|
|
"activeFrom": "2026-04-03T10:00:00+09:00",
|
|
"activeUntil": "2026-04-06T10:00:00+09:00"
|
|
}
|
|
}
|
|
```
|
|
|
|
## 3. 전역 후보 제외 생성
|
|
|
|
### endpoint
|
|
|
|
`POST /api/vessel-analysis/parent-inference/candidate-exclusions/global`
|
|
|
|
### request
|
|
|
|
```json
|
|
{
|
|
"candidateMmsi": "412333326",
|
|
"actor": "analyst-01",
|
|
"comment": "모든 어구에서 후보 제외"
|
|
}
|
|
```
|
|
|
|
### 생성 규칙
|
|
|
|
- `scopeType='GLOBAL'`
|
|
- `reasonType='GLOBAL_NOT_PARENT_TARGET'`
|
|
- `activeUntil = NULL`
|
|
- 동일 candidate active global exclusion이 있으면 재사용
|
|
|
|
## 4. exclusion 해제
|
|
|
|
### endpoint
|
|
|
|
`POST /api/vessel-analysis/parent-inference/candidate-exclusions/{id}/release`
|
|
|
|
### request
|
|
|
|
```json
|
|
{
|
|
"actor": "analyst-01",
|
|
"comment": "해제"
|
|
}
|
|
```
|
|
|
|
### 동작
|
|
|
|
- `released_at = NOW()`
|
|
- `released_by = actor`
|
|
- `updated_at = NOW()`
|
|
|
|
## 5. label session 종료
|
|
|
|
### endpoint
|
|
|
|
`POST /api/vessel-analysis/parent-inference/label-sessions/{id}/cancel`
|
|
|
|
### request
|
|
|
|
```json
|
|
{
|
|
"actor": "analyst-01",
|
|
"comment": "조기 종료"
|
|
}
|
|
```
|
|
|
|
### 동작
|
|
|
|
- `status='CANCELLED'`
|
|
- `updated_at = NOW()`
|
|
|
|
## 6. active exclusion 조회
|
|
|
|
### endpoint
|
|
|
|
`GET /api/vessel-analysis/parent-inference/candidate-exclusions?status=ACTIVE&scopeType=GROUP|GLOBAL&candidateMmsi=...&groupKey=...`
|
|
|
|
### response 필드
|
|
|
|
- `id`
|
|
- `scopeType`
|
|
- `groupKey`
|
|
- `subClusterId`
|
|
- `candidateMmsi`
|
|
- `reasonType`
|
|
- `durationDays`
|
|
- `activeFrom`
|
|
- `activeUntil`
|
|
- `releasedAt`
|
|
- `actor`
|
|
- `comment`
|
|
- `isActive`
|
|
|
|
## 7. label session 목록
|
|
|
|
### endpoint
|
|
|
|
`GET /api/vessel-analysis/parent-inference/label-sessions?status=ACTIVE|EXPIRED|CANCELLED&groupKey=...`
|
|
|
|
### response 필드
|
|
|
|
- `id`
|
|
- `groupKey`
|
|
- `subClusterId`
|
|
- `labelParentMmsi`
|
|
- `labelParentName`
|
|
- `durationDays`
|
|
- `activeFrom`
|
|
- `activeUntil`
|
|
- `status`
|
|
- `actor`
|
|
- `comment`
|
|
- `latestTrackingSummary`
|
|
|
|
## 8. label tracking 상세
|
|
|
|
### endpoint
|
|
|
|
`GET /api/vessel-analysis/parent-inference/label-sessions/{id}/tracking`
|
|
|
|
### response 필드
|
|
|
|
- `session`
|
|
- `count`
|
|
- `items[]`
|
|
- `observedAt`
|
|
- `autoStatus`
|
|
- `topCandidateMmsi`
|
|
- `topCandidateScore`
|
|
- `topCandidateMargin`
|
|
- `candidateCount`
|
|
- `labeledCandidatePresent`
|
|
- `labeledCandidateRank`
|
|
- `labeledCandidateScore`
|
|
- `labeledCandidatePreBonusScore`
|
|
- `matchedTop1`
|
|
- `matchedTop3`
|
|
|
|
## backend 구현 위치
|
|
|
|
### 새 DTO/Request 제안
|
|
|
|
- `GroupParentLabelSessionRequest`
|
|
- `GroupParentCandidateExclusionRequest`
|
|
- `ReleaseParentCandidateExclusionRequest`
|
|
- `CancelParentLabelSessionRequest`
|
|
- `ParentCandidateExclusionDto`
|
|
- `ParentLabelSessionDto`
|
|
- `ParentLabelTrackingCycleDto`
|
|
|
|
### service 추가 메서드 제안
|
|
|
|
- `createGroupCandidateExclusion(...)`
|
|
- `createGlobalCandidateExclusion(...)`
|
|
- `releaseCandidateExclusion(...)`
|
|
- `createLabelSession(...)`
|
|
- `cancelLabelSession(...)`
|
|
- `listCandidateExclusions(...)`
|
|
- `listLabelSessions(...)`
|
|
- `getLabelSessionTracking(...)`
|
|
|
|
## prediction 명세
|
|
|
|
## 적용 함수
|
|
|
|
중심 파일은 [prediction/algorithms/gear_parent_inference.py](/Users/lht/work/devProjects/iran-airstrike-replay-codex/prediction/algorithms/gear_parent_inference.py)다.
|
|
|
|
### 새 load 함수
|
|
|
|
- `_load_active_candidate_exclusions(conn, group_keys)`
|
|
- `_load_active_label_sessions(conn, group_keys)`
|
|
|
|
### 반환 구조
|
|
|
|
`_load_active_candidate_exclusions`
|
|
|
|
```python
|
|
{
|
|
"global": {"412333326", "413000111"},
|
|
"group": {("58399", 0): {"412333326"}}
|
|
}
|
|
```
|
|
|
|
`_load_active_label_sessions`
|
|
|
|
```python
|
|
{
|
|
("58399", 0): {
|
|
"id": 12,
|
|
"label_parent_mmsi": "412333326",
|
|
"active_until": ...,
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
### 후보 pruning 순서
|
|
|
|
1. 기존 candidate union 생성
|
|
2. `GLOBAL` exclusion 제거
|
|
3. 해당 그룹의 `GROUP` exclusion 제거
|
|
4. 남은 후보만 scoring
|
|
|
|
### tracking row write 규칙
|
|
|
|
각 그룹 처리 후:
|
|
|
|
- active label session이 없으면 skip
|
|
- 있으면 현재 cycle 결과를 `gear_parent_label_tracking_cycles`에 upsert-like insert
|
|
|
|
필수 기록값:
|
|
|
|
- `label_session_id`
|
|
- `observed_at`
|
|
- `candidate_snapshot_observed_at`
|
|
- `auto_status`
|
|
- `top_candidate_mmsi`
|
|
- `top_candidate_score`
|
|
- `top_candidate_margin`
|
|
- `candidate_count`
|
|
- `labeled_candidate_present`
|
|
- `labeled_candidate_rank`
|
|
- `labeled_candidate_score`
|
|
- `labeled_candidate_pre_bonus_score`
|
|
- `matched_top1`
|
|
- `matched_top3`
|
|
|
|
### pre-bonus score 취득
|
|
|
|
현재 candidate evidence에 이미 아래가 있다.
|
|
|
|
- `evidence.scoreBreakdown.preBonusScore`
|
|
|
|
tracking row에서는 이 값을 직접 읽어 저장한다.
|
|
|
|
### resolution 처리 원칙
|
|
|
|
Phase 1에서는 다음을 적용한다.
|
|
|
|
- `LABEL_PARENT`, `EXCLUDE_GROUP`, `EXCLUDE_GLOBAL`은 `gear_group_parent_resolution` 상태를 바꾸지 않는다.
|
|
- 자동 추론은 기존 상태 전이를 그대로 사용한다.
|
|
- legacy `MANUAL_CONFIRMED` 로직은 남겨두되, 새 UI에서는 호출하지 않는다.
|
|
|
|
## 프론트 최소 계약
|
|
|
|
## 기존 패널 액션 치환
|
|
|
|
현재:
|
|
|
|
- `확정`
|
|
- `24시간 제외`
|
|
|
|
Phase 1 새 기본 액션:
|
|
|
|
- `정답 라벨`
|
|
- `이 그룹에서 제외`
|
|
- `전체 후보 제외`
|
|
|
|
### 기간 선택 UI
|
|
|
|
- `정답 라벨`: `1일`, `3일`, `5일`
|
|
- `이 그룹에서 제외`: `1일`, `3일`, `5일`
|
|
- `전체 후보 제외`: 기간 없음
|
|
|
|
### 표시 정보
|
|
|
|
후보 card badge:
|
|
|
|
- `이 그룹 제외 중`
|
|
- `전체 후보 제외 중`
|
|
- `정답 라벨 대상`
|
|
|
|
그룹 summary box:
|
|
|
|
- active label session 여부
|
|
- active group exclusion count
|
|
|
|
## API 에러 규약
|
|
|
|
### 400
|
|
|
|
- 잘못된 duration
|
|
- 필수 필드 누락
|
|
- groupKey/subClusterId 없음
|
|
|
|
### 404
|
|
|
|
- 대상 group 없음
|
|
- exclusion/session id 없음
|
|
|
|
### 409
|
|
|
|
- active label session 중복 생성
|
|
|
|
단, Phase 1에서는 backend에서 충돌 시 기존 active row를 그대로 반환하는 방식도 허용한다.
|
|
|
|
## 테스트 기준
|
|
|
|
## DB
|
|
|
|
- GROUP exclusion active query가 정확히 동작
|
|
- GLOBAL exclusion active query가 정확히 동작
|
|
- label session 만료 시 `EXPIRED` 전환
|
|
|
|
## backend
|
|
|
|
- create/release exclusion API
|
|
- create/cancel label session API
|
|
- list APIs 필터 조건
|
|
|
|
## prediction
|
|
|
|
- active exclusion candidate pruning
|
|
- global/group exclusion 우선 적용
|
|
- label session tracking row 생성
|
|
- labeled candidate absent/present/top1/top3 케이스
|
|
|
|
## 수용 기준
|
|
|
|
1. 특정 그룹에서 후보 제외를 걸면 다음 cycle부터 그 그룹 후보 목록에서만 빠진다.
|
|
2. 전역 후보 제외를 걸면 모든 그룹 후보 목록에서 빠진다.
|
|
3. 정답 라벨 세션 생성 후 다음 cycle부터 tracking row가 쌓인다.
|
|
4. 자동 resolution은 계속 자동 상태를 유지한다.
|
|
5. 기존 manual override API를 쓰지 않아도 review/label/exclusion 흐름이 독립적으로 동작한다.
|
|
|
|
## Phase 1 이후 바로 이어질 일
|
|
|
|
### Phase 2
|
|
|
|
- 라벨 추적 대시보드
|
|
- exclusion 관리 화면
|
|
- 지표 요약 endpoint
|
|
- episode continuity read model 노출
|
|
- prior bonus calibration report
|
|
|
|
### Phase 3
|
|
|
|
- label session anchor 기반 재매칭
|
|
- group case/episode lineage API 확장
|
|
- calibration report
|
|
|
|
## 권장 구현 순서
|
|
|
|
1. `014_gear_parent_workflow_v2_phase1.sql`
|
|
2. backend DTO + controller/service
|
|
3. prediction active exclusion/load + tracking write
|
|
4. frontend 버튼 교체와 최소 조회 화면
|
|
|
|
이 순서가 현재 코드 충돌과 운영 영향이 가장 적다.
|