feat(z01): 기초단가 표 수준 출처 — 출처표(data_master_sources) 를 kind 마다 목록으로(내는 곳 · 받는 자리 · 주기 · 최신 공표일 · 우리 판 · 확인한 날 · outdated) · 우리 판은 출처표 글자가 아니라 실제 파일에서 · 뒤처짐 판정은 서버만 · 판 기준일은 한 함수(effective_date · 파생본 derived_from.effective_date) · 모아 보기 항목에 단위(브레인)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-15 22:06:05 +09:00
co-authored by Claude Opus 5
parent 177534709f
commit 4fe974bcaa
2 changed files with 131 additions and 8 deletions
+67 -8
View File
@@ -10,6 +10,7 @@
from __future__ import annotations
import json
from datetime import date
from decimal import Decimal
from functools import lru_cache
from pathlib import Path
@@ -20,6 +21,9 @@ from Z01_MasterData import Z01_MasterData_Overrides as overrides
from Z01_MasterData import Z01_MasterData_Tables as tables
KINDS = ("labor", "machine", "material", "oil", "rate")
SOURCES_DIR = (
tables.RESOURCES / "data_master_sources"
) # 출처표(데스크탑 서브) — 어디서 받고 최신이 무엇인지
_KEY = "자료 열쇠 — 바꾸면 덮개·다른 표가 이 줄을 못 찾음"
_SOURCE = "공표 원문 칸 — 원본 갱신으로만 바뀜"
_RATE_EDITABLE = (
@@ -53,6 +57,58 @@ def _doc(file_id: str) -> tuple[dict[str, Any], str]:
return _read(str(path), path.stat().st_mtime_ns), path.name
def edition_of(doc: dict[str, Any]) -> str | None:
"""판 기준일 — 자료마다 자리가 달라 **여기 한 곳**에서 뽑음(`effective_date` · 파생본은 `derived_from.effective_date`)."""
derived = doc.get("derived_from")
return doc.get("effective_date") or (
derived.get("effective_date") if isinstance(derived, dict) else None
)
def _date(text: Any) -> date | None:
try:
return date.fromisoformat(str(text))
except ValueError:
return None
def sources(kind: str) -> list[dict[str, Any]]:
"""표 수준 출처 — kind 하나에 판이 둘인 곳(노임·유가·기계)이 있어 목록.
`our_edition` 은 출처표 글자가 아니라 **실제 파일**에서 읽음(글자가 굳으면 또 뒤처짐) ·
`outdated` 판정은 서버만(최신 공표일 > 우리 판 · 둘 중 하나라도 모르면 None).
"""
found = sorted(SOURCES_DIR.glob("sources_*.json")) if SOURCES_DIR.is_dir() else []
if not found:
return []
listing = _read(str(found[-1]), found[-1].stat().st_mtime_ns)
out = []
for entry in listing.get("sources") or []:
if entry.get("kind") != kind:
continue
editions = []
for name in entry.get("files") or []:
path = (tables.ROOT / name).resolve()
if path.is_file() and path.is_relative_to(tables.RESOURCES):
editions.append(_date(edition_of(_read(str(path), path.stat().st_mtime_ns))))
ours = min((e for e in editions if e), default=None)
latest = _date(entry.get("latest_published"))
out.append(
{
"source_id": entry.get("source_id"),
"name": entry.get("name_ko") or entry.get("source_id"),
"publisher": entry.get("publisher") or "",
"where": entry.get("where_to_get") or "",
"cycle": entry.get("cycle") or "",
"latest_published": latest.isoformat() if latest else None,
"our_edition": ours.isoformat() if ours else None,
"checked_at": entry.get("checked_on") or None,
"outdated": None if ours is None or latest is None else latest > ours,
}
)
return out
def _records(doc: dict[str, Any]) -> list[dict[str, Any]]:
return next(
v["records"] for v in doc["variables"].values() if isinstance(v.get("records"), list)
@@ -70,7 +126,7 @@ def _labor() -> list[dict[str, Any]]:
"@id": f"{dataset}/{r['occupation_code']}",
"@source": name,
"dataset_id": dataset,
"effective_date": doc.get("effective_date"),
"effective_date": edition_of(doc),
**r,
}
row.setdefault("daily_wage_krw", None) # 미공표 직종 — 사람이 넣을 수 있음
@@ -80,7 +136,7 @@ def _labor() -> list[dict[str, Any]]:
def _material() -> list[dict[str, Any]]:
doc, name = _doc("mat_price_public")
date = doc.get("effective_date")
date = edition_of(doc)
return [
{"@id": str(r["item_code"]), "@source": name, "effective_date": date, **r}
for r in _records(doc)
@@ -96,7 +152,7 @@ def _oil() -> list[dict[str, Any]]:
common = {
"scope": v.get("scope"),
"fuel": variable,
"effective_date": doc.get("effective_date"), # 전국 · 지역 판 기준일이 다름
"effective_date": edition_of(doc), # 전국 · 지역 판 기준일이 다름
**head,
}
if "records" not in v:
@@ -140,7 +196,7 @@ def _rate() -> list[dict[str, Any]]:
"@source": name,
"variable": variable,
"part": "",
"effective_date": doc.get("effective_date"),
"effective_date": edition_of(doc),
**_flat(v),
}
)
@@ -161,7 +217,7 @@ def _rate() -> list[dict[str, Any]]:
"@source": name,
"variable": variable,
"part": part,
"effective_date": doc.get("effective_date"),
"effective_date": edition_of(doc),
"base": v.get("base"),
**item,
}
@@ -176,7 +232,7 @@ def base_rows(kind: str) -> list[dict[str, Any]]:
"""덮개 얹기 전 원본 줄 — 기계는 입력 칸까지(계산 칸은 덮개를 얹은 뒤 셈)."""
if kind == "machine":
doc, name = _doc("mach_base")
return machine.base_rows(name, doc.get("effective_date"))
return machine.base_rows(name, edition_of(doc))
return _BUILDERS[kind]()
@@ -196,7 +252,7 @@ def _machine_inputs() -> machine.Inputs:
wage_date=labor[0]["effective_date"] if labor else None,
oil={r["fuel"]: Decimal(str(r["value"])) for r in oil},
oil_dates={r["fuel"]: r.get("date") or r["effective_date"] for r in oil},
operating_date=_doc("pum_const")[0].get("effective_date"),
operating_date=edition_of(_doc("pum_const")[0]),
)
@@ -300,6 +356,7 @@ def table(
"rows": [public(r) for r in hits[start : start + size]],
"total": len(hits),
**spec(kind, columns),
"source": sources(kind),
}
@@ -335,7 +392,9 @@ def override_items(
if (kind and each != kind) or (state and item["state"] != state):
continue
item["row_label"] = row_label(each, by_id.get(item["row_id"]))
item["column_label"] = column_meta(each, item["column"])["label"]
meta = column_meta(each, item["column"])
item["column_label"] = meta["label"]
item["unit"] = meta["unit"]
out.append(item)
out.sort(key=lambda i: overrides.STATE_ORDER.index(i["state"]))
size = max(1, min(size, tables.MAX_PAGE_SIZE))