# -*- coding: utf-8 -*- """관급자재 단가 — **0 원 공시를 건너뛰고 앞 제값 공시를 채택**하는 규칙(2026-09-16 브레인). 왜 — 우리 고르기 규칙이 「물품코드마다 가장 나중 공시」라, 값이 안 실린 공시(`prce = 0`)가 옛 제값을 덮었다. 각형강관 25*25*1 이 `1,460 → 1,500 → 0` 으로 끝난 것이 그것이다. 「가격이 0」이 아니라 **그 공시에 값이 안 실린 것**이라, 0 을 값으로 받으면 안 된다. 무엇을 고치나 - 마스터의 0 원 줄만 고친다. **원천 스냅숏은 안 건드린다**(읽기만). - 0 이 아닌 줄은 한 글자도 안 건드린다 — 규칙을 통째로 다시 돌리지 않는다. - 고친 줄마다 **사유**(`price_adopted_reason`)를 남긴다. 조용히 바꾸지 않는다. - 공시 날짜·번호도 **채택한 공시의 것**으로 바꾼다 — 어느 판 값인지 화면에서 보여야 한다. - 앞 공시에도 값이 없어 0 이 남는 줄은 `zero_price_unrecovered` 로 낸다(규칙으로 못 고침). ⚠ 같은 날짜에 0 과 제값이 함께 실린다(2025-04-24) — 날짜만 보지 말고 **0 을 건너뛴 뒤** 가장 나중 공시를 고른다. 돌리기: `./venv/Scripts/python.exe resources/data_cost_input_value/_build_mat_price.py` """ from __future__ import annotations import json from pathlib import Path ROOT = Path(__file__).resolve().parents[2] RAW = ROOT / "resources/knowledge/original/원가계산/자재단가/나라장터_시설공통자재_2026-08-14.json" MASTER = ROOT / "resources/data_cost_input_value/mat_price_public_2026-08-14.json" #: 값이 안 실린 공시를 가리는 잣대 — 빈 칸·0 은 「값 없음」이다. EMPTY_PRICES = {"", "0", "0.0", "0.00"} SELECTION_POLICY = ( "latest notice per product identification number within the local snapshot, " "skipping notices whose price is empty or zero " "(a zero price means the notice carried no price, not a price of zero)" ) UNRECOVERED_NOTE = ( "앞 공시에도 값이 없어 0 이 남은 줄. **규칙으로 못 고친다** — 0 을 값으로 쓰지 말고 " "그 자재가 실제로 필요해지면 원천을 다시 받거나 견적을 받아야 한다." ) def price_of(row: dict) -> str: return str(row.get("prce", "")).strip() def has_price(row: dict) -> bool: return price_of(row) not in EMPTY_PRICES def notice_day(row: dict) -> str: return str(row.get("nticeDt", ""))[:10] def build() -> dict: raw = json.loads(RAW.read_text(encoding="utf-8")) master = json.loads(MASTER.read_text(encoding="utf-8")) records = master["variables"]["mat_price"]["records"] 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) repaired, unrecovered = 0, [] for record in records: if str(record.get("price_krw")).strip() not in EMPTY_PRICES: continue was = notice_day(record | {"nticeDt": record.get("notice_datetime")}) adopted = next((r for r in history.get(str(record["item_code"]), ()) if has_price(r)), None) if adopted is None: unrecovered.append( { "item_code": record["item_code"], "classification_name": record.get("classification_name", ""), "specification": record.get("specification", ""), "notice_datetime": record.get("notice_datetime", ""), "reason": "이 물품코드의 모든 공시에 값이 없음", } ) continue record["price_krw"] = int(price_of(adopted)) record["notice_datetime"] = str(adopted.get("nticeDt", "")) record["notice_number"] = str(adopted.get("prceNticeNo", "")) record["price_adopted_reason"] = f"{was} 공시에 값이 없어 {notice_day(adopted)} 값을 씀" repaired += 1 master["selection_policy"] = SELECTION_POLICY master["zero_price_unrecovered"] = {"note": UNRECOVERED_NOTE, "items": unrecovered} return {"master": master, "repaired": repaired, "unrecovered": len(unrecovered)} if __name__ == "__main__": result = build() MASTER.write_text( json.dumps(result["master"], ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) print(f"고친 줄 {result['repaired']} · 못 고친 줄 {result['unrecovered']}")