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
This commit is contained in:
@@ -23,6 +23,7 @@ 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]
|
||||
@@ -57,7 +58,7 @@ def notice_day(row: dict) -> str:
|
||||
|
||||
def build() -> dict:
|
||||
raw = json.loads(RAW.read_text(encoding="utf-8"))
|
||||
master = json.loads(MASTER.read_text(encoding="utf-8"))
|
||||
master = mk.old_view(MASTER.name)
|
||||
records = master["줄"]
|
||||
|
||||
history: dict[str, list[dict]] = {}
|
||||
@@ -99,3 +100,4 @@ if __name__ == "__main__":
|
||||
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())
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
돌리기: `./venv/Scripts/python.exe resources/master_data/scripts/_migrate_base.py`
|
||||
옛 파일은 `old/` 에서 읽기만 함. 새 파일은 `resources/master_data/` 첫 층에 씀. 값은 바꾸지 않음.
|
||||
`dump_master` 는 `_build_mat_price.py` 도 씀.
|
||||
`dump_master` 는 `_build_mat_price.py` 도 씀. 한 번 돌린 옮기기 — 지금 틀(키 · 인력.json)은 뒤에 `_migrate_keys.py` 로 옮김.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,7 +14,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
TARGET = HERE.parent / "계수_산림품셈_01장.json"
|
||||
TARGET = HERE.parent / "계수_산림품셈_01장_적용기준.json" # 한 번 돌림 — 이미 들어감
|
||||
BODY = HERE.parents[2] / "resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준"
|
||||
SRC = "산림품셈 "
|
||||
|
||||
@@ -599,7 +599,7 @@ def main() -> None:
|
||||
text = TARGET.read_bytes().decode("utf-8")
|
||||
crlf = "\r\n" in text
|
||||
text = text.replace("\r\n", "\n")
|
||||
if '"열쇠": "1-2-2 ' in text:
|
||||
if '"원문번호": "1-2-2 ' in text:
|
||||
raise SystemExit("이미 추가됨")
|
||||
A, T = from_1_02(), from_text()
|
||||
front = [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
본문 md(확정) 표를 읽어 기계 한 줄로 합침. 값은 md 에서만 · 계산·추정 없음.
|
||||
실행: python resources/master_data/scripts/build_기계_건설품셈.py [--dry]
|
||||
옛 모양(열쇠)으로 쓰고 `master_keys.adopt` 가 기계.json(키 · 세부분류 건설품셈)으로 받음.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -10,6 +11,9 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
|
||||
DIR = (
|
||||
REPO
|
||||
/ "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제08장_건설기계"
|
||||
@@ -653,3 +657,4 @@ if __name__ == "__main__":
|
||||
with open(OUT, "w", encoding="utf8", newline="\n") as f:
|
||||
f.write(text)
|
||||
print("씀", OUT)
|
||||
print("새 틀로 받음", mk.adopt(OUT.parent))
|
||||
|
||||
@@ -12,6 +12,7 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import master_keys as mk
|
||||
from _계수_공통2장 import lift_table
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -681,6 +682,7 @@ if __name__ == "__main__":
|
||||
if dup:
|
||||
raise SystemExit(f"열쇠 겹침 {dup}")
|
||||
OUT.write_text(dump(tables), encoding="utf-8", newline="\n")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
for f, (a, b, _) in count.items():
|
||||
print(f"{f} · 옮긴 표 {a} · 계수 표 {b}")
|
||||
print("합계", sum(a for a, _, _ in count.values()), sum(b for _, b, _ in count.values()))
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
import master_keys as mk # noqa: E402
|
||||
from _md표_읽기 import ( # noqa: E402
|
||||
LOSE,
|
||||
PCT,
|
||||
@@ -607,3 +608,4 @@ if __name__ == "__main__":
|
||||
f()
|
||||
write()
|
||||
print(len(TABLES), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -9,6 +9,7 @@ import re
|
||||
import sys
|
||||
|
||||
import _토목_읽기 as base
|
||||
import master_keys as mk
|
||||
from _설비_계수 import rows_of
|
||||
from _설비_읽기 import DIV, read_chapter
|
||||
from _설비_표 import convert
|
||||
@@ -58,3 +59,4 @@ if __name__ == "__main__":
|
||||
for w in sys.argv[1:] or ["7", "8", "9", "10", "11", "12", "13"]:
|
||||
o = build(int(w), f"소요량_건설품셈_기계설비{w}장.json")
|
||||
print(w, len(o.tables), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
import master_keys as mk
|
||||
from _md표_읽기 import N, R, join
|
||||
from _토목_계수 import lift
|
||||
from _토목_손 import SPECIAL
|
||||
@@ -163,3 +164,4 @@ if __name__ == "__main__":
|
||||
}[w]
|
||||
o = build(ch[0], ch[1], SPECIAL.get(w, {}))
|
||||
print(w, len(o.tables), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
이음 = 원문 회차의 전체직종 평균을 그대로 이음(미공표직종_추적.md 4절) · 산정값은 같은 md 5절과 대조.
|
||||
|
||||
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_인력_노임.py
|
||||
옛 모양(원문별 파일 · 열쇠)으로 쓰고 끝에 `master_keys.adopt` 가 인력.json(키 · 조사)으로 받음.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
@@ -19,6 +20,7 @@ from decimal import ROUND_HALF_UP, Decimal
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -289,14 +291,14 @@ def main():
|
||||
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"))
|
||||
wage = mk.old_view("인력_건설노임.json")
|
||||
make = mk.old_view("인력_제조노임.json")
|
||||
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"))
|
||||
alias = mk.old_view(alias_path.name)
|
||||
moved, kept = {}, []
|
||||
for r in alias["줄"]:
|
||||
name = r["이름"]
|
||||
@@ -318,7 +320,7 @@ def main():
|
||||
r["후보"] = "비슷한 이름: " + " · ".join(near)
|
||||
kept.append(r)
|
||||
alias["비고"] = (
|
||||
"값을 확인 못 한 직종(공표 노임에 이름이 온전히 같은 직종 없음) — 이름·단위만 · 준용 = 관리자가 고르는 공표 직종 열쇠(`인력:원문:열쇠`)"
|
||||
"값을 확인 못 한 직종(공표 노임에 이름이 온전히 같은 직종 없음) — 이름·단위만 · 준용 = 관리자가 고르는 공표 직종 키(`LB000123`)"
|
||||
" · 통합 = 건설노임 원문 「통합·명칭변경 직종」 목록의 새 직종(엔진이 그 값을 씀) · 후보 = 비슷한 이름(값 안 씀) · 비고 = 이 직종을 요구한 품셈 절"
|
||||
)
|
||||
head = {k: v for k, v in alias.items() if k != "줄"}
|
||||
@@ -345,6 +347,7 @@ def main():
|
||||
)
|
||||
|
||||
estimate(wage)
|
||||
print("새 틀로 받음", mk.adopt())
|
||||
|
||||
|
||||
# ── 건설노임 미공표 산정 ─────────────────────────────────────────────
|
||||
|
||||
@@ -23,6 +23,7 @@ import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
from compare_estx import ESTX, ROOT, SLOTS, Estx, bare_spec, base_name # noqa: E402
|
||||
|
||||
@@ -169,12 +170,13 @@ def main():
|
||||
"비고": [
|
||||
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
||||
"값 = 시중(물가지 다섯 · 면수) · 조달(값 · 출처 · 기준) — 기본가·채택값 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<열쇠>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<키>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
|
||||
],
|
||||
}
|
||||
nara = json.loads(NARA.read_text(encoding="utf-8"))["줄"]
|
||||
nara = mk.old_view(NARA.name)["줄"]
|
||||
dump_master(OUT, head, build(rows, nara_links(rows, nara)))
|
||||
check(rows)
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 나라장터 연결 → 키
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -122,9 +122,10 @@ def labor(estx: Estx, files: dict):
|
||||
heads = [r["Name"] for r in rows if r.get("Name") and not r.get("Unite")]
|
||||
print("## 노임 — ESTX 머리줄", heads)
|
||||
ours = {}
|
||||
for fname, tag in (("인력_건설노임.json", "건설"), ("인력_제조노임.json", "제조")):
|
||||
for r in files[fname]["줄"]:
|
||||
ours.setdefault((tag, norm(r["이름"])), r)
|
||||
for survey, tag in (("건설업", "건설"), ("제조업", "제조")):
|
||||
for r in files["인력.json"]["줄"]:
|
||||
if r["조사"] == survey:
|
||||
ours.setdefault((tag, norm(r["이름"])), r)
|
||||
theirs = {}
|
||||
for r in rows:
|
||||
if r.get("Unite") != "인":
|
||||
@@ -230,7 +231,7 @@ def material(estx: Estx, files: dict):
|
||||
for r in files["재료_품셈재료.json"]["줄"]:
|
||||
if r.get("연결"):
|
||||
continue
|
||||
mine = {base_name(r["이름"]), base_name(r["열쇠"])}
|
||||
mine = {base_name(r["이름"]), base_name(r["원문번호"])}
|
||||
hits = [e for m in mine for e in by_name.get(m, [])]
|
||||
if hits:
|
||||
have.append((r, hits))
|
||||
@@ -243,15 +244,15 @@ def material(estx: Estx, files: dict):
|
||||
f"### 재료_품셈재료 못 이음 {len(have) + len(near) + len(none)} — ESTX 품명 같음 {len(have)} · 품명 끝 같음 {len(near)} · 없음 {len(none)}"
|
||||
)
|
||||
for r, hits in near:
|
||||
print(f" 비슷 {r['열쇠']} | {sorted({e['Name'] for e in hits})[:4]}")
|
||||
print(f" 비슷 {r['원문번호']} | {sorted({e['Name'] for e in hits})[:4]}")
|
||||
for r, hits in have:
|
||||
s = bare_spec(r["이름"], r.get("규격"))
|
||||
same = [e for e in hits if s and spec_match(s, bare_spec(e["Name"], e.get("Size")))]
|
||||
h = (same or hits)[0]
|
||||
print(
|
||||
f" 있음 {r['열쇠']} [{r.get('단위')}] | ESTX {len(hits)}줄 규격같음 {len(same)} | 예 {h['Name']} {h.get('Size')} {h.get('Unite')} {h['TC']:.0f}"
|
||||
f" 있음 {r['원문번호']} [{r.get('단위')}] | ESTX {len(hits)}줄 규격같음 {len(same)} | 예 {h['Name']} {h.get('Size')} {h.get('Unite')} {h['TC']:.0f}"
|
||||
)
|
||||
print(" 없음", [r["열쇠"] for r, _ in none])
|
||||
print(" 없음", [r["원문번호"] for r, _ in none])
|
||||
|
||||
|
||||
# ── (3) 기계경비 ──────────────────────────────────────────────────────
|
||||
@@ -284,7 +285,7 @@ def mach_key(name, size) -> str:
|
||||
def machine_cost(master: mf.Master, mid: str, region="전국평균"):
|
||||
"""로직으로 계산 — 고르기 밖 기계는 메모리에서만 고르기에 더함."""
|
||||
for key in LOGICS:
|
||||
row = master.logic("건설품셈:" + key)
|
||||
row = master.logic("GC:" + key)
|
||||
spec = next(i for i in row["입력"] if i["이름"] == "기계")
|
||||
added = mid not in spec["고르기"]
|
||||
if added:
|
||||
@@ -292,7 +293,7 @@ def machine_cost(master: mf.Master, mid: str, region="전국평균"):
|
||||
try:
|
||||
given = {"기계": mid, "지역": region}
|
||||
given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row["입력"])}
|
||||
return key, mf.run(master, "건설품셈:" + key, given)
|
||||
return key, mf.run(master, "GC:" + key, given)
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError):
|
||||
continue
|
||||
finally:
|
||||
@@ -306,17 +307,17 @@ def machine(estx: Estx, files: dict, master: mf.Master):
|
||||
for r in estx.table("MachTitleDat"):
|
||||
if r.get("Unite") == "hr" and r.get("TC"):
|
||||
est.setdefault(mach_key(r.get("Name"), r.get("Size")), r)
|
||||
ours = files["기계_건설품셈.json"]["줄"]
|
||||
ours = [m for m in files["기계.json"]["줄"] if m["세부분류"] == "건설품셈"]
|
||||
pairs = [(m, est[k]) for m in ours if (k := mach_key(m["이름"], m["규격"])) in est]
|
||||
print(f"## 기계 — 우리 {len(ours)} · ESTX 시간당 {len(est)} · 기종·규격 짝 {len(pairs)}")
|
||||
out = []
|
||||
for m, e in pairs:
|
||||
key, res = machine_cost(master, m["열쇠"])
|
||||
key, res = machine_cost(master, m["원문번호"])
|
||||
if not res:
|
||||
continue
|
||||
out.append((m, e, key, res))
|
||||
print(f" 로직 계산됨 {len(out)}")
|
||||
first = [x for x in out if x[0]["열쇠"].startswith(FIRST)]
|
||||
first = [x for x in out if x[0]["원문번호"].startswith(FIRST)]
|
||||
for label, group in (("임도 기계", first), ("전체", out)):
|
||||
print(f"### {label}")
|
||||
print(" 계:", spread([rate(e["TC"], res["계"]) for m, e, k, res in group]))
|
||||
@@ -340,18 +341,18 @@ def machine(estx: Estx, files: dict, master: mf.Master):
|
||||
e.get("GC") and abs(rate(e["GC"], res["재료비"])) > 5
|
||||
):
|
||||
print(
|
||||
f" {m['열쇠']} {m['이름']} {m['규격']} {k} | ESTX 재 {e.get('GC', 0):.0f} 경 {e['JC']:.0f} | 우리 재 {res['재료비']:.0f} 경 {res['경비']:.0f}"
|
||||
f" {m['원문번호']} {m['이름']} {m['규격']} {k} | ESTX 재 {e.get('GC', 0):.0f} 경 {e['JC']:.0f} | 우리 재 {res['재료비']:.0f} 경 {res['경비']:.0f}"
|
||||
)
|
||||
for m, e, k, res in first:
|
||||
print(
|
||||
f" {m['열쇠']} {m['이름']} {m['규격']} | ESTX {e['TC']:.0f} (노 {e.get('NC', 0):.0f} 재 {e.get('GC', 0):.0f} 경 {e.get('JC', 0):.0f})"
|
||||
f" {m['원문번호']} {m['이름']} {m['규격']} | ESTX {e['TC']:.0f} (노 {e.get('NC', 0):.0f} 재 {e.get('GC', 0):.0f} 경 {e.get('JC', 0):.0f})"
|
||||
f" | 우리 {res['계']:.0f} (노 {res['노무비']:.0f} 재 {res['재료비']:.0f} 경 {res['경비']:.0f}) {rate(e['TC'], res['계']):+.1f}%"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── (4) 일위대가 ──────────────────────────────────────────────────────
|
||||
# (ESTX 제목 코드, 로직 열쇠, 입력[, 단위 환산 배수 — 우리 값 × 배수 = ESTX 단위])
|
||||
# (ESTX 제목 코드, 로직 원문번호, 입력[, 단위 환산 배수 — 우리 값 × 배수 = ESTX 단위])
|
||||
REGION = {"지역": "전국평균", "보정작업": "해당 없음"}
|
||||
PAIRS = [
|
||||
(
|
||||
@@ -456,14 +457,15 @@ def with_estx_materials(master: mf.Master, estx: Estx, code: str):
|
||||
"""ESTX 일위대가의 자재 단가를 품명으로 우리 빈 재료(값 없음)에만 임시로 넣은 사본 — 파일에 안 씀."""
|
||||
temp = copy.copy(master)
|
||||
temp.index = {k: dict(v) for k, v in master.index.items()}
|
||||
temp.numbers = {k: dict(v) for k, v in master.numbers.items()}
|
||||
items = [
|
||||
d
|
||||
for d in estx.details("HeadMainDat").get(code, [])
|
||||
if str(d.get("Code", "")).startswith("M")
|
||||
]
|
||||
put = []
|
||||
for book in (("재료", "자체"), ("재료", "나라장터자재")):
|
||||
rows = temp.index.get(book, {})
|
||||
for tid in ("MN",):
|
||||
rows = temp.index.get(tid, {})
|
||||
for key, row in list(rows.items()):
|
||||
if row.get("값") is not None:
|
||||
continue
|
||||
@@ -472,6 +474,7 @@ def with_estx_materials(master: mf.Master, estx: Estx, code: str):
|
||||
)
|
||||
if hit:
|
||||
rows[key] = {**row, "값": Decimal(str(hit["TU"]))}
|
||||
temp.numbers[tid][str(row["원문번호"])] = rows[key]
|
||||
put.append(f"{key}={hit['TU']:.0f}")
|
||||
return temp, put
|
||||
|
||||
@@ -484,11 +487,11 @@ def unit_price(estx: Estx, master: mf.Master):
|
||||
h = heads[code]
|
||||
k = Decimal(scale[0]) if scale else Decimal(1)
|
||||
temp, put = with_estx_materials(master, estx, code)
|
||||
row = temp.logic("건설품셈:" + key)
|
||||
row = temp.logic("GC:" + key)
|
||||
given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row.get("입력", []))}
|
||||
given = {k: Decimal(str(v)) if isinstance(v, (int, float)) else v for k, v in given.items()}
|
||||
try:
|
||||
res = mf.run(temp, "건설품셈:" + key, given)
|
||||
res = mf.run(temp, "GC:" + key, given)
|
||||
res = {c: res[c] * k for c in ("계", *mf.COST_ITEMS)}
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError) as e:
|
||||
print(f" {code} {h['Name']} {h.get('Size')} | {key} | 멈춤: {str(e)[:120]}")
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
MASTER = Path(__file__).resolve().parent.parent
|
||||
@@ -62,12 +63,18 @@ def save_book(book: dict, path: Path = BOOK) -> None:
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def issue(book: dict, tid: str, number: str, file: str) -> str:
|
||||
"""(테이블ID, 원문번호) 의 키 — 대장에 있으면 그 키 · 없으면(원문번호 빈 새 줄 포함) 다음 번호를 줌."""
|
||||
def lookup(book: dict) -> dict:
|
||||
"""(테이블ID, 원문번호) → 키 — 대장에서 한 번 세워 둠(`finish` 가 걷음)."""
|
||||
index = book.setdefault("_찾기", {})
|
||||
if not index:
|
||||
for key, v in book["키"].items():
|
||||
index[(key[:2], v["원문번호"])] = key
|
||||
return index
|
||||
|
||||
|
||||
def issue(book: dict, tid: str, number: str, file: str) -> str:
|
||||
"""(테이블ID, 원문번호) 의 키 — 대장에 있으면 그 키 · 없으면 다음 번호(원문번호 빈 새 줄 포함)."""
|
||||
index = lookup(book)
|
||||
key = index.get((tid, number)) if number else None
|
||||
if key:
|
||||
book["키"][key]["파일"] = file
|
||||
@@ -97,3 +104,254 @@ def stamp(
|
||||
key = row.pop("키", None) or issue(book, tid, number, file)
|
||||
out.append({"키": key, "원문번호": number, **(extra or {}), **row})
|
||||
return out
|
||||
|
||||
|
||||
# ── 빌더 잇기 — 빌더는 옛 모양(「열쇠」 · 원문별 파일 이름)으로 쓰고 adopt 가 새 틀로 받음 ──
|
||||
LABOR = {
|
||||
"건설노임": "건설업",
|
||||
"제조노임": "제조업",
|
||||
"엔지니어링노임": "엔지니어링",
|
||||
"측량노임": "측량",
|
||||
"건설사업관리노임": "건설사업관리",
|
||||
"SW노임": "SW",
|
||||
"산림노임": "산림",
|
||||
"준용대상": "준용대상",
|
||||
}
|
||||
MERGED = {
|
||||
"인력": ("인력.json", "조사", LABOR),
|
||||
"기계": ("기계.json", "세부분류", {"건설품셈": "건설품셈", "산림품셈": "산림품셈"}),
|
||||
}
|
||||
DIVISIONS = ("공통", "토목", "건축", "기계설비", "유지관리")
|
||||
OLD_CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈)_(\D*)(\d+)장\.json$")
|
||||
CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈|자체)_(\d\d)장_.+\.json$")
|
||||
_BODY = MASTER.parents[1] / "resources/knowledge/original/원가계산"
|
||||
_OLDREF = re.compile(r"(인력|재료|기계|소요량|계수|로직|환율|요율):([^:\s·]+):(.+?)(?=\s·\s|$)")
|
||||
_STR = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
|
||||
|
||||
def title(book: str, division: str, number: int) -> str:
|
||||
"""원문 장 제목 — 본문 폴더 이름 그대로(「제07장_지붕_및_홈통공사」 → 「지붕_및_홈통공사」)."""
|
||||
base = _BODY / "산림_표준품셈/본문"
|
||||
if book == "건설품셈":
|
||||
index = DIVISIONS.index(division) + 1
|
||||
base = _BODY / "건설공사_표준품셈/본문" / f"{index:02d}_{division}부문"
|
||||
(hit,) = base.glob(f"제{number:02d}장_*")
|
||||
return hit.name.split("_", 1)[1]
|
||||
|
||||
|
||||
def chapter_file(group: str, book: str, division: str, number: int) -> str:
|
||||
return f"{group}_{book}_{number:02d}장_{title(book, division, number)}.json"
|
||||
|
||||
|
||||
def _items(data: dict) -> str:
|
||||
return "표" if data.get("그룹") in ("소요량", "계수") else "줄"
|
||||
|
||||
|
||||
def _read(path: Path, exact: bool = True) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
return json.loads(text, parse_float=Decimal, parse_int=Decimal) if exact else json.loads(text)
|
||||
|
||||
|
||||
def old_view(name: str, folder: Path = MASTER) -> dict:
|
||||
"""옛 파일 이름으로 읽기(빌더용 · 수는 float) — 줄의 「열쇠」 = 원문번호 · 키·갈래 칸 뺌.
|
||||
준용·통합·연결 같은 참조는 키 그대로."""
|
||||
m = re.match(r"^(인력|기계)_(.+)\.json$", name)
|
||||
if m:
|
||||
file, slot, labels = MERGED[m.group(1)]
|
||||
data = _read(folder / file, exact=False)
|
||||
label = labels[m.group(2)]
|
||||
head = {"그룹": m.group(1), **data[slot][label]}
|
||||
rows = [r for r in data["줄"] if r.get(slot) == label]
|
||||
else:
|
||||
c = OLD_CHAPTER.match(name)
|
||||
path = folder / (chapter_file(c[1], c[2], c[3], int(c[4])) if c else name)
|
||||
data = _read(path, exact=False)
|
||||
head, rows, slot = (
|
||||
{k: v for k, v in data.items() if k != _items(data)},
|
||||
data[_items(data)],
|
||||
"",
|
||||
)
|
||||
drop = ("키", "원문번호", slot)
|
||||
out = [{"열쇠": r["원문번호"], **{k: v for k, v in r.items() if k not in drop}} for r in rows]
|
||||
return {**head, _items(head): out}
|
||||
|
||||
|
||||
def _old_ref(book: dict, text: str) -> str:
|
||||
"""옛 참조 `그룹:원문:열쇠` → 키(대장에 없으면 그대로 — check_master 가 잡음)."""
|
||||
group, src, number = (text.split(":", 2) + ["", ""])[:3]
|
||||
try:
|
||||
tid = table_id(group, src)
|
||||
except KeyError:
|
||||
return text
|
||||
return lookup(book).get((tid, number), text)
|
||||
|
||||
|
||||
def _convert(book: dict, row: dict, names: dict) -> dict:
|
||||
"""줄 속 옛 참조 — 준용·통합 · 후보 글(키 옆 이름) · 품셈재료 연결 · 시중물가 조달 나라장터 연결."""
|
||||
for col in ("준용", "통합"):
|
||||
value = row.get(col)
|
||||
if isinstance(value, str) and ":" in value:
|
||||
row[col] = _old_ref(book, value)
|
||||
if isinstance(row.get("후보"), str):
|
||||
|
||||
def one(m):
|
||||
key = _old_ref(book, m.group(0))
|
||||
return f"{key} {names.get(key, m.group(3))}" if KEY.fullmatch(key) else key
|
||||
|
||||
row["후보"] = _OLDREF.sub(one, row["후보"])
|
||||
link = row.get("연결")
|
||||
if isinstance(link, str) and not KEY.fullmatch(link):
|
||||
row["연결"] = lookup(book).get(("MT", link), link)
|
||||
value = row.get("값")
|
||||
supply = value.get("조달") if isinstance(value, dict) else None
|
||||
if isinstance(supply, dict) and str(supply.get("출처") or "").startswith("나라장터:"):
|
||||
code = supply["출처"].split(":", 1)[1]
|
||||
if not KEY.fullmatch(code): # 묶음은 새로 만듦 — 옛 줄과 나눠 쓰지 않게
|
||||
source = "나라장터:" + lookup(book).get(("MN", code), code)
|
||||
row["값"] = {**value, "조달": {**supply, "출처": source}}
|
||||
return row
|
||||
|
||||
|
||||
def _store():
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(MASTER.parents[1]))
|
||||
from M01_MasterData import M01_MasterData_Store as st
|
||||
|
||||
return st
|
||||
|
||||
|
||||
def dump_rows(head: dict, rows: list[dict]) -> str:
|
||||
"""요소 파일 — 머리는 들여쓰기 2칸 · 줄은 한 줄씩(길면 펼침)."""
|
||||
st = _store()
|
||||
out = ["{"] + [f" {st._atom(k)}: {st.dump(v, 2)}," for k, v in head.items()]
|
||||
out.append(' "줄": [')
|
||||
for i, row in enumerate(rows):
|
||||
line = st._flat(row)
|
||||
body = line if len(line) <= 450 else st.dump(row, 4, expand=True)
|
||||
out.append(" " + body + ("," if i < len(rows) - 1 else ""))
|
||||
return "\n".join(out + [" ]", "}"]) + "\n"
|
||||
|
||||
|
||||
def keep_text(text: str, old: dict, new: dict) -> str | None:
|
||||
"""글 모양 그대로 — 「열쇠」 칸 · 바뀐 글 · 머리 부문·차례만 갈음 · 읽어 맞대어 다르면 None."""
|
||||
rows_old, rows_new = old[_items(old)], new[_items(new)]
|
||||
spots = list(re.finditer(r'"열쇠": ("(?:[^"\\]|\\.)*"|[-\d.]+)', text))
|
||||
if len(spots) != len(rows_new):
|
||||
return None
|
||||
parts, at = [], 0
|
||||
for m, row in zip(spots, rows_new):
|
||||
number = json.dumps(row["원문번호"], ensure_ascii=False)
|
||||
parts += [text[at : m.start()], f'"키": "{row["키"]}", "원문번호": {number}']
|
||||
at = m.end()
|
||||
text = "".join(parts) + text[at:]
|
||||
swap = {}
|
||||
for a_row, b_row in zip(rows_old, rows_new):
|
||||
for k in b_row:
|
||||
if k in a_row and a_row[k] != b_row[k]:
|
||||
for a, b in zip(_strings(a_row[k]), _strings(b_row[k])):
|
||||
if a != b:
|
||||
swap[json.dumps(a, ensure_ascii=False)] = json.dumps(b, ensure_ascii=False)
|
||||
text = _STR.sub(lambda m: swap.get(m.group(0), m.group(0)), text)
|
||||
add = "".join(
|
||||
f" {json.dumps(k, ensure_ascii=False)}: {_store()._atom(new[k])},\n"
|
||||
for k in ("부문", "차례")
|
||||
if k in new and k not in old
|
||||
)
|
||||
if add:
|
||||
text = re.sub(r'(\n "원문": "[^"]*",\n)', lambda m: m.group(1) + add, text, count=1)
|
||||
same = json.loads(text, parse_float=Decimal, parse_int=Decimal) == new
|
||||
return text if same else None
|
||||
|
||||
|
||||
def _strings(v):
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
elif isinstance(v, dict):
|
||||
for x in v.values():
|
||||
yield from _strings(x)
|
||||
elif isinstance(v, list):
|
||||
for x in v:
|
||||
yield from _strings(x)
|
||||
|
||||
|
||||
def order_chapters(folder: Path = MASTER) -> None:
|
||||
"""장 파일 머리 「차례」 — 그룹마다 산림 1~14장 → 건설 공통 · 토목 · 건축 · 기계설비 · 유지관리."""
|
||||
found = {}
|
||||
for path in folder.glob("*.json"):
|
||||
m = CHAPTER.match(path.name)
|
||||
if m:
|
||||
data = _read(path)
|
||||
division = data.get("부문") or ""
|
||||
rank = (m[2] != "산림품셈", DIVISIONS.index(division) if division else -1, int(m[3]))
|
||||
found.setdefault(m[1], []).append((rank, path.name, data.get("차례")))
|
||||
for items in found.values():
|
||||
for i, (_, name, had) in enumerate(sorted(items), start=1):
|
||||
if had != i:
|
||||
path = folder / name
|
||||
text = re.sub(
|
||||
r'"차례": \d+', f'"차례": {i}', path.read_text(encoding="utf-8"), count=1
|
||||
)
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def adopt(folder: Path = MASTER) -> list[str]:
|
||||
"""빌더가 옛 모양으로 쓴 파일 → 새 틀(키 · 원문번호 · 한 테이블 · 장 파일 이름) · 옛 파일은 지움.
|
||||
키는 대장의 (테이블ID, 원문번호) 키 · 없으면 다음 번호. 로직은 키로 적어 넣으므로 받지 않음."""
|
||||
book = load_book(folder / BOOK.name)
|
||||
names = {}
|
||||
if (folder / "인력.json").is_file():
|
||||
names = {r["키"]: r.get("이름") for r in _read(folder / "인력.json")["줄"]}
|
||||
done = []
|
||||
for path in sorted(folder.glob("*.json")):
|
||||
if path.name.startswith("_"):
|
||||
continue
|
||||
data = _read(path)
|
||||
rows = data.get(_items(data)) or []
|
||||
if not rows or "열쇠" not in rows[0]:
|
||||
continue
|
||||
group, src = data["그룹"], data["원문"]
|
||||
if group == "로직":
|
||||
raise SystemExit(f"{path.name} · 로직은 키로 적음 — 옛 모양은 받지 않음")
|
||||
c = OLD_CHAPTER.match(path.name)
|
||||
target = chapter_file(c[1], c[2], c[3], int(c[4])) if c else path.name
|
||||
target = MERGED[group][0] if group in MERGED else target
|
||||
if c and c[3]: # 건설품셈 원문번호 앞에 부문(「기계설비 7-1-1 …」)
|
||||
head = c[3] + " "
|
||||
rows = [
|
||||
r if str(r["열쇠"]).startswith(head) else {**r, "열쇠": head + r["열쇠"]}
|
||||
for r in rows
|
||||
]
|
||||
data[_items(data)] = rows
|
||||
stamped = [
|
||||
_convert(book, r, names) for r in stamp(book, table_id(group, src), rows, target)
|
||||
]
|
||||
if group in MERGED:
|
||||
file, slot, labels = MERGED[group]
|
||||
label = labels[src]
|
||||
whole = _read(folder / file) if (folder / file).is_file() else {"그룹": group}
|
||||
heads = whole.setdefault(slot, {})
|
||||
heads[label] = {k: v for k, v in data.items() if k not in ("그룹", "줄")}
|
||||
stamped = [
|
||||
{"키": r["키"], "원문번호": r["원문번호"], slot: label, **r} for r in stamped
|
||||
]
|
||||
keep = [r for r in whole.get("줄", []) if r.get(slot) != label]
|
||||
rows_all = sorted(keep + stamped, key=lambda r: r["키"])
|
||||
text = dump_rows({k: v for k, v in whole.items() if k != "줄"}, rows_all)
|
||||
else:
|
||||
head = {"그룹": group, "원문": src}
|
||||
if c and c[3]:
|
||||
head["부문"] = c[3]
|
||||
if c:
|
||||
head["차례"] = Decimal(0) # order_chapters 가 채움
|
||||
rest = {k: v for k, v in data.items() if k not in head}
|
||||
new = {**head, **rest, _items(data): stamped}
|
||||
old_text = path.read_text(encoding="utf-8")
|
||||
text = keep_text(old_text, data, new) or _store().dump(new)
|
||||
(folder / target).write_text(text, encoding="utf-8", newline="\n")
|
||||
if target != path.name:
|
||||
path.unlink()
|
||||
done.append(target)
|
||||
save_book(finish(book), folder / BOOK.name)
|
||||
order_chapters(folder)
|
||||
return done
|
||||
|
||||
Reference in New Issue
Block a user