"""관급자재 단가 — **0 원 공시를 건너뛰고 앞 제값을 쓴다**(2026-09-16 브레인). 왜 — 우리 고르기 규칙이 「물품코드마다 가장 나중 공시」라, 값이 안 실린 공시(`prce = 0`)가 옛 제값을 덮었다(각형강관 25*25*1.4t `1,460 → 1,500 → 0`). 「가격이 0」이 아니라 **그 공시에 값이 안 실린 것**이라, 0 을 값으로 받으면 안 된다. ⚠ 조용히 바꾸지 않는다 — 고친 줄에는 사유가 남고, 공시 날짜·번호도 **채택한 공시의 것**이 된다. ⚠ 앞 공시에도 값이 없어 0 이 남는 줄은 **빨강이 아니라 목록**(`zero_price_unrecovered`)으로 남는다. """ from __future__ import annotations import json from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] MASTER = ROOT / "resources" / "data_cost_input_value" / "mat_price_public_2026-08-14.json" RAW = ROOT / "resources/knowledge/original/원가계산/자재단가/나라장터_시설공통자재_2026-08-14.json" EMPTY_PRICES = {"", "0", "0.0", "0.00"} @pytest.fixture(scope="module") def master() -> dict: return json.loads(MASTER.read_text(encoding="utf-8")) @pytest.fixture(scope="module") def records(master) -> list[dict]: return master["variables"]["mat_price"]["records"] def _empty(value) -> bool: return str(value).strip() in EMPTY_PRICES def test_값이_없는_줄은_목록에만_남는다(master, records): """⭐ 규칙으로 고칠 수 있는 0 원 줄은 하나도 안 남는다. 앞 공시에도 값이 없는 줄만 `zero_price_unrecovered` 에 남고, 그 줄은 표에서도 0 이다. """ listed = {str(item["item_code"]) for item in master["zero_price_unrecovered"]["items"]} zeros = {str(r["item_code"]) for r in records if _empty(r.get("price_krw"))} assert zeros == listed, f"목록에 없는 0 원 줄: {sorted(zeros - listed)}" def test_못_고친_줄에는_사유가_붙는다(master): for item in master["zero_price_unrecovered"]["items"]: assert item["reason"].strip(), item["item_code"] assert master["zero_price_unrecovered"]["note"].strip() def test_고친_줄마다_사유가_남는다(records): """조용히 바꾸지 않는다 — 「○ 공시에 값이 없어 ○ 값을 씀」이 줄에 남아야 한다.""" repaired = [r for r in records if r.get("price_adopted_reason")] assert repaired, "고친 줄이 하나도 없다" for row in repaired: reason = row["price_adopted_reason"] assert "값이 없어" in reason and "씀" in reason, reason assert not _empty(row["price_krw"]), row["item_code"] def test_고친_줄의_공시_날짜와_번호가_채택한_공시의_것이다(records): """어느 판 값인지 화면에서 보여야 한다 — 날짜가 0 원 공시에 머물면 안 된다.""" raw = json.loads(RAW.read_text(encoding="utf-8")) history: dict[str, list[dict]] = {} for row in raw: history.setdefault(str(row.get("prdctIdntNo")), []).append(row) for rows in history.values(): rows.sort(key=lambda r: str(r.get("nticeDt")), reverse=True) wrong = [] for row in (r for r in records if r.get("price_adopted_reason")): adopted = next( (h for h in history.get(str(row["item_code"]), ()) if not _empty(h.get("prce"))), None, ) assert adopted is not None, row["item_code"] if str(row["price_krw"]) != str(int(str(adopted["prce"]).strip())): wrong.append(f"{row['item_code']} 단가 {row['price_krw']} ≠ {adopted['prce']}") elif row["notice_datetime"] != str(adopted.get("nticeDt", "")): wrong.append(f"{row['item_code']} 날짜 {row['notice_datetime']}") elif row["notice_number"] != str(adopted.get("prceNticeNo", "")): wrong.append(f"{row['item_code']} 번호 {row['notice_number']}") elif ( row["price_adopted_reason"].endswith(f"{str(adopted['nticeDt'])[:10]} 값을 씀") is False ): wrong.append(f"{row['item_code']} 사유가 채택 공시와 다름") assert not wrong, wrong def test_0원_아닌_줄은_안_건드린다(records): """규칙을 통째로 다시 돌리지 않는다 — 사유가 붙은 줄만 손댄 줄이다.""" touched = [r for r in records if r.get("price_adopted_reason")] assert len(touched) < len(records) // 10, ( "손댄 줄이 너무 많다 — 규칙이 전체를 다시 쓴 것 아닌가" ) assert len(records) == 6999, "줄 수가 바뀌었다" def test_고르는_방침에_0원_건너뛰기가_적혀_있다(master): policy = master["selection_policy"] assert "zero" in policy and "skip" in policy.lower(), policy def test_같은_날짜에_0과_제값이_함께_있어도_제값을_고른다(): """2025-04-24 는 같은 날 0 과 제값이 함께 실린다 — 날짜만 보면 0 을 집을 수 있다.""" raw = json.loads(RAW.read_text(encoding="utf-8")) same_day: dict[str, set[bool]] = {} for row in raw: if str(row.get("nticeDt", ""))[:10] != "2025-04-24": continue same_day.setdefault(str(row.get("prdctIdntNo")), set()).add(_empty(row.get("prce"))) both = [code for code, kinds in same_day.items() if kinds == {True, False}] assert both, "같은 날 0 과 제값이 함께 있는 물품이 없다 — 시험 전제가 바뀌었다" master = json.loads(MASTER.read_text(encoding="utf-8")) rows = {str(r["item_code"]): r for r in master["variables"]["mat_price"]["records"]} for code in both: row = rows.get(code) if row is not None: assert not _empty(row["price_krw"]), f"{code} 가 0 으로 섰다"