feat(master_data): 노임 마스터화 — 원문 md 다섯 갈래 노임 · 준용대상 짝짓기·통합 · 미공표 산정 · ESTX 항목 비교
- 새 요소 파일(우리 md 에서만 · 판 2026-01-01): 엔지니어링노임 56 · 측량노임 20 · 건설사업관리노임 4 · SW노임 17 · 산림노임 2 — 빌더 build_인력_노임.py · 줄 수·값 md 기계 대조
- 준용대상 49 → 48: 측량중급기술자 옮김(로직 참조 8) · 원문 통합 목록 5 직종 통합 칸(목도 · 특수비계공 · 함석공 · 갱부 · 스레이트공) · 절단공 갈림 · 비슷한 이름 21 후보
- 건설노임 미공표 14 산정{값 · 근거 · 마지막공표} — 원문 회차 평균 그대로 이음 · 석조각공·드잡이공편수 다산소프트와 이음 계열 다름 비고 · 2026-09-01 적용분 확보됨 비고
- 엔진 차례 값 → 준용 → 통합 → 산정 · 호표 출처 · M01 준용 후보 7 노임 파일 · 통합·산정·후보 칸 읽기 전용
- ref/_비교_estx.md 10절: ESTX 378 줄 중 371 짝 · 짝 없는 직종 42 · 일괄 시험 계산 통과 1104 → 1115
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
"""노임 공표 원문 md → 인력 요소 파일 다섯 · 준용대상 짝짓기 · 건설노임 미공표 산정.
|
||||
|
||||
원문 = resources/knowledge/original/원가계산/노임단가_* (우리 md · ESTX 값 쓰지 않음).
|
||||
- 인력_엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 — md 표 그대로 · 줄 수·값을 md 와 기계 대조.
|
||||
- 인력_준용대상 — 새 노임 직종과 이름이 온전히 같은 줄은 옮김(로직 참조도) · 비슷한 이름은 비고에 후보.
|
||||
건설노임 원문 「통합·명칭변경 직종」 목록의 옛 직종은 `통합` = 새 직종 ref(엔진이 그 값을 씀).
|
||||
- 인력_건설노임 미공표 — `산정` = {값(원 미만 처리 안 함) · 근거 · 마지막공표} · 원문 4-라.
|
||||
이음 = 원문 회차의 전체직종 평균을 그대로 이음(미공표직종_추적.md 4절) · 산정값은 같은 md 5절과 대조.
|
||||
|
||||
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_인력_노임.py
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MD = ROOT / "resources/master_data"
|
||||
COST = ROOT / "resources/knowledge/original/원가계산"
|
||||
CAK = COST / "노임단가_건설업_대한건설협회"
|
||||
WAGE = "원/일"
|
||||
_INT = re.compile(r"\d{1,3}(?:,\d{3})+")
|
||||
|
||||
|
||||
def num(text: str) -> int:
|
||||
return int(text.replace(",", ""))
|
||||
|
||||
|
||||
def cells(line: str) -> list[str]:
|
||||
return [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
|
||||
|
||||
def table_after(text: str, head: str) -> list[list[str]]:
|
||||
"""`head` 로 시작하는 md 표 줄부터 표 끝까지 — 구분 줄 뺌."""
|
||||
lines = text.splitlines()
|
||||
i = next(n for n, x in enumerate(lines) if x.startswith(head))
|
||||
out = []
|
||||
for x in lines[i:]:
|
||||
if not x.startswith("|"):
|
||||
break
|
||||
if not set(x) <= set("|-: "):
|
||||
out.append(cells(x))
|
||||
return out
|
||||
|
||||
|
||||
def origin(path: Path) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"경로": path.relative_to(ROOT).as_posix(),
|
||||
"해시": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
"역할": "primary",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def row(name: str, value, source: str, **more) -> dict:
|
||||
return {
|
||||
"열쇠": name,
|
||||
"이름": name,
|
||||
"규격": "",
|
||||
"단위": WAGE,
|
||||
"값": value,
|
||||
"출처": source,
|
||||
**more,
|
||||
}
|
||||
|
||||
|
||||
# ── 갈래별 노임 ──────────────────────────────────────────────────────
|
||||
def engineering():
|
||||
d = COST / "노임단가_엔지니어링_한국엔지니어링협회"
|
||||
src = d / "공표문_2025 엔지니어링업체 임금실태조사결과 공표.md"
|
||||
t = table_after(src.read_text(encoding="utf-8"), "| 구분 |")
|
||||
parts = [p.replace(r"\*", "") for p in t[0][1:]] # 「기타**」 주석 기호
|
||||
rows = [
|
||||
row(f"{r[0]}({p})", num(v), f"엔지니어링노임 {r[0]} {p}", 부문=p)
|
||||
for r in t[1:]
|
||||
for p, v in zip(parts, r[1:])
|
||||
]
|
||||
# 같은 게시물 xls(연도별) 2025 칸과 대조
|
||||
y = table_after(
|
||||
(d / "노임단가_연도별_엔지니어링(2025).md").read_text(encoding="utf-8"), "| 년도 |"
|
||||
)
|
||||
xls = {(r[0], p): num(v) for r in y[2:10] for p, v in zip(y[1][-7:], r[-7:])}
|
||||
assert {(r["이름"].split("(")[0], r["부문"]): r["값"] for r in rows} == xls
|
||||
head = {
|
||||
"그룹": "인력",
|
||||
"원문": "엔지니어링노임",
|
||||
"판": "2026-01-01",
|
||||
"공표일": "2025-12-02",
|
||||
"원천": origin(src),
|
||||
"비고": [
|
||||
"한국엔지니어링협회 2025 엔지니어링업체 임금실태조사결과 공표(기획홍보팀-1515호 · 국가승인통계 제372001호) · 7 기술부문 × 8 기술등급 · 1인 1일 평균임금 · 월평균 근무일수 20.3일",
|
||||
"이름 = 기술등급(기술부문) · 기타 = 선박·항공우주·금속·화학·광업·농림·산업·해양·수산",
|
||||
"보고서 표 2 「전체」(기술등급 단순평균)는 공표 노임단가가 아니라 넣지 않음",
|
||||
],
|
||||
}
|
||||
return src, head, rows
|
||||
|
||||
|
||||
def survey():
|
||||
src = (
|
||||
COST
|
||||
/ "노임단가_측량_한국공간정보산업협회/공표문_2025년도 측량업체 임금실태조사 결과 공표.md"
|
||||
)
|
||||
t = table_after(src.read_text(encoding="utf-8"), "| 구분 |")
|
||||
rows, top, field = [], "", ""
|
||||
for r in t[1:]:
|
||||
if r[0]:
|
||||
top, field = r[0], r[1]
|
||||
elif r[1]:
|
||||
field = r[1]
|
||||
grade = r[2]
|
||||
if top == "기술계":
|
||||
name = "측량기술사" if grade == "기술사" else f"측량{grade}기술자"
|
||||
elif top == "기능계":
|
||||
name = f"{field}{grade}기능사"
|
||||
else:
|
||||
name = grade
|
||||
rows.append(
|
||||
row(
|
||||
name,
|
||||
num(r[3]),
|
||||
f"측량노임 {' '.join(x for x in (top, field if top == '기능계' else '', grade) if x)}",
|
||||
)
|
||||
)
|
||||
head = {
|
||||
"그룹": "인력",
|
||||
"원문": "측량노임",
|
||||
"판": "2026-01-01",
|
||||
"공표일": "2025-12-22",
|
||||
"원천": origin(src),
|
||||
"비고": [
|
||||
"한국공간정보산업협회 2025년도 측량업체 임금실태조사 결과 공표(국가승인통계 제377001호) · 20 직종 · 1인 1일 · 월평균 근무일수 20.3일",
|
||||
"이름 = 기술계 「측량<등급>기술자」·측량기술사 · 기능계 「<분야><등급>기능사」 · 기타 원문 그대로 — 원문 칸은 출처에",
|
||||
],
|
||||
}
|
||||
return src, head, rows
|
||||
|
||||
|
||||
def cm():
|
||||
src = (
|
||||
COST
|
||||
/ "노임단가_건설사업관리_한국건설엔지니어링협회/2026년도_건설사업관리기술인_임금실태조사결과_공표_한국건설엔지니어링협회.md"
|
||||
)
|
||||
t = table_after(src.read_text(encoding="utf-8"), "| 구 분 |")
|
||||
rows = []
|
||||
for r in t[1:]:
|
||||
grade = r[0].replace(" ", "")
|
||||
rows.append(
|
||||
row(
|
||||
f"{grade}건설사업관리기술인",
|
||||
num(r[1]),
|
||||
f"건설사업관리노임 {grade}",
|
||||
환산비=float(r[2]),
|
||||
)
|
||||
)
|
||||
head = {
|
||||
"그룹": "인력",
|
||||
"원문": "건설사업관리노임",
|
||||
"판": "2026-01-01",
|
||||
"공표일": "2025-12-29",
|
||||
"원천": origin(src),
|
||||
"비고": [
|
||||
"한국건설엔지니어링협회 2026년도 건설사업관리기술인 노임가격(정책381-3804호 · 통계청승인 제378001호) · 일임금액",
|
||||
"환산비 = 원문 Si(고급 = 1.000 기준 등급별 비율 · 소수 넷째 자리 반올림)",
|
||||
],
|
||||
}
|
||||
return src, head, rows
|
||||
|
||||
|
||||
def sw():
|
||||
src = (
|
||||
COST
|
||||
/ "노임단가_SW기술자_한국인공지능소프트웨어산업협회/2026년_적용_SW기술자_평균임금_공표.md"
|
||||
)
|
||||
t = table_after(src.read_text(encoding="utf-8"), "| 구 분 | 월평균임금")
|
||||
rows = []
|
||||
for r in t[1:]:
|
||||
mark, name = r[0].split(" ", 1)
|
||||
n = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰".index(mark) + 1
|
||||
more = {"월평균임금": num(r[1]), "시간평균임금": num(r[3])}
|
||||
if r[4]:
|
||||
more["포함직무"] = r[4]
|
||||
rows.append(row(name, num(r[2]), f"SW노임 {n}", **more))
|
||||
head = {
|
||||
"그룹": "인력",
|
||||
"원문": "SW노임",
|
||||
"판": "2026-01-01",
|
||||
"공표일": "2025-12-19",
|
||||
"원천": origin(src),
|
||||
"비고": [
|
||||
"한국인공지능·소프트웨어산업협회 2026년 적용 SW기술자 평균임금(통계승인 제375001호) · 값 = 일평균임금(M/D) · 적용 2026-01-01 ~ 2026-12-31",
|
||||
"⚠ 기본급·제수당·상여금·퇴직급여충당금·법인부담금(4대보험) 모두 포함 — 건설노임(기본급 일급)과 성격 다름",
|
||||
"일평균 = 월평균 ÷ 20.5일 · 시간평균 = 일평균 ÷ 8시간(원문)",
|
||||
],
|
||||
}
|
||||
return src, head, rows
|
||||
|
||||
|
||||
def forest():
|
||||
src = COST / "노임단가_산림_산림청/2026년_나무의사_수목치료기술자_노임단가_알림.md"
|
||||
text = src.read_text(encoding="utf-8")
|
||||
rows = [
|
||||
row(name, num(v), f"산림노임 {name}")
|
||||
for name, v in re.findall(r"(나무의사|수목치료기술자) ([\d,]+)원", text)
|
||||
]
|
||||
head = {
|
||||
"그룹": "인력",
|
||||
"원문": "산림노임",
|
||||
"판": "2026-01-01",
|
||||
"공표일": "2026-01-06",
|
||||
"원천": origin(src),
|
||||
"비고": [
|
||||
"산림청 2026년 나무의사·수목치료기술자 노임단가 알림(수목진료 표준품셈 예정가격 작성용) · 2025년 소비자물가상승률 2.1% 반영(원문) · 통계 승인 노임 아님",
|
||||
"판 = 2026년 적용(원문에 적용 시작일 없음 · 1월 1일로 적음)",
|
||||
],
|
||||
}
|
||||
return src, head, rows
|
||||
|
||||
|
||||
def check_md(src: Path, rows: list[dict], table_head: str | None = None) -> None:
|
||||
"""md 의 그 표(없으면 본문) 안 큰 수 모음 ⊇ 파일 값 · 개수 같음."""
|
||||
text = src.read_text(encoding="utf-8")
|
||||
if table_head:
|
||||
text = "\n".join("|".join(r) for r in table_after(text, table_head))
|
||||
have = Counter(num(x) for x in _INT.findall(text))
|
||||
got = Counter(r["값"] for r in rows)
|
||||
assert not (got - have), (src.name, got - have)
|
||||
print(f" {src.parent.name[5:]}: {len(rows)} 줄 · 값 합 {sum(got.elements()):,} · md 대조 같음")
|
||||
|
||||
|
||||
# ── 준용대상 짝짓기 · 통합 ───────────────────────────────────────────
|
||||
def merge_list() -> dict[str, list[str]]:
|
||||
"""건설노임 원문 「통합직종」 · 「직종명칭 변경」 표 — 옛 이름 → 새 이름(여럿이면 갈림)."""
|
||||
text = (CAK / "2026-09-01/2. 임금적용요령.md").read_text(encoding="utf-8")
|
||||
out: dict[str, list[str]] = {}
|
||||
lines = [cells(x) for x in text.splitlines() if x.startswith("| ") and "---" not in x]
|
||||
for r in lines:
|
||||
if len(r) != 6 or not r[0].isdigit():
|
||||
continue
|
||||
for no, old, new in ((r[0], r[1], r[2]), (r[3], r[4], r[5])):
|
||||
if not no.isdigit() or not new:
|
||||
continue
|
||||
for name in re.split(r"\+", old.replace("<br>", "").replace(" ", "")):
|
||||
if name and name != new:
|
||||
out.setdefault(name, []).append(new)
|
||||
return out
|
||||
|
||||
|
||||
def candidates(name: str, pool: dict[str, str]) -> list[str]:
|
||||
"""비슷한 이름 — 새 이름이 옛 이름 뿌리(괄호 앞)와 괄호 안 말을 모두 품음."""
|
||||
m = re.fullmatch(r"(.+?)\((.+)\)", name)
|
||||
base, qual = (m.group(1), m.group(2)) if m else (name, "")
|
||||
return [ref for n, ref in pool.items() if n != name and base in n and qual in n]
|
||||
|
||||
|
||||
def main():
|
||||
books = {
|
||||
"엔지니어링노임": engineering(),
|
||||
"측량노임": survey(),
|
||||
"건설사업관리노임": cm(),
|
||||
"SW노임": sw(),
|
||||
"산림노임": forest(),
|
||||
}
|
||||
table_heads = {
|
||||
"엔지니어링노임": "| 구분 |",
|
||||
"측량노임": "| 구분 |",
|
||||
"건설사업관리노임": "| 구 분 |",
|
||||
"SW노임": "| 구 분 | 월평균임금",
|
||||
}
|
||||
print("갈래별 노임")
|
||||
for book, (src, head, rows) in books.items():
|
||||
assert len({r["열쇠"] for r in rows}) == len(rows)
|
||||
check_md(src, rows, table_heads.get(book))
|
||||
expect = {
|
||||
"엔지니어링노임": 56,
|
||||
"측량노임": 20,
|
||||
"건설사업관리노임": 4,
|
||||
"SW노임": 17,
|
||||
"산림노임": 2,
|
||||
}
|
||||
assert {b: len(v[2]) for b, v in books.items()} == expect
|
||||
|
||||
# 이름 → ref (새 다섯 + 건설·제조 공표)
|
||||
wage = json.loads((MD / "인력_건설노임.json").read_text(encoding="utf-8"))
|
||||
make = json.loads((MD / "인력_제조노임.json").read_text(encoding="utf-8"))
|
||||
pool = {r["이름"]: f"인력:{b}:{r['열쇠']}" for b, (_, _, rows) in books.items() for r in rows}
|
||||
old_pool = {r["이름"]: f"인력:{d['원문']}:{r['열쇠']}" for d in (wage, make) for r in d["줄"]}
|
||||
merges = merge_list()
|
||||
|
||||
alias_path = MD / "인력_준용대상.json"
|
||||
alias = json.loads(alias_path.read_text(encoding="utf-8"))
|
||||
moved, kept = {}, []
|
||||
for r in alias["줄"]:
|
||||
name = r["이름"]
|
||||
if name in pool:
|
||||
moved[f"인력:준용대상:{r['열쇠']}"] = pool[name]
|
||||
continue
|
||||
assert name not in old_pool, name
|
||||
r = {k: v for k, v in r.items() if k not in ("통합", "후보")}
|
||||
old = name.replace("스레이트", "슬레이트") # 원문 표기 「슬레이트공」
|
||||
new = merges.get(old, [])
|
||||
refs = [old_pool[n] for n in new if n in old_pool]
|
||||
if len(refs) == 1:
|
||||
r["통합"] = refs[0]
|
||||
elif refs:
|
||||
r["통합"] = None
|
||||
r["후보"] = "통합 갈림: " + " · ".join(new) + " — 관리자가 준용으로 고름"
|
||||
near = candidates(name, {**pool, **old_pool})
|
||||
if near and "후보" not in r:
|
||||
r["후보"] = "비슷한 이름: " + " · ".join(near)
|
||||
kept.append(r)
|
||||
alias["비고"] = (
|
||||
"값을 확인 못 한 직종(공표 노임에 이름이 온전히 같은 직종 없음) — 이름·단위만 · 준용 = 관리자가 고르는 공표 직종 열쇠(`인력:원문:열쇠`)"
|
||||
" · 통합 = 건설노임 원문 「통합·명칭변경 직종」 목록의 새 직종(엔진이 그 값을 씀) · 후보 = 비슷한 이름(값 안 씀) · 비고 = 이 직종을 요구한 품셈 절"
|
||||
)
|
||||
head = {k: v for k, v in alias.items() if k != "줄"}
|
||||
dump_master(alias_path, head, kept)
|
||||
|
||||
for book, (src, head, rows) in books.items():
|
||||
dump_master(MD / f"인력_{book}.json", head, rows)
|
||||
|
||||
# 로직 참조 옮김 — 긴 열쇠부터
|
||||
n_ref = 0
|
||||
for path in sorted(MD.glob("로직_*.json")):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
new = text
|
||||
for old in sorted(moved, key=len, reverse=True):
|
||||
n_ref += new.count(old)
|
||||
new = new.replace(old, moved[old])
|
||||
if new != text:
|
||||
path.write_bytes(new.encode("utf-8"))
|
||||
print(
|
||||
f"준용대상 {len(alias['줄'])} → 옮김 {len(moved)} {sorted(moved.values())} · 남김 {len(kept)} · 로직 참조 {n_ref} 곳"
|
||||
)
|
||||
print(
|
||||
f" 통합 {sum(1 for r in kept if r.get('통합'))} · 통합 갈림 {sum(1 for r in kept if '통합 갈림' in str(r.get('후보')))} · 비슷한 이름 {sum(1 for r in kept if '비슷한' in str(r.get('후보')))}"
|
||||
)
|
||||
|
||||
estimate(wage)
|
||||
|
||||
|
||||
# ── 건설노임 미공표 산정 ─────────────────────────────────────────────
|
||||
def estimate(wage: dict) -> None:
|
||||
text = (CAK / "미공표직종_추적.md").read_text(encoding="utf-8")
|
||||
steps = [
|
||||
(r[0].split(" → ")[0], r[0].split(" → ")[1], r[1], num(r[2]), num(r[3]))
|
||||
for r in table_after(text, "| 앞 → 뒤 |")[1:]
|
||||
]
|
||||
hist = {r[0].split()[0]: r[1:] for r in table_after(text, "| 직종 | 2020.1.1")[1:]}
|
||||
dates = table_after(text, "| 직종 | 2020.1.1")[0][1:]
|
||||
shown = {r[0].split()[0]: r for r in table_after(text, "| 직종 | 마지막 공표")[1:]}
|
||||
until = "2026.1.1"
|
||||
n = 0
|
||||
for r in wage["줄"]:
|
||||
if r.get("상태") != "미공표":
|
||||
continue
|
||||
vals = hist[r["열쇠"]]
|
||||
last = max(i for i, v in enumerate(vals) if v != "-" and dates[i] != "2026.9.1")
|
||||
at, base = dates[last], num(vals[last])
|
||||
value, used, start = Decimal(base), [], at
|
||||
while start != until:
|
||||
a, b, kinds, top, bottom = next(s for s in steps if s[0] == start)
|
||||
value = value * top / bottom
|
||||
used.append(f"{top:,}/{bottom:,}({kinds}종)")
|
||||
start = b
|
||||
value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
want = shown[r["열쇠"]]
|
||||
assert str(value) == want[4].replace(",", ""), (r["열쇠"], value, want[4])
|
||||
note = []
|
||||
if want[5] != want[6]:
|
||||
note.append(
|
||||
f"다산소프트(ESTX) {want[5]} — 2020.1(123종) → 2020.9 를 127종 평균으로 이음 · 우리는 원문 회차 평균 그대로(123종끼리)"
|
||||
)
|
||||
after = vals[dates.index("2026.9.1")]
|
||||
if after != "-":
|
||||
note.append(f"2026.9.1 공표 {after} 있음(뒤 시기 · 판 밖)")
|
||||
r["산정"] = {
|
||||
"값": value,
|
||||
"근거": f"원문 4-라 · {at} 공표 {base:,} × 전체직종 평균 증가율 " + " × ".join(used),
|
||||
"마지막공표": at,
|
||||
**({"비고": " · ".join(note)} if note else {}),
|
||||
}
|
||||
n += 1
|
||||
notes = wage["비고"]
|
||||
notes[0] = notes[0].replace("(값은 관리자 판단 — 마스터에 짓지 않음)", "(산정 칸)")
|
||||
notes[:] = [x for x in notes if not x.startswith(("산정 =", "2026-09-01"))]
|
||||
notes.append(
|
||||
"산정 = 미공표 직종의 원문 4-라 산정값(원 미만 처리 안 함 · 소수 둘째 자리) — 엔진 차례 값 → 준용(관리자가 골랐으면) → 통합 → 산정 · 호표 출처에 「산정: …」"
|
||||
)
|
||||
notes.append(
|
||||
"2026-09-01 적용분 확보됨(옛공표 md `2026-09-01/`) — 마스터 판은 2026-01-01 그대로"
|
||||
)
|
||||
head = {k: v for k, v in wage.items() if k != "줄"}
|
||||
dump_master(MD / "인력_건설노임.json", head, [_plain(r) for r in wage["줄"]])
|
||||
print(f"건설노임 미공표 산정 {n} 줄 · 미공표직종_추적.md 5절 값과 같음")
|
||||
|
||||
|
||||
def _plain(r: dict) -> dict:
|
||||
"""Decimal → JSON 수(소수 둘째 자리 그대로)."""
|
||||
out = dict(r)
|
||||
if "산정" in out:
|
||||
out["산정"] = {k: float(v) if isinstance(v, Decimal) else v for k, v in out["산정"].items()}
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -34,6 +34,11 @@ BOOKS = (
|
||||
"건설품셈",
|
||||
"건설노임",
|
||||
"제조노임",
|
||||
"엔지니어링노임",
|
||||
"측량노임",
|
||||
"건설사업관리노임",
|
||||
"SW노임",
|
||||
"산림노임",
|
||||
"준용대상",
|
||||
"나라장터자재",
|
||||
"오피넷유가",
|
||||
@@ -171,12 +176,23 @@ def _check_linked(where: str, row: dict) -> list[str]:
|
||||
|
||||
|
||||
def check_links(whole: mf.Master) -> list[str]:
|
||||
"""품셈재료 연결 · 시중물가 조달 연결이 가리키는 줄."""
|
||||
"""품셈재료 연결 · 시중물가 조달 연결 · 인력 준용·통합이 가리키는 줄."""
|
||||
out = []
|
||||
for key, row in whole.index.get(("재료", "품셈재료"), {}).items():
|
||||
link = row.get("연결")
|
||||
if isinstance(link, str) and link not in whole.index.get(("재료", "시중물가"), {}):
|
||||
out.append(f"재료:품셈재료:{key} · 연결 「{link}」 이 시중물가에 없음")
|
||||
for (group, book), rows in whole.index.items():
|
||||
for key, row in rows.items() if group == "인력" else ():
|
||||
for col in ("준용", "통합"):
|
||||
ref = row.get(col)
|
||||
if ref in (None, ""):
|
||||
continue
|
||||
ref = str(ref) if ":" in str(ref) else f"인력:{book}:{ref}"
|
||||
try:
|
||||
whole.get(ref)
|
||||
except mf.FormulaError:
|
||||
out.append(f"인력:{book}:{key} · {col} 「{ref}」 없는 요소")
|
||||
nara = whole.index.get(("재료", "나라장터자재"), {})
|
||||
for key, row in whole.index.get(("재료", "시중물가"), {}).items():
|
||||
source = str(row["값"]["조달"].get("출처") or "")
|
||||
|
||||
@@ -240,14 +240,23 @@ class Master:
|
||||
return row
|
||||
|
||||
def value(self, ref: str):
|
||||
"""요소 값 · 준용 이름 — 값이 비고 「준용」(같은 원문 열쇠 또는 `그룹:원문:열쇠`)이 있으면 그 요소 값."""
|
||||
"""요소 값 · 출처 글 — 값이 비면 준용(관리자가 고른 열쇠) → 통합(원문 통합 직종) → 산정(원문 4-라) 차례.
|
||||
준용·통합 = 같은 원문 열쇠 또는 `그룹:원문:열쇠`."""
|
||||
row = self.get(ref)
|
||||
alias = row.get("준용")
|
||||
if row.get("값") is not None or alias in (None, ""):
|
||||
if row.get("값") is not None:
|
||||
return row.get("값"), None
|
||||
alias = str(alias)
|
||||
target = self.get(alias if ":" in alias else ":".join(ref.split(":", 2)[:2] + [alias]))
|
||||
return target.get("값"), target.get("이름") or alias
|
||||
for col in ("준용", "통합"):
|
||||
alias = row.get(col)
|
||||
if alias not in (None, ""):
|
||||
alias = str(alias)
|
||||
target = self.get(
|
||||
alias if ":" in alias else ":".join(ref.split(":", 2)[:2] + [alias])
|
||||
)
|
||||
return target.get("값"), f"{col}: {target.get('이름') or alias}"
|
||||
guess = row.get("산정")
|
||||
if isinstance(guess, dict) and guess.get("값") is not None:
|
||||
return guess["값"], f"산정: 원문 4-라 · 마지막 공표 {guess.get('마지막공표')}"
|
||||
return None, None
|
||||
|
||||
def logic(self, ref: str) -> dict:
|
||||
return self.get(f"로직:{ref}")
|
||||
|
||||
@@ -86,8 +86,7 @@ def element(master: mf.Master, ref: str, env: dict) -> tuple[object, str | None]
|
||||
"""요소 값과 출처 — 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값` 그대로(출처 None)."""
|
||||
row = master.get(ref)
|
||||
if not ref.startswith(LINKED):
|
||||
value, alias = master.value(ref)
|
||||
return value, f"준용: {alias}" if alias else None
|
||||
return master.value(ref)
|
||||
link = row.get("연결")
|
||||
if not link:
|
||||
return None, None
|
||||
|
||||
Reference in New Issue
Block a user