diff --git a/app/api/v1/orchestrator.py b/app/api/v1/orchestrator.py index 042f4c5..243cc30 100644 --- a/app/api/v1/orchestrator.py +++ b/app/api/v1/orchestrator.py @@ -176,7 +176,8 @@ async def assess_and_record( result = await _orchestrate_or_http(db, llm_client, base) live = (await db.execute(_QUERY_LIVE_STATE, {"call_sign": request.call_sign})).mappings().first() - stage = stage_from_nav_status(live["nav_status_code"] if live else None) + stage = stage_from_nav_status(live["nav_status_code"] if live else None, + target_source=result.target_source) port_call_key = request.port_call_key or ( await db.execute(_QUERY_CURRENT_CALL_KEY, {"call_sign": request.call_sign}) ).scalar() diff --git a/app/services/assessment.py b/app/services/assessment.py index a619afb..6a3e2a8 100644 --- a/app/services/assessment.py +++ b/app/services/assessment.py @@ -36,11 +36,18 @@ # AIS 항해상태 하나만 본다. PORT-MIS 를 쓰지 않는 이유는 모델 주석 참고 # (수집창 [어제, 오늘+3일] 밖이면 동결 — 2026-09-21 실측 79척 중 32척). -def stage_from_nav_status(nav_status_code: str | None) -> AssessmentStage | None: - """UPA 항해상태 → 시점. 상태를 모르면 None(= 판정불가로 기록).""" +def stage_from_nav_status( + nav_status_code: str | None, *, target_source: str | None = None, +) -> AssessmentStage | None: + """UPA 항해상태 → 시점. 상태를 모르면 None(= 판정불가로 기록). + + [2026-09-29] 상태가 없어도 판정 대상이 PORT-MIS 배정 선석이면 입항 전이다 — 아직 AIS 에 + 잡히지 않은 배다. 예전엔 None 이 되어 '입항전'으로 기록되면서도 조치안·받는 곳이 비었다 + (운영 308 HYODONG CHEMI, [판정 요청] 경로). + """ nav = (nav_status_code or "").strip() if not nav: - return None + return AssessmentStage.BEFORE_ARRIVAL if target_source == "PORT-MIS" else None if nav.startswith("정박(계류)"): return AssessmentStage.DURING_CARGO if nav.startswith("정박(앵커링)"): diff --git a/tests/test_stage_before_arrival_20260929.py b/tests/test_stage_before_arrival_20260929.py new file mode 100644 index 0000000..754718d --- /dev/null +++ b/tests/test_stage_before_arrival_20260929.py @@ -0,0 +1,30 @@ +"""AIS 항해상태가 없는 입항 전 배의 시점 (2026-09-29). + +입항 목록에서 [판정 요청]한 배는 아직 AIS 에 잡히지 않아 항해상태가 없다. 예전엔 시점이 +None 이 되어 '입항전'으로 기록되면서도 조치안·받는 곳이 비었다(운영 308 HYODONG CHEMI). +판정 대상이 PORT-MIS 배정 선석이면 아직 붙지 않은 배이므로 입항 전으로 본다. +""" +from app.models.assessment_history import AssessmentLevel, AssessmentStage +from app.services.assessment import _action_for, _RECIPIENT_BY_STAGE, stage_from_nav_status + + +def test_no_ais_with_portmis_target_is_before_arrival(): + assert stage_from_nav_status(None, target_source="PORT-MIS") is AssessmentStage.BEFORE_ARRIVAL + assert stage_from_nav_status(" ", target_source="PORT-MIS") is AssessmentStage.BEFORE_ARRIVAL + + +def test_no_ais_without_portmis_target_stays_unknown(): + # AIS 로 접안한 배인데 상태가 없거나, 대상 출처를 모르면 여전히 모른다. + assert stage_from_nav_status(None) is None + assert stage_from_nav_status(None, target_source="AIS") is None + + +def test_nav_status_wins_over_target_source(): + assert stage_from_nav_status("정박(계류)", target_source="PORT-MIS") is AssessmentStage.DURING_CARGO + + +def test_unfit_before_arrival_gets_action_and_berth_operator(): + stage = stage_from_nav_status(None, target_source="PORT-MIS") + action = _action_for(AssessmentLevel.UNFIT, stage) + assert action is not None and action.value == "대체선석" + assert _RECIPIENT_BY_STAGE[stage].value == "선석운영주체"