Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

# Conflicts:
#	M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts
This commit is contained in:
2026-11-05 19:43:52 +09:00
70 changed files with 1764 additions and 8 deletions
@@ -0,0 +1,128 @@
"""로직 줄 칸 「개정」 — 원문 절 머리 개정 연도 목록 자동 채움(PLAN 794-2b · 분석 652-794-2a 5 장).
건설 = 본문 md 절 머리 괄호 「('08, '20, 25년 보완)」 · 「('19년 신설, '24년 보완)」 의 연도 전부.
산림 = [2026](2026-01-01 제정 · 절 머리에 연도 없음). 절 머리에 연도가 없으면 윗절로 올라감.
못 찾음 = 칸 없음(빈칸). 글 꼴 그대로 — 줄 「출처」 바로 뒤에 끼우거나 옛 값만 갈음.
사람 입력 없음 · 셈 값 바뀜 0.
./venv/Scripts/python.exe resources/master_data/scripts/build_로직_개정.py [--check]
"""
from __future__ import annotations
import json
import re
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
MASTER = ROOT / "resources/master_data"
SRC = ROOT / "resources/knowledge/original/원가계산"
PART = {"01": "공통", "02": "토목", "03": "건축", "04": "기계설비", "05": "유지관리"}
HEAD = re.compile(r"^#{2,4}\s*(\d+(?:-\d+)+)\.?\s+(.*)$")
FOREST = [2026]
def years(title: str) -> list[int]:
"""절 제목 괄호 속 「년」 글의 두 자리 연도 → [2008, 2020, 2025]."""
found = {
int(y) + (2000 if int(y) < 50 else 1900)
for g in re.findall(r"\(([^()]*년[^()]*)\)", title)
for y in re.findall(r"(?<!\d)(\d\d)(?!\d)", g)
}
return sorted(found)
def heads() -> dict[tuple, list[int]]:
"""(책, 부문, 절 번호) → 개정 연도 목록(머리는 있는데 연도 없음 = [])."""
out: dict[tuple, list[int]] = {}
for f in sorted((SRC / "건설공사_표준품셈/본문").rglob("*.md")):
part = PART.get(f.relative_to(SRC / "건설공사_표준품셈/본문").parts[0][:2])
for line in f.read_text(encoding="utf-8").splitlines():
if m := HEAD.match(line):
out.setdefault(("건설품셈", part, m.group(1)), years(m.group(2)))
for f in sorted((SRC / "산림_표준품셈/본문").rglob("*.md")):
for line in f.read_text(encoding="utf-8").splitlines():
if m := HEAD.match(line):
out.setdefault(("산림품셈", None, m.group(1)), FOREST)
return out
def revision(h: dict, row: dict, book: str) -> tuple[list[int] | None, str]:
"""(연도 목록 | None, 까닭) — 절 머리 → 윗절 차례로 연도 든 첫 머리."""
num = str(row.get("원문번호") or "")
if not num:
return None, "원문번호 없음"
part = str(row.get("구분") or "").replace("건설품셈", "").strip() or None
seen = False
while num:
got = h.get((book, part if book == "건설품셈" else None, num))
if got:
return got, "채움"
seen |= got is not None
num = num.rsplit("-", 1)[0] if "-" in num else ""
return None, "머리에 연도 없음" if seen else "절 머리 못 찾음"
def _spans(text: str) -> list[tuple[int, int]]:
"""「줄」 배열 속 줄 하나하나의 글 자리."""
m = re.search(r'"줄"\s*:\s*\[', text)
dec, pos, out = json.JSONDecoder(), m.end(), []
while True:
while text[pos] in " \t\r\n,":
pos += 1
if text[pos] == "]":
return out
_, end = dec.raw_decode(text, pos)
out.append((pos, end))
pos = end
def put(row_text: str, value: list[int] | None) -> str:
"""줄 글에 「개정」 끼움 · 갈음 · 지움 — 출처 바로 뒤 · 출처와 같은 구분자(한 줄이면 「, 」)."""
old = re.search(r'"개정": \[[^\]]*\](,\s*)?', row_text)
if old:
row_text = row_text[: old.start()] + row_text[old.end() :]
if value is None:
return row_text
src = re.search(r'"출처": "(?:[^"\\]|\\.)*"(,\s*)', row_text)
if not src:
raise ValueError("줄 출처 없음")
sep = src.group(1)
return row_text[: src.end()] + f'"개정": {json.dumps(value)}' + sep + row_text[src.end() :]
def build(check: bool = False) -> Counter:
h, why = heads(), Counter()
for path in sorted(MASTER.glob("로직_*.json")):
raw = path.read_bytes().decode("utf-8")
data = json.loads(raw)
if data.get("원문") not in ("건설품셈", "산림품셈"):
continue
rows, spans = data["줄"], _spans(raw)
assert len(rows) == len(spans), path.name
parts, last = [], 0
for row, (s, e) in zip(rows, spans):
value, tag = revision(h, row, data["원문"])
why[tag] += 1
why[row["키"][:2] + (" 채움" if value else " 빈칸")] += 1
parts += [raw[last:s], put(raw[s:e], value)]
if value:
row["개정"] = value
else:
row.pop("개정", None)
last = e
out = "".join(parts + [raw[last:]])
assert json.loads(out) == data, path.name # 끼운 칸 말고 같음
if out != raw:
why["바뀐 파일"] += 1
if not check:
path.write_bytes(out.encode("utf-8"))
return why
if __name__ == "__main__":
got = build("--check" in sys.argv)
for k, v in sorted(got.items()):
print(f"{k}: {v}")
@@ -0,0 +1,86 @@
"""잔토처리(인력) 단독 로직 한 줄(PLAN 794-2b · 분석 652-794-2a 2 장 6 번 · 요구 653).
근거 = 건설 공통 3-2-1 비고 「현장 내에서 소운반하여 깔고 고르는 잔토처리는 ㎥당 보통인부
0.2인을 별도 계상」 · 호표 보통인부 = QC000172 「잔토처리 보통인부」(GC000083 옵션과 같은 표).
이미 있으면 그대로 · 글 꼴 그대로 GC000083 줄 뒤에 끼움 · 키 = `_키대장` 다음 GC 번호.
뒤에 `build_로직_개정.py` 를 돌림.
./venv/Scripts/python.exe resources/master_data/scripts/build_로직_잔토처리.py
"""
from __future__ import annotations
import json
import re
from pathlib import Path
MASTER = Path(__file__).resolve().parents[1]
FILE = MASTER / "로직_건설품셈_03장_토공사.json"
BOOK = MASTER / "_키대장.json"
NAME = "잔토처리(인력)"
LEDGER = f"공통 3-2-1 {NAME}"
AFTER = "GC000083"
SRC = "건설품셈 공통 3-2-1 비고"
def _row(key: str, nl: str) -> str:
q = "찾기(QC000172, 구분='잔토처리 보통인부').값"
lines = [
"{",
f' "키": "{key}", "원문번호": "3-2-1", "구분": "건설품셈 공통", '
'"상세구분": "03장 토공사",',
f' "이름": "{NAME}",',
' "결과단위": "원/㎥",',
f' "출처": "{SRC}",',
' "소유": "공용",',
' "비고": "현장 내 소운반하여 깔고 고르는 잔토처리 — ㎥당 보통인부 0.2인 별도 계상",', # noqa: E501
' "입력": [],',
' "중간": [],',
' "호표": [',
" {",
' "종류": "인력",',
' "요소": "LB000002",',
' "이름": "보통인부",',
' "단위": "인",',
f' "수량": "{q}",',
' "비목": "노무비",',
f' "출처": "{SRC}"',
" }",
" ],",
' "덧줄": [],',
' "끝수": null',
"}",
]
return (nl + " ").join(lines)
def build() -> str:
text = FILE.read_bytes().decode("utf-8")
book = BOOK.read_bytes().decode("utf-8")
have = re.search(rf'"(GC\d{{6}})": \{{"원문번호": "{re.escape(LEDGER)}"', book)
if have and f'"키": "{have.group(1)}"' in text:
return have.group(1)
nl = "\r\n" if "\r\n" in text else "\n"
bnl = "\r\n" if "\r\n" in book else "\n"
n = int(re.search(r'"GC": (\d+)', book).group(1))
key = f"GC{n:06d}"
entry = json.dumps({"원문번호": LEDGER, "파일": FILE.name}, ensure_ascii=False)
prev = re.search(rf' "GC{n - 1:06d}": .*?,{bnl}', book)
book = book.replace(f'"GC": {n}', f'"GC": {n + 1}', 1)
at = book.index(prev.group(0)) + len(prev.group(0))
book = book[:at] + f' "{key}": {entry},{bnl}' + book[at:]
# 줄 끼움 — AFTER 줄 글 끝 뒤
start = text.index(f'"키": "{AFTER}"')
start = text.rindex("{", 0, start)
_, end = json.JSONDecoder().raw_decode(text, start)
out = text[:end] + "," + nl + " " + _row(key, nl) + text[end:]
data = json.loads(out)
assert [r["키"] for r in data["줄"]].count(key) == 1
json.loads(book)
FILE.write_bytes(out.encode("utf-8"))
BOOK.write_bytes(book.encode("utf-8"))
return key
if __name__ == "__main__":
print(build())