Files
Aislo/resources/master_data/scripts/_migrate_base.py
T
eomsangdonandClaude Sonnet 5 5e2509cbc8 chore(master_data): 1_기초 8 파일 새 틀로 옮김 · 옛 7 old 로
- 새 요소 파일 6 — 인력_건설노임 · 인력_제조노임 · 재료_나라장터자재 · 재료_오피넷유가 · 환율_한국은행환율 · 요율_조달청제비율
- 출처목록 → _원문목록.json 이름만 바꿈
- scripts/_migrate_base.py 옮김·대조 · _build_mat_price.py 경로·칸 이름 새 파일에 맞춤

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
2026-09-19 15:33:47 +09:00

473 lines
16 KiB
Python

# -*- coding: utf-8 -*-
"""1_기초 옛 파일 7 → 새 틀(`_틀.md` 2~4장) 요소 파일 6 옮김 · 대조.
돌리기: `./venv/Scripts/python.exe resources/master_data/scripts/_migrate_base.py`
옛 파일은 `old/` 에서 읽기만 함. 새 파일은 `resources/master_data/` 첫 층에 씀. 값은 바꾸지 않음.
`dump_master` 는 `_build_mat_price.py` 도 씀.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
MD = Path(__file__).resolve().parents[1]
OLD = MD / "old"
UNIT = {
"KRW/day": "원/일",
"KRW/L": "원/L",
"KRW/USD": "원/USD",
"KRW/JPY100": "원/JPY100",
"KRW/EUR": "원/EUR",
"KRW/GBP": "원/GBP",
"KRW/CNY": "원/CNY",
}
STATUS = {"published": "공표", "not_published": "미공표"}
def dump_master(path: Path, head: dict, rows: list[dict]) -> None:
"""머리는 들여쓰기 2칸 · 줄은 한 줄씩(길면 펼침) · UTF-8 · LF."""
out = ["{"]
for key, val in head.items():
text = json.dumps(val, ensure_ascii=False, indent=2).replace("\n", "\n ")
out.append(f" {json.dumps(key, ensure_ascii=False)}: {text},")
out.append(' "줄": [')
for i, row in enumerate(rows):
line = json.dumps(row, ensure_ascii=False, separators=(", ", ": "))
if len(line) <= 450:
body = "{ " + line[1:-1] + " }"
else:
body = json.dumps(row, ensure_ascii=False, indent=2).replace("\n", "\n ")
out.append(" " + body + ("," if i < len(rows) - 1 else ""))
out += [" ]", "}"]
path.write_bytes(("\n".join(out) + "\n").encode("utf-8"))
def load(name: str) -> dict:
return json.loads((OLD / name).read_text(encoding="utf-8"))
def origin(sources: list[dict]) -> list[dict]:
return [{"경로": s["path"], "해시": s["sha256"], "역할": s["role"]} for s in sources]
def pick(rec: dict, allowed: set[str]) -> None:
extra = set(rec) - allowed
assert not extra, f"모르는 칸 {extra}"
# ── 인력 ────────────────────────────────────────────────────────────────
LABOR_EXTRA = { # 옛 칸 → 새 칸
"hours_per_day": "일시간",
"reliability": "신뢰도",
"industry": "업종",
"variation_coefficient": "변동계수",
"source_flag": "원문기호",
}
LABOR_KEYS = {
"occupation_code",
"occupation_name",
"unit",
"daily_wage_krw",
"status",
*LABOR_EXTRA,
}
def labor_rows(recs: list[dict], origin_name: str) -> list[dict]:
rows = []
for r in recs:
pick(r, LABOR_KEYS)
row = {
"열쇠": r["occupation_code"],
"이름": r["occupation_name"],
"규격": "",
"단위": UNIT[r["unit"]],
"값": r.get("daily_wage_krw"),
"출처": f"{origin_name} {r['occupation_code']}",
}
for old, new in LABOR_EXTRA.items():
if old in r:
row[new] = r[old]
row["상태"] = STATUS[r["status"]]
rows.append(row)
return rows
def labor_back(rows: list[dict]) -> list[dict]:
inv_unit = {v: k for k, v in UNIT.items()}
inv_status = {v: k for k, v in STATUS.items()}
out = []
for w in rows:
r = {
"occupation_code": w["열쇠"],
"occupation_name": w["이름"],
"unit": inv_unit[w["단위"]],
}
for old, new in LABOR_EXTRA.items():
if new in w:
r[old] = w[new]
if w["값"] is not None:
r["daily_wage_krw"] = w["값"]
r["status"] = inv_status[w["상태"]]
out.append(r)
return out
# ── 재료 ────────────────────────────────────────────────────────────────
MAT_EXTRA = {
"classification_code": "분류코드",
"notice_datetime": "공시일시",
"notice_number": "공시번호",
"business_division_code": "사업구분코드",
"business_division_name": "사업구분",
"vat_basis": "부가세",
"price_type": "가격종류",
"delivery_condition": "인도조건",
"field": "분야",
"price_adopted_reason": "비고",
}
MAT_KEYS = {"item_code", "classification_name", "specification", "unit", "price_krw", *MAT_EXTRA}
def mat_rows(recs: list[dict]) -> list[dict]:
rows = []
for r in recs:
pick(r, MAT_KEYS)
row = {
"열쇠": r["item_code"],
"이름": r["classification_name"],
"규격": r["specification"],
"단위": r["unit"],
"값": r["price_krw"],
"출처": f"나라장터자재 {r['item_code']}",
}
for old, new in MAT_EXTRA.items():
if old in r:
row[new] = r[old]
rows.append(row)
return rows
def mat_back(rows: list[dict]) -> list[dict]:
inv = {v: k for k, v in MAT_EXTRA.items()}
out = []
for w in rows:
r = {
"item_code": w["열쇠"],
"classification_name": w["이름"],
"specification": w["규격"],
"unit": w["단위"],
"price_krw": w["값"],
}
for new, old in inv.items():
if new in w:
r[old] = w[new]
out.append(r)
return out
# ── 유가 · 환율 ─────────────────────────────────────────────────────────
FUEL = {"oil_gasoline": "휘발유", "oil_diesel": "경유"}
FX_NAME = {
"fx_usd": "USD",
"fx_jpy100": "JPY100",
"fx_eur": "EUR",
"fx_gbp": "GBP",
"fx_cny": "CNY",
}
def oil_rows(avg: dict, reg: dict) -> list[dict]:
rows = []
for var, v in avg["variables"].items():
rows.append(
{
"열쇠": f"{FUEL[var]}:전국평균",
"이름": v["source_product_name"],
"규격": "",
"단위": UNIT[v["unit"]],
"값": v["value"],
"출처": f"오피넷유가 {v['source_product_code']}",
"구분": "전국평균",
"지역": "전국평균",
"기준일": v["date"],
"상품코드": v["source_product_code"],
}
)
for var, v in reg["variables"].items():
for r in v["records"]:
rows.append(
{
"열쇠": f"{FUEL[var]}:{r['sido_name']}",
"이름": v["source_product_name"],
"규격": "",
"단위": UNIT[v["unit"]],
"값": r["value"],
"출처": f"오피넷유가 {v['source_product_code']}",
"구분": "시도별",
"지역": r["sido_name"],
"지역코드": r["sido_code"],
"기준일": v["date"],
"상품코드": v["source_product_code"],
}
)
return rows
def fx_rows(fx: dict) -> list[dict]:
return [
{
"열쇠": v["source_item_code"],
"이름": FX_NAME[var],
"규격": "",
"단위": UNIT[v["unit"]],
"값": v["value"],
"출처": f"한국은행환율 {v['source_item_code']}",
"기준일": v["date"],
}
for var, v in fx["variables"].items()
]
# ── 요율 ────────────────────────────────────────────────────────────────
RATE_NAME = {
"rate_sanjae": "산재보험료",
"rate_goyong": "고용보험료",
"rate_health": "건강보험료",
"rate_pension": "국민연금보험료",
"rate_care": "노인장기요양보험료",
"rate_safety_pct": "산업안전보건관리비",
"rate_safety_base": "산업안전보건관리비 기초액",
"rate_indirect_labor": "간접노무비",
"rate_other_expense": "기타경비",
"rate_environment": "환경보전비",
"rate_retirement_mutual_aid": "퇴직공제부금비",
"rate_wage_claim_contribution": "임금채권보장분담금",
"rate_asbestos_contribution": "석면피해구제분담금",
"rate_subcontract_payment_guarantee": "하도급대금 지급보증 수수료",
"rate_equipment_payment_guarantee": "건설기계대여대금 지급보증 수수료",
"rate_performance_guarantee_fee": "공사이행보증 수수료",
"rate_overhead": "일반관리비",
"rate_profit": "이윤",
"rate_vat": "부가가치세",
}
def rate_rows(rates: dict) -> list[dict]:
assert set(rates["variables"]) == set(RATE_NAME)
return [
{"열쇠": k, "이름": RATE_NAME[k], "값": v, "출처": "조달청제비율"}
for k, v in rates["variables"].items()
]
# ── 옮기기 ──────────────────────────────────────────────────────────────
F_LC = "1_기초_노임_건설업_대한건설협회_2026-01-01.json"
F_LM = "1_기초_노임_제조업_중소기업중앙회_2026-07-01.json"
F_OR = "1_기초_유가_시도별_오피넷_2026-09-09.json"
F_OA = "1_기초_유가_전국평균_오피넷_2026-08-14.json"
F_MT = "1_기초_자재_시설공통_나라장터_2026-08-14.json"
F_RT = "1_기초_제비율_토목_조달청_2026-04-13.json"
F_FX = "1_기초_환율_일별_한국은행_2026-08-14.json"
NEW = {
"lc": "인력_건설노임.json",
"lm": "인력_제조노임.json",
"mt": "재료_나라장터자재.json",
"oil": "재료_오피넷유가.json",
"fx": "환율_한국은행환율.json",
"rt": "요율_조달청제비율.json",
}
def migrate() -> None:
lc, lm, mt = load(F_LC), load(F_LM), load(F_MT)
orr, oa, rt, fx = load(F_OR), load(F_OA), load(F_RT), load(F_FX)
lcv = lc["variables"]["labor_rate"]
dump_master(
MD / NEW["lc"],
{
"그룹": "인력",
"원문": "건설노임",
"판": lc["effective_date"],
"공표일": lc["publication_date"],
"원천": origin(lc["sources"]),
"비고": [lcv["reliability_note"]],
},
labor_rows(lcv["records"], "건설노임"),
)
lmv = lm["variables"]["labor_mfg"]
dump_master(
MD / NEW["lm"],
{
"그룹": "인력",
"원문": "제조노임",
"판": lm["effective_date"],
"공표일": lm["publication_date"],
"조사월": lm["survey_month"],
"원천": origin(lm["sources"]),
"비고": [lmv["industry_note"]],
},
labor_rows(lmv["records"], "제조노임"),
)
dump_master(
MD / NEW["mt"],
{
"그룹": "재료",
"원문": "나라장터자재",
"판": mt["effective_date"],
"원천": origin(mt["sources"]),
"선정규칙": mt["selection_policy"],
"원천행수": mt["source_row_count"],
"제외묶음": mt["excluded_named_groups"],
"0원미회수": {
"비고": mt["zero_price_unrecovered"]["note"],
"줄": mt["zero_price_unrecovered"]["items"],
},
},
mat_rows(mt["variables"]["mat_price"]["records"]),
)
dump_master(
MD / NEW["oil"],
{
"그룹": "재료",
"원문": "오피넷유가",
"판": orr["effective_date"],
"원천": origin(oa["sources"]) + origin(orr["sources"]),
"비고": [orr["note"]],
},
oil_rows(oa, orr),
)
dump_master(
MD / NEW["fx"],
{
"그룹": "환율",
"원문": "한국은행환율",
"판": fx["effective_date"],
"원천": origin(fx["sources"]),
},
fx_rows(fx),
)
dump_master(
MD / NEW["rt"],
{
"그룹": "요율",
"원문": "조달청제비율",
"판": rt["effective_date"],
"원천": origin(rt["sources"]),
"처리규칙": rt["processing_rules"],
},
rate_rows(rt),
)
# ── 대조 ────────────────────────────────────────────────────────────────
def new(key: str) -> dict:
return json.loads((MD / NEW[key]).read_text(encoding="utf-8"))
def num_sum(vals) -> float:
return round(sum(v for v in vals if isinstance(v, (int, float))), 6)
def leaves(o):
if isinstance(o, dict):
for v in o.values():
yield from leaves(v)
elif isinstance(o, list):
for v in o:
yield from leaves(v)
elif isinstance(o, (int, float)) and not isinstance(o, bool):
yield o
def check() -> bool:
res = [] # (이름, 옛 줄, 새 줄, 옛 합, 새 합, 같음)
def add(name, old_rows, new_rows, old_sum, new_sum, same):
res.append((name, len(old_rows), len(new_rows), old_sum, new_sum, same))
for key, f, v, fn in (
("lc", F_LC, "labor_rate", "건설노임"),
("lm", F_LM, "labor_mfg", "제조노임"),
):
old = load(f)["variables"][v]["records"]
nw = new(key)["줄"]
add(
fn,
old,
nw,
num_sum(r.get("daily_wage_krw") for r in old),
num_sum(r["값"] for r in nw),
labor_back(nw) == old,
)
old = load(F_MT)["variables"]["mat_price"]["records"]
nw = new("mt")["줄"]
add(
"나라장터자재",
old,
nw,
num_sum(r["price_krw"] for r in old),
num_sum(r["값"] for r in nw),
mat_back(nw) == old,
)
oa, orr = load(F_OA), load(F_OR)
nw = new("oil")["줄"]
old_vals = [v["value"] for v in oa["variables"].values()] + [
r["value"] for v in orr["variables"].values() for r in v["records"]
]
n_old = len(oa["variables"]) + sum(len(v["records"]) for v in orr["variables"].values())
keys_ok = len({r["열쇠"] for r in nw}) == len(nw)
same = keys_ok and [r["값"] for r in nw] == old_vals and nw == oil_rows(oa, orr)
res.append(
("오피넷유가", n_old, len(nw), num_sum(old_vals), num_sum(r["값"] for r in nw), same)
)
fx = load(F_FX)
nw = new("fx")["줄"]
old_vals = [v["value"] for v in fx["variables"].values()]
same = [r["값"] for r in nw] == old_vals and nw == fx_rows(fx)
res.append(
(
"한국은행환율",
len(old_vals),
len(nw),
num_sum(old_vals),
num_sum(r["값"] for r in nw),
same,
)
)
rt = load(F_RT)
nw = new("rt")
same = {r["열쇠"]: r["값"] for r in nw["줄"]} == rt["variables"] and nw["처리규칙"] == rt[
"processing_rules"
]
old_sum = num_sum(leaves(rt["variables"]))
res.append(
(
"조달청제비율",
len(rt["variables"]),
len(nw["줄"]),
old_sum,
num_sum(leaves([r["값"] for r in nw["줄"]])),
same,
)
)
print(f"{'파일':<12}{'옛 줄':>7}{'새 줄':>7}{'옛 값합':>16}{'새 값합':>16} 일치")
ok = True
for name, a, b, sa, sb, same in res:
good = a == b and sa == sb and same
ok &= good
print(f"{name:<12}{a:>7}{b:>7}{sa:>16}{sb:>16} {'예' if good else '아니오'}")
return ok
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
migrate()
sys.exit(0 if check() else 1)