From 7fa2ac52f7697a423cf37027b63430b30ab18897 Mon Sep 17 00:00:00 2001 From: KimDongAn Date: Tue, 29 Sep 2026 19:43:15 +0900 Subject: [PATCH] =?UTF-8?q?fix(assessment):=20AIS=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=9E=85=ED=95=AD=20=EC=A0=84=20=EB=B0=B0?= =?UTF-8?q?=EB=8F=84=20=EC=A1=B0=EC=B9=98=EC=95=88=C2=B7=EB=B0=9B=EB=8A=94?= =?UTF-8?q?=20=EA=B3=B3=EC=9D=84=20=EB=B6=99=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [판정 요청]으로 입항 목록의 배를 판정하면 아직 AIS 에 잡히지 않아 항해상태가 없다. 시점이 None 이 되어 '입항전'으로 기록되면서도 조치안·받는 곳이 비었다(운영 308 HYODONG CHEMI). 판정 대상이 PORT-MIS 배정 선석이면 입항 전으로 본다 → 대체선석 / 선석운영주체. 판정 잡은 그대로(AIS 상태 없으면 판정불가). Co-Authored-By: Claude Opus 5.5 --- app/api/v1/orchestrator.py | 3 ++- app/services/assessment.py | 13 ++++++--- tests/test_stage_before_arrival_20260929.py | 30 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tests/test_stage_before_arrival_20260929.py 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 == "선석운영주체"