Files
Aislo/resources/master_data/scripts/_build_mat_price.py
T
eomsangdonandClaude Opus 5 32b19c547d feat(master_data): 키 개편 뒤따름 — _틀.md 새 규칙 · 빌더 잇기(adopt) · ESTX 비교 · 화면 계약
- _틀.md: 파일 이름(인력.json · 기계.json · NN장_장 제목) · 머리 부문·차례 · 키·테이블ID·대장 · 변수(키 · ID:원문번호 · 찾기(QF…) · 로직(GC…))
- master_keys: old_view(옛 이름으로 읽기) · adopt(빌더 옛 모양 → 키·한 테이블·장 이름 · 부문 앞말 · 참조 변환) · order_chapters(차례)
- 빌더 7개 끝에서 adopt · 노임·시중물가 빌더 다시 돌려 결과 같음(시중물가 머리 비고 글 한 줄만 「키」) · 기계·기계설비 7장은 사본 폴더에서 같음
- compare_estx: 인력.json 조사 · 기계.json 세부분류 · 로직 GC:원문번호 · 네 갈래 모두 돌아감
- _화면_계약.md: key = 키 · 새 줄 키는 서버가 대장 다음 번호 · 키 못 고침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-20 02:09:04 +09:00

104 lines
4.4 KiB
Python

# -*- 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/master_data/scripts/_build_mat_price.py`
"""
from __future__ import annotations
import json
from pathlib import Path
import master_keys as mk
from _migrate_base import dump_master
ROOT = Path(__file__).resolve().parents[3]
RAW = ROOT / "resources/knowledge/original/원가계산/자재단가/나라장터_시설공통자재_2026-08-14.json"
MASTER = ROOT / "resources/master_data/재료_나라장터자재.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 = mk.old_view(MASTER.name)
records = master["줄"]
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("값")).strip() not in EMPTY_PRICES:
continue
was = notice_day(record | {"nticeDt": record.get("공시일시")})
adopted = next((r for r in history.get(str(record["열쇠"]), ()) if has_price(r)), None)
if adopted is None:
unrecovered.append(
{
"열쇠": record["열쇠"],
"이름": record.get("이름", ""),
"규격": record.get("규격", ""),
"공시일시": record.get("공시일시", ""),
"사유": "이 물품코드의 모든 공시에 값이 없음",
}
)
continue
record["값"] = int(price_of(adopted))
record["공시일시"] = str(adopted.get("nticeDt", ""))
record["공시번호"] = str(adopted.get("prceNticeNo", ""))
record["비고"] = f"{was} 공시에 값이 없어 {notice_day(adopted)} 값을 씀"
repaired += 1
master["선정규칙"] = SELECTION_POLICY
master["0원미회수"] = {"비고": UNRECOVERED_NOTE, "줄": unrecovered}
return {"master": master, "repaired": repaired, "unrecovered": len(unrecovered)}
if __name__ == "__main__":
result = build()
master = result["master"]
dump_master(MASTER, {k: v for k, v in master.items() if k != "줄"}, master["줄"])
print(f"고친 줄 {result['repaired']} · 못 고친 줄 {result['unrecovered']}")
print("새 틀로 받음", mk.adopt())