feat(782-2): 건설 공통 1장 나머지 기초 — 계수 표 33 · 환경관리비 · 공사용수 로직 · 성격 칸 · check_master 목차 대조

- 계수_건설품셈_01장_적용기준.json 덧(CC000495~527): 규칙 · 계수만 절(1-2-2 · 1-2-3 · 1-2-4 · 1-2-5 · 1-2-6 · 1-2-8 · 1-3-2 · 1-4-1 · 1-4-2 · 1-5-1~10)마다 「규칙 글」 표(원문 글 그대로) + 수 표(단위 · 소수 표준 · 금액 단위 · 체적 변화율 L · C · 체적환산계수 f · 파쇄된 돌 10∼40% · 잡재료 2∼5% · 작업반장 · 공구손료 3% · 시공단위 품 소수자리 · 품 산정 식 · 예시 · 건설폐기물 발생량 · 공사용수) · 1-2-9 소규모 적용시공량 · 다수 규격 예시 = 계수로만
- 표 칸 「성격」 — 기존 1-3 · 1-4 표 33 + 1-2-9 둘(같은 성격 묶음 · 1-2-9 ↔ 1-4-6 작업시간 제한 한 성격) · 기존 표의 다른 글자 그대로
- 로직_건설품셈_01장_적용기준.json 덧: GC001192 건설폐기물 발생량(1-5-4 · 면적 × TON/㎡ × 폐콘크리트 증가율) · GC001193 공사용수(1-5-6 · 작업량 × 단위용수 × 잡용수율) · 원문 「-」 칸 = 줄 없음(멈춤)
- 1-2-9 소규모 · 1-4 품의 할증 로직 = 만들지 않음(PLAN 783-1)
- 빌더 덧 build_로직_건설품셈_01장_적용기준.py 나머지(+ _적용기준_나머지_표 · _적용기준_나머지_로직) · 키 = master_keys.issue(adopt 는 로직을 받지 않음)
- check_master 「목차」(check_master_toc.py) — 공통 1장 원문 절 목록 ↔ 마스터 전수 · 통째 빠진 절 · 로직 없는 품·표·식 절 · 1-2-9 · 1-4-2 = 783-1 로 미룸 표지
- 지킴 시험 test_782_2_common_ch1.py · test_782_1_carry_127.py 로직 줄 목록 = 앞 셋만 봄(같은 파일에 1-5 로직이 붙어 낡음)
- check_master 전후 같음(+ 목차 0) · 기존 로직 1513 셈 값 전후 같음 · 계산전부 1515

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 20:42:38 +09:00
co-authored by Claude Opus 5.5
parent 8ddb5bbda7
commit a8ed3390e9
10 changed files with 1843 additions and 4 deletions
@@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
"""건설품셈 공통 1장 나머지(782-2) — 1-5-4 · 1-5-6 로직 둘 + 계수 표 넣기 · 기존 할증 표 「성격」 칸.
`build_로직_건설품셈_01장_적용기준.py 나머지` 가 부름 · 한 번 돌림(이미 넣었으면 멈춤).
- 계수 표 = `_적용기준_나머지_표.all_tables()` · 절 차례 자리에 끼움(처음으로 절 번호가 더 큰 기존 표 앞 · 없으면 끝).
- 로직 = 로직_건설품셈_01장_적용기준.json 끝에 덧 · 키 = master_keys.issue(adopt 는 로직을 받지 않음).
- 1-2-9 소규모 · 1-4 할증 로직 = 만들지 않음(PLAN 783-1).
"""
from __future__ import annotations
import json
import re
from decimal import Decimal
import _적용기준_나머지_표 as T
import build_로직_건설품셈_01장_적용기준 as base
import build_로직_건설품셈_08장_건설기계 as b08
import master_keys as mk
MASTER = base.MASTER
COEF, LOGIC, HEAD = base.COEF, base.LOGIC, base.HEAD
SRC4, SRC6 = "건설품셈 공통 1-5-4", "건설품셈 공통 1-5-6"
# 기존 1-3 · 1-4 표 → 성격(같은 성격 묶음 · 1-4-2 이중 적용 불가) — 기본 = 같은 표 · 딸린 표(정의 · 한도)는 본 표와 같음
NATURE = {
**{f"CC{n:06d}": "재료 할증" for n in range(1, 9)},
"CC000009": "작업지연 — 현장조건",
"CC000010": "작업지연 — 열차의 운행빈도",
"CC000011": "작업지연 — 건물 층수",
"CC000012": "작업지연 — 건물 층수",
**{f"CC{n:06d}": "지세/지형 — 지세" for n in range(13, 20)},
"CC000020": "지세/지형 — 도심지",
"CC000021": "위험 — 고소작업",
"CC000022": "위험 — 교량상 작업",
**{f"CC{n:06d}": "위험 — 터널내 작업" for n in range(23, 26)},
"CC000026": "위험 — 유해 작업",
"CC000027": "위험 — 유해 작업",
"CC000028": T.SMALL,
"CC000029": "작업제한 — 폭염시 휴게시간",
"CC000030": "작업환경 — 야간",
"CC000031": "작업환경 — 특수작업",
"CC000032": "작업환경 — 기타",
"CC000033": "작업환경 — 기타",
}
def inp(name, desc, unit=None, pick=None, src=None):
return base.inp(name, desc, unit, pick, src)
def logics(K: dict) -> list[dict]:
waste, water = K["건설폐기물 발생량"], K["공사용수"]
w_rows = T.t_1_5_4()[0]["줄"]
builds = list(dict.fromkeys(r["구조"] for r in w_rows))
kinds = [r["구분"] for r in T.t_1_5_6()[0]["줄"] if r["구분"] != "잡용수"]
env = {
"열쇠": "공통 1-5-4 건설폐기물 발생량",
**HEAD,
"이름": "건설폐기물 발생량 — 건설 공통 1-5-4",
"결과단위": "TON",
"출처": f"{SRC4} 2",
"소유": "공용",
"비고": "발생량 = 면적 × 단위면적당 발생량(TON/㎡) × (1 + 증가율 ÷ 100) · 증가율 = 폐콘크리트류만(주③ 파일 시공 15% · 20% 이내 · 주④ 세륜 시설 개소당 3% 이내 — 설계자가 넣음 · 0 = 증가 없음) · 원문 칸 「-」 = 줄 없음(멈춤) · 건축물 해체는 설계도서 산출 우선(원문 2) · 주⑤ 현장 여건 조정",
"시험입력": {
"공사": "신축",
"구조": "철근콘크리트조",
"폐기물": "폐콘크리트류",
"면적": 1000,
"증가율": 0,
},
"입력": [
inp("공사", "신축 · 해체", pick=["신축", "해체"], src=f"{SRC4} 2"),
inp("구조", "건물 구조(주거용 = 단독주택 · 아파트)", pick=builds, src=f"{SRC4} 2"),
inp("폐기물", "건설폐기물 종류", pick=list(T.WASTE), src=f"{SRC4} 2"),
inp("면적", "단위면적 산출 면적", "㎡", src=f"{SRC4} 2"),
inp(
"증가율",
"폐콘크리트 수량 증가(%) — 주③ 파일 15 · 20% 이내 · 주④ 세륜 개소당 3% 이내 · 0 = 없음",
"%",
src=f"{SRC4} 2 주③ ④",
),
],
"중간": [
{
"이름": "원단위",
"식": f"찾기({waste}, 공사=공사, 구조=구조, 폐기물=폐기물).발생량",
"단위": "TON/㎡",
},
{
"이름": "증가",
"식": "만약(폐기물 == '폐콘크리트류', 증가율 / 100, 0)",
},
],
"결과": {"식": "면적 * 원단위 * (1 + 증가)", "출처": f"{SRC4} 2"},
"끝수": None,
}
use = {
"열쇠": "공통 1-5-6 공사용수",
**HEAD,
"이름": "공사용수 — 건설 공통 1-5-6",
"결과단위": "㎥",
"출처": f"{SRC6} 3",
"소유": "공용",
"비고": "용수량 = 작업량 × 단위당 용수(㎥) × (1 + 잡용수율 ÷ 100) · 양생 물 포함([주]) · 해석: 잡용수 = 원문 「사용량비의 40∼50%」 — 잡용수율 0 = 더하지 않음(설계자가 40∼50 넣음)",
"시험입력": {"구분": "콘크리트혼합 및 양생", "작업량": 100, "잡용수율": 0},
"입력": [
inp("구분", "용수 구분", pick=kinds, src=f"{SRC6} 3"),
inp("작업량", "구분의 단위 분모 수량(㎡ · ㎥ · 1,000매 · 표면적 ㎡)", src=f"{SRC6} 3"),
inp(
"잡용수율",
"잡용수(%) — 원문 사용량비의 40∼50% · 0 = 더하지 않음",
"%",
src=f"{SRC6} 3",
),
],
"중간": [
{"이름": "단위용수", "식": f"찾기({water}, 구분=구분).수량", "단위": "㎥"},
],
"결과": {"식": "작업량 * 단위용수 * (1 + 잡용수율 / 100)", "출처": f"{SRC6} 3"},
"끝수": None,
}
return [env, use]
def emit(t: dict) -> str:
"""기존 표 글 모양 + 「성격」 칸(용도 다음 줄)."""
text = base.emit(t)
if "성격" in t:
text = re.sub(
r'(\n "용도": [^\n]*\n)',
lambda m: m[1] + f' "성격": {json.dumps(t["성격"], ensure_ascii=False)},\n',
text,
count=1,
)
return text
def _sec(no: str) -> tuple[int, ...]:
return b08._sec(no)
def insert_tables(text: str, made: list[dict]) -> str:
"""새 표 → 처음으로 절 번호가 더 큰 기존 표 앞(없으면 끝)."""
starts = [
(m.start(), m[1])
for m in re.finditer(r' \{\n "키": "CC\d+", "원문번호": "([^"]+)"', text)
]
pieces, last, left = [], 0, list(made)
for at, no in starts:
due = [t for t in left if _sec(t["원문번호"]) < _sec(no)]
if due:
pieces += [text[last:at], "".join(emit(t) + ",\n" for t in due)]
left, last = [t for t in left if t not in due], at
tail = text.rindex("\n ]\n}")
pieces.append(text[last:tail])
if left:
pieces.append(",\n" + ",\n".join(emit(t) for t in left))
pieces.append(text[tail:])
return "".join(pieces)
def add_nature(text: str) -> str:
"""기존 1-3 · 1-4 표에 「성격」 한 줄 — 다른 글자는 그대로."""
for key, nature in NATURE.items():
pat = re.compile(
rf'( "키": "{key}",[^\n]*\n(?: (?!"용도")[^\n]*\n)*? "용도": [^\n]*\n)'
)
text, n = pat.subn(
lambda m: m[1] + f' "성격": {json.dumps(nature, ensure_ascii=False)},\n',
text,
count=1,
)
assert n == 1, key
return text
def run() -> None:
coef_path = MASTER / COEF
text = coef_path.read_text(encoding="utf-8")
if '"원문번호": "1-5-4"' in text:
raise SystemExit("이미 넣음 — 멈춤")
old = json.loads(text, parse_float=Decimal, parse_int=Decimal)["표"]
book = mk.load_book()
made = T.all_tables()
K = {}
for t in made:
t["키"] = mk.issue(book, "CC", t.pop("열쇠"), COEF)
K[t["이름"]] = K[t["키"]] = t["키"]
t.update(HEAD)
K["건설폐기물 발생량"] = next(
t["키"] for t in made if t["원문번호"] == "1-5-4" and "발생량" in t["이름"]
)
K["공사용수"] = next(
t["키"] for t in made if t["원문번호"] == "1-5-6" and "공사용수" in t["이름"]
)
rows = []
for r in logics(K):
r["키"] = mk.issue(book, "GC", r.pop("열쇠"), LOGIC)
r["원문번호"] = r["출처"].split()[2]
head = ["키", "원문번호"]
rows.append({k: r[k] for k in head + [k for k in r if k not in head]})
for t in made:
t["용도"]["로직키"] = [
r["키"] for r in rows if t["키"] in json.dumps(r, ensure_ascii=False)
]
new = add_nature(insert_tables(text, made))
data = json.loads(new, parse_float=Decimal, parse_int=Decimal)["표"]
keep = {t["키"]: {k: v for k, v in t.items() if k != "성격"} for t in data}
assert [keep[t["키"]] for t in old] == old # 기존 표 = 성격 칸만 더함
assert len(data) == len(old) + len(made)
assert sum("성격" in t for t in data) == len(NATURE) + 2
coef_path.write_text(new, encoding="utf-8", newline="\n")
b08.append_rows(LOGIC, rows)
mk.save_book(mk.finish(book))
print("계수", made[0]["키"], "~", made[-1]["키"], len(made), "· 로직", [r["키"] for r in rows])
@@ -0,0 +1,553 @@
# -*- coding: utf-8 -*-
"""건설품셈 공통 1장 나머지 — 규칙 · 계수만 절의 계수 표(782-2) · `build_로직_건설품셈_01장_적용기준.py 나머지` 가 부름.
- 절 범위 = `### N-N-N` 머리부터 다음 같은 · 윗 단계 머리 앞까지(요구 630).
- 절마다 「규칙 글」 표 하나(원문 글 그대로 · 항 = 「N.」 · 번호 없는 머리 글 = 「머리」) + 수 · 표가 있는 절은 수 표.
- 1-2-9 소규모 · 1-4-2 중복가산 = 계수로만(로직은 PLAN 783-1) · 성격 칸 = 같은 성격 묶음(1-4-2 이중 적용 불가 판정).
- 값은 원문 글자 그대로(N) · 「-」 칸 = 값 없음(줄 · 칸을 두지 않음) · 범위 = [아래, 위].
"""
from __future__ import annotations
import re
from pathlib import Path
import build_로직_건설품셈_01장_적용기준 as base
N, cell = base.N, base.cell
CH1 = base.BODY.parent
FILES = {
"1-2": "1-02_설계_및_수량.md",
"1-3": "1-03_재료_및_노임의_할증.md",
"1-4": "1-04_품의_할증.md",
"1-5": "1-05_기타.md",
}
SMALL = "작업시간 제한 · 소규모" # 1-2-9 ↔ 1-4-6 1 같은 성격(_제안_634 1-2)
def section(no: str, md: Path | None = None) -> tuple[str, list[str]]:
"""(절 이름, 절 줄) — 머리 줄부터 다음 같은 · 윗 단계 머리 앞까지."""
md = md or CH1 / FILES[no.rsplit("-", 1)[0]]
lines = md.read_text(encoding="utf-8").split("\n")
head = re.compile(rf"^(#+) {re.escape(no)} (.+)$")
a = next(i for i, x in enumerate(lines) if head.match(x))
level = len(head.match(lines[a])[1])
b = next(
(j for j in range(a + 1, len(lines)) if re.match(rf"^#{{1,{level}}} ", lines[j])),
len(lines),
)
name = re.sub(r"\s*\(['‘].*$", "", head.match(lines[a])[2]).strip()
return name, lines[a:b]
def _plain(x: str) -> str:
x = re.sub(r"<!--.*?-->", "", x)
x = re.sub(r"^(\d+)\\\.", r"\1.", x.strip())
return x.strip()
def rules(lines: list[str]) -> list[dict]:
"""규칙 글 — 표 · 그림 · 주석 줄을 뺀 원문 글을 항(「N.」)마다 한 줄."""
out, item = [], {"항": "머리", "글": []}
for x in lines[1:]:
if x.startswith(("|", "![")) or re.fullmatch(r"\s*<!--.*-->\s*", x):
continue
m = re.match(r"^(\d+)\\\. ", x)
if m:
out.append(item)
item = {"항": m[1], "글": []}
if _plain(x):
item["글"].append(_plain(x))
out.append(item)
return [{"항": r["항"], "규칙": "\n".join(r["글"])} for r in out if r["글"]]
def table(no, label, name, src, cols, rows, *, cond=None, note=(), base_="", extra=None, 대상=None):
return {
"열쇠": f"공통 {no} {label}",
"원문번호": no,
"이름": name,
"기준": base_,
"출처": f"건설품셈 공통 {no} {src}".strip(),
**(extra or {}),
"용도": {"공종": section(no)[0], "대상": 대상 or ["작업량 조건"], "로직키": []},
"조건": cond or {},
"값칸": cols,
"줄": rows,
"주": list(note),
}
def rule_table(no: str, extra: dict | None = None) -> dict:
name, lines = section(no)
return table(
no,
"규칙 글",
f"{name} — 규칙 글",
"",
{"규칙": "글"},
rules(lines),
cond={"항": "고르기"},
base_="원문 글 그대로",
extra={
"비고": "계산 칸 없는 규칙 · 기준 글 — 항 = 원문 「N.」 · 머리 = 번호 없는 글",
**(extra or {}),
},
)
def notes(lines: list[str], after: int = 0) -> list[str]:
"""`after` 줄 뒤 첫 「[주]」 묶음(빈 줄까지)."""
i = next(j for j in range(after, len(lines)) if lines[j].strip() == "[주]")
out = []
for x in lines[i + 1 :]:
if not x.strip():
break
if not x.startswith("<!--"):
out.append(_plain(x))
return out
def md_tables(lines: list[str]) -> list[list[list[str]]]:
return base.md_tables(lines)
def num(c: str):
"""값 칸 — 수 · 범위 · 「-」(None)."""
c = c.replace("\n", "").strip()
return cell(c)[0] if c not in ("", "-") else None
# ── 1-2-2 단위표준 ───────────────────────────────────────────────────
def t_1_2_2() -> list[dict]:
_, lines = section("1-2-2")
unit, money = md_tables(lines)
rows, kind = [], ""
for r in unit[2:]:
name, su, sd, qu, qd, note = r
kind = name or kind
row = {"종목": kind}
for col, v in (("규격단위", su), ("수량단위", qu), ("비고", note)):
if v not in ("", "-"):
row[col] = v
for col, v in (("규격소수자리", sd), ("수량소수자리", qd)):
if v == "1 위": # 원문 그대로 「1 위」
row[col], row[col + "원문"] = N("1"), v
elif v not in ("", "-"):
row[col] = N(v)
rows.append(dict(sorted(row.items(), key=lambda kv: list(UNIT_ORDER).index(kv[0]))))
i = lines.index("2\\. 금액의 단위표준")
t1 = table(
"1-2-2",
"설계서의 단위 및 소수의 표준",
"설계서의 단위 및 소수의 표준",
"1",
{
"규격단위": "글",
"규격소수자리": "자리",
"수량단위": "글",
"수량소수자리": "자리",
"비고": "글",
},
rows,
cond={"종목": "고르기"},
note=notes(lines),
extra={
"비고": "원문 둘째 줄 이하 규격(말뚝 · 돌망태 · 관류)은 윗줄 종목 · 같은 종목 여러 줄 = 원문 그대로 · 「-」 = 칸 없음"
},
)
t2 = table(
"1-2-2",
"금액의 단위표준",
"금액의 단위표준",
"2",
{"단위": "글", "자리": "원", "비고": "글"},
[{"종목": a, "단위": u, "자리": num(d), "비고": n} for a, u, d, n in money[1:]],
cond={"종목": "고르기"},
note=notes(lines, i),
)
return [t1, t2]
UNIT_ORDER = (
"종목",
"규격단위",
"규격소수자리",
"규격소수자리원문",
"수량단위",
"수량소수자리",
"수량소수자리원문",
"비고",
)
# ── 1-2-3 토질 ───────────────────────────────────────────────────────
def t_1_2_3() -> list[dict]:
_, lines = section("1-2-3")
md = md_tables(lines)
rate = md[1]
rows = []
for kind, lv, cv in rate[1:]:
row = {"종별": kind, "L": num(lv)}
if re.search(r"\d", cv):
row["C"] = num(cv)
else:
row["C원문"] = cv # 「별도 설계」
rows.append(row)
i = next(j for j, x in enumerate(lines) if x.startswith("다. 체적의 변화율"))
f_rows = [
{
"기준이 되는 q": "자연상태의 체적",
"자연상태의 체적": N("1"),
"흐트러진상태의 체적": "L",
"다져진후의 체적": "C",
},
{
"기준이 되는 q": "흐트러진상태의 체적",
"자연상태의 체적": "1/L",
"흐트러진상태의 체적": N("1"),
"다져진후의 체적": "C/L",
},
]
assert md[2][2][0] == "자연상태의 체적" and md[2][3][3] == "C/L"
stone = next(x for x in lines if x.startswith("다. 모암을 발파하여"))
assert "10∼40%" in stone
return [
table(
"1-2-3",
"체적의 변화율",
"체적의 변화율 L · C",
"3 다",
{"L": "", "C": ""},
rows,
cond={"종별": "고르기"},
note=notes(lines, i),
base_="L = 흐트러진 상태의 체적 ÷ 자연상태의 체적 · C = 다져진 상태의 체적 ÷ 자연상태의 체적",
extra={"비고": "범위 = [아래, 위] · 폐콘크리트 C = 원문 「별도 설계」(값 없음)"},
대상=["작업량 조건", "환산"],
),
table(
"1-2-3",
"체적환산계수",
"체적환산계수(f) — 구하는 Q ÷ 기준이 되는 q",
"3 라",
{"자연상태의 체적": "식", "흐트러진상태의 체적": "식", "다져진후의 체적": "식"},
f_rows,
cond={"기준이 되는 q": "고르기"},
extra={"비고": "값칸 = 구하는 Q · L · C = 체적의 변화율 표"},
대상=["작업량 조건", "환산"],
),
table(
"1-2-3",
"파쇄된 돌 발생량",
"토취장 및 골재원 — 규격품으로 못 쓰는 파쇄된 돌 발생량",
"4 다",
{"발생량": "%"},
[{"발생량": [N("10"), N("40")], "발생량원문": "10∼40%를 표준"}],
base_="모암을 발파하여 깬돌 등 규격품을 채취할 경우",
extra={"비고": "유용하면 경비 별도 계상 · 발생량은 무대(無代)"},
),
rule_table("1-2-3"),
]
# ── 1-2-4 · 1-2-5 · 1-2-6 ────────────────────────────────────────────
def t_1_2_4() -> list[dict]:
_, lines = section("1-2-4")
seven = next(x for x in lines if x.startswith("각 항목에 명시되어 있는 잡재료"))
assert "2∼5%" in seven
return [
table(
"1-2-4",
"잡재료 및 소모재료",
"잡재료 및 소모재료 — 명시 없는 잡재료 별도 계상 한도",
"7",
{"주재료비 대비": "%"},
[
{
"주재료비 대비": [N("2"), N("5")],
"주재료비 대비원문": "주재료비(재료비의 할증수량 제외)의 2∼5%까지",
}
],
extra={"비고": "산정근거 명시 · 항목에 명시된 잡재료는 그대로 계상"},
대상=["재료"],
),
rule_table("1-2-4"),
]
def t_1_2_5() -> list[dict]:
_, lines = section("1-2-5")
rows = []
for cond, text in md_tables(lines)[0][1:]:
m = re.fullmatch(r"보통인부 (\d+)인∼(\d+)인에 1인", text)
rows.append(
{
"현장작업조건": cond,
"보통인부": [N(m[1]), N(m[2])],
"작업반장": N("1"),
"보통인부원문": text,
}
)
return [
table(
"1-2-5",
"작업반장",
"작업반장 — 현장작업조건별 작업반장 수(참고)",
"2",
{"보통인부": "인", "작업반장": "인"},
rows,
cond={"현장작업조건": "고르기"},
base_="보통인부 인원 범위당 작업반장 1인",
extra={"비고": "원문 [참고] 표 · 다수 보통인부 지휘통제가 필요할 때 계상할 수 있음"},
대상=["인력"],
),
rule_table("1-2-5"),
]
def t_1_2_6() -> list[dict]:
_, lines = section("1-2-6")
one = next(x for x in lines if x.startswith("일반공구 및 시험용"))
assert "인력품(노임할증과 작업시간 증가에 의하지 않은 품 할증 제외)의 3%까지" in one
return [
table(
"1-2-6",
"공구손료",
"공구손료 — 명시 없는 공구손료 계상 한도",
"1",
{"인력품 대비": "%"},
[
{
"인력품 대비": N("3"),
"인력품 대비원문": "인력품(노임할증과 작업시간 증가에 의하지 않은 품 할증 제외)의 3%까지",
}
],
extra={"비고": "특수공구 · 검사용 특수계측기류 손료는 별도 계상 · 상한(「까지」)"},
대상=["기계"],
),
rule_table("1-2-6"),
]
# ── 1-2-8 · 1-2-9 ────────────────────────────────────────────────────
def t_1_2_8() -> list[dict]:
_, lines = section("1-2-8")
digits, sample = md_tables(lines)
heads = digits[0][1:]
rows = [{"일당시공량": h, "소수자리": N(v)} for h, v in zip(heads, digits[1][1:])]
sizes = [re.search(r"\(([\d,]+)㎡\)", h)[1] for h in sample[1][3:8]]
ex, kind = [], ""
for r in sample[2:6]:
kind = r[0] or kind
for h, size, v in zip(heads, sizes, r[3:8]):
ex.append(
{
"구분": kind,
"단위": r[1],
"수량": N(r[2]),
"일당시공량": h,
"예시 시공량": N(size.replace(",", "")),
"품": N(v),
}
)
calc = [_plain(r[0]) for r in sample[6:8]]
assert calc[0].startswith("※ 인력품") and "8(hr)" in calc[1]
return [
table(
"1-2-8",
"시공단위 품 소수자리",
"시공단위 품 산정 — 일당시공량 단위별 소수자리",
"3 가",
{"소수자리": "자리"},
rows,
cond={"일당시공량": "고르기"},
extra={
"비고": "품의 규격 · 단위수량을 고려해 소수자리 정도를 조정할 수 있음(원문 「참고하여」)"
},
),
table(
"1-2-8",
"시공단위 품 산정 식",
"시공단위 품 산정 식 — 인력품 · 장비품",
"3 가",
{"식": "식", "단위": "글"},
[
{"구분": "인력", "식": "인력(인) ÷ 시공량(일당)", "단위": "인"},
{"구분": "장비", "식": "장비(대) × 8(hr) ÷ 시공량(일당)", "단위": "hr"},
],
cond={"구분": "고르기"},
note=calc,
extra={"비고": "모든 로직의 품 환산 공통 규칙 — 일당시공량 기준 품을 시공단위 품으로"},
),
table(
"1-2-8",
"시공단위 품 표기 예시",
"시공단위 품 — 소수자리 표기 예시(참고)",
"3 가 [참고]",
{"예시 시공량": "㎡", "품": "인 · hr"},
ex,
cond={"구분": "고르기", "수량": "수", "일당시공량": "고르기"},
extra={"비고": "원문 [참고] 표를 줄로 폄 — 예시 시공량 = 원문 머리 괄호(㎡)"},
),
rule_table("1-2-8"),
]
def t_1_2_9() -> list[dict]:
_, lines = section("1-2-9")
small, sample = md_tables(lines)
rows = [{"구분": N(a), "적용조건": b, "적용시공량": c} for a, b, c in small[1:]]
note = notes(lines)
ex, group, case = [], "", ""
for r in sample[1:]:
group = r[0].replace("\n", " ") or group
case = r[1] or case
row = {"적용": group, "예시": case, "규격": r[2], "현장수량": N(r[3])}
for col, v in (("일당시공량", r[4]), ("적용시공량", r[5]), ("소요일수", r[6])):
if v not in ("", "-"):
row[col] = N(v)
if r[7] not in ("", "-"):
row["일수편차"], row["일수편차원문"] = num(r[7].split("\n")[0]), r[7].replace("\n", "")
ex.append(row)
return [
table(
"1-2-9",
"소규모 적용시공량",
"소규모(작업물량 제한) — 적용시공량",
"",
{"적용조건": "글", "적용시공량": "식"},
rows,
cond={"구분": "수"},
note=note,
base_="A 시공량(총 시공량) · B 1일시공량(표준품셈) · Q 적용시공량",
extra={
"비고": "「시공량/일」 명시 항목 · 재료량 제외 · A ≧ B = 미적용 · 로직(적용 배)은 PLAN 783-1 프로젝트 조정(B09)",
"성격": SMALL,
},
대상=["작업량 조건", "할증"],
),
table(
"1-2-9",
"다수 규격 예시",
"소규모(작업물량 제한) — 동일 항목 다수 규격 예시",
"주③",
{
"현장수량": "m",
"일당시공량": "m",
"적용시공량": "m",
"소요일수": "일",
"일수편차": "",
},
ex,
cond={"적용": "고르기", "예시": "고르기", "규격": "고르기"},
extra={
"비고": "원문 예시 표를 줄로 폄 · 일수편차 = 예시 첫 줄에만(원문 병합 칸) · 계 줄 = 원문 그대로",
"성격": SMALL,
},
대상=["작업량 조건", "할증"],
),
]
# ── 1-3-2 · 1-4-1 · 1-4-2 ────────────────────────────────────────────
def t_1_3_4() -> list[dict]:
return [
rule_table("1-3-2"),
rule_table("1-4-1"),
rule_table(
"1-4-2",
{
"비고": "W = 기본품 × (1 + a1 + … + an) · 합산(곱 아님) · 동일 성격 이중 적용 불가 — 성격 = 계수 표 「성격」 칸 · 로직(할증 배)은 PLAN 783-1 프로젝트 조정(B09)",
},
),
]
# ── 1-5 기타 ─────────────────────────────────────────────────────────
WASTE = ("폐콘크리트류", "폐금속류", "폐보드류", "폐목재류", "폐합성수지류", "혼합폐기물")
WATER_UNIT = "단위 칸"
def t_1_5() -> list[dict]:
out = []
for n in range(1, 11):
no = f"1-5-{n}"
if n == 4:
out += t_1_5_4()
elif n == 6:
out += t_1_5_6()
out.append(rule_table(no))
return out
def t_1_5_4() -> list[dict]:
_, lines = section("1-5-4")
(md,) = md_tables(lines)
assert [h.replace("\n", "") for h in md[0][3:]] == list(WASTE)
rows, work, use = [], "", ""
for r in md[1:]:
work, use = r[0] or work, r[1].replace("\n", "") or use
for kind, v in zip(WASTE, r[3:]):
if num(v) is not None:
rows.append(
{"공사": work, "용도": use, "구조": r[2], "폐기물": kind, "발생량": num(v)}
)
return [
table(
"1-5-4",
"건설폐기물 발생량",
"환경관리비 — 건설폐기물 단위면적당 발생량",
"2",
{"발생량": "TON/㎡"},
rows,
cond={"공사": "고르기", "용도": "고르기", "구조": "고르기", "폐기물": "고르기"},
note=notes(lines),
base_="㎡당",
extra={
"비고": "원문 칸 「-」 = 줄 없음 · 건축물 해체는 설계도서 산출 우선(원문 2) · 주③ ④ = 폐콘크리트 수량 증가(이내)"
},
)
]
def t_1_5_6() -> list[dict]:
_, lines = section("1-5-6")
(md,) = md_tables(lines)
rows = []
for kind, unit, v in md[1:]:
if re.fullmatch(r"[\d.]+", v):
rows.append({"구분": kind, "단위": unit, "수량": N(v)})
else:
m = re.search(r"(\d+)∼(\d+)%", v)
rows.append({"구분": kind, "단위": unit, "수량": [N(m[1]), N(m[2])], "수량원문": v})
return [
table(
"1-5-6",
"공사용수",
"사용료 — 공사용수 소요량",
"3",
{"수량": WATER_UNIT},
rows,
cond={"구분": "고르기"},
note=notes(lines),
extra={
"비고": "줄마다 단위 칸 · 잡용수 = 원문 「사용량비의 40∼50%」(범위 [아래, 위] %)"
},
)
]
def all_tables() -> list[dict]:
"""원문 절 차례 — 같은 절은 만든 차례."""
return [
*t_1_2_2(),
*t_1_2_3(),
*t_1_2_4(),
*t_1_2_5(),
*t_1_2_6(),
*t_1_2_8(),
*t_1_2_9(),
*t_1_3_4(),
*t_1_5(),
]
@@ -2,6 +2,7 @@
"""건설품셈 공통 1-2-7 운반 — 계수 표 여덟(계수_건설품셈_01장_적용기준.json 에 덧) + 로직 셋(새 파일).
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_로직_건설품셈_01장_적용기준.py
- `… 나머지` = 782-2 1장 나머지(`_적용기준_나머지_표.py` · `_적용기준_나머지_로직.py`).
- 원문 = 건설품셈 공통 `1-02_설계_및_수량.md` 1-2-7 운반(머리 ~ 1-2-8 앞).
- 계수 표는 1-2-4 표 뒤 · 1-3-1 표 앞에 글로 끼움(기존 표 글자 그대로) · 화물자동차 적재량은 본문 md 표에서 읽음.
- 로직 파일 `로직_건설품셈_01장_적용기준.json` 은 통째로 씀 · 차례 13(같은 차례 공통 2장보다 이름이 앞).
@@ -616,4 +617,9 @@ def main() -> None:
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
main()
if sys.argv[1:] == ["나머지"]: # 782-2 1장 나머지 — 규칙 · 계수만 절 표 · 1-5-4 · 1-5-6 로직
import _적용기준_나머지_로직
_적용기준_나머지_로직.run()
else:
main()
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
"""마스터 데이터 검사 도구 (`resources/master_data/_틀.md`).
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/check_master.py [틀|본문|로직|조합] [파일 …]
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/check_master.py [틀|본문|로직|조합|목차] [파일 …]
./venv/Scripts/python.exe resources/master_data/scripts/check_master.py 계산 GF000123 뒷길이=35 돌=견치돌 …
(1) 틀 — 파일 이름 · 머리 칸 · 줄 칸 · 출처 모양 · 키 모양·겹침 · 대장
(2) 본문(`check_master_body.py`) — 표형 요소의 출처 절을 본문 md 에서 찾아 수를 맞댐 · 결손(본문 표에 있고 요소에 없음)·허구(요소에 있고 본문 절에 없음)
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 재료 고르기 조건 · 돌고 도는 참조 · 끊긴 키
(4) 조합 — 일위대가 조합이 담은 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음 (`master_combo.py`)
(목차) 건설품셈 공통 1장 원문 절 목록 ↔ 마스터 전수 — 통째 빠진 절 · 로직 없는 품·표·식 절(`check_master_toc.py`)
(배) 로직 결과단위 배 ↔ 부르는 표 기준배(10㎡당 표를 원/㎡ 로직이 「/ 10」 없이 씀) (`master_basis.py`)
(5) 계산 — 로직 키와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
수 뽑기는 `resources/knowledge/original/_pipeline/pum_md_tool.py` 의 것을 씀.
@@ -35,6 +36,7 @@ import master_keys as mk # noqa: E402
# (2) 본문은 따로 모듈 — 부르는 쪽은 `cm.check_body` · `cm.DIVISIONS` 그대로
from check_master_body import DIVISIONS, check_body # noqa: E402, F401
from check_master_toc import check_toc # noqa: E402
BOOKS = (
"산림품셈",
@@ -562,6 +564,8 @@ def main(argv: list[str]) -> int:
report["조합"] = mcb.check_all(files, whole)
if mode in ("단위", "전부"):
report["단위"] = check_units(files, whole)
if mode in ("목차", "전부") and "계수_건설품셈_01장_적용기준.json" in files:
report["목차"] = check_toc(files) # 공통 1장 원문 절 목록 ↔ 마스터 전수
if mode in ("배", "전부"): # 결과단위 배 ↔ 부르는 표 기준배(읽을 때 파생 · `master_basis`)
report["배"] = mbs.check_times(files)
for title, found in report.items():
@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""마스터 데이터 검사 — 원문 절 목록 ↔ 마스터 전수 대조(건설품셈 공통 1장) · `check_master.py` 가 부름.
- 절 = 원문 `###` 머리 전부 · 범위 = 머리부터 다음 같은 · 윗 단계 머리 앞까지(요구 630).
- 가이드(1-1 · 1-2-1) 빼고 계산 대상 절마다 마스터 키(계수 · 로직)가 하나 이상 — 없으면 「통째 빠짐」.
- 품 · 표 · 식 절(_대조_632 판정)은 로직이 있어야 함 · 1-2-9 · 1-4-2 = PLAN 783-1 로 미룸 표지.
"""
from __future__ import annotations
import re
from pathlib import Path
HERE = Path(__file__).resolve().parent
CH1 = (
HERE.parents[2]
/ "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제01장_적용기준"
)
WHO = ("건설품셈 공통", "01장 적용기준")
GUIDE = ("1-1-", "1-2-1") # 사용자 판정(요구 632) — 가이드
CALC = ("1-2-7", "1-2-9", "1-4-2", "1-5-4", "1-5-6") # 품 · 표 · 식(_대조_632 2장)
DEFER = {"1-2-9": "783-1", "1-4-2": "783-1"} # 로직 = 프로젝트 조정(B09) 승인 뒤
def sections(folder: Path = CH1) -> list[tuple[str, str]]:
"""(절 번호, 이름) — 원문 차례."""
out = []
for md in sorted(folder.glob("1-*.md")):
for line in md.read_text(encoding="utf-8").split("\n"):
m = re.match(r"^### (\d+-\d+-\d+) (.+)$", line)
if m:
out.append((m[1], re.sub(r"\s*\(['‘].*$", "", m[2]).strip()))
return out
def _guide(no: str) -> bool:
return no.startswith(GUIDE[0]) or no == GUIDE[1]
def table(files: dict[str, dict], folder: Path = CH1) -> list[dict]:
"""절마다 {절 · 이름 · 쓰임 · 판정 · 계수 · 로직 · 표지}."""
keys: dict[str, dict[str, list[str]]] = {}
for data in files.values():
group = data.get("그룹")
for row in data.get("표") or data.get("줄") or []:
if (row.get("구분"), row.get("상세구분")) != WHO:
continue
kind = "로직" if group == "로직" else "계수"
keys.setdefault(str(row.get("원문번호")), {}).setdefault(kind, []).append(row["키"])
out = []
for no, name in sections(folder):
got = keys.pop(no, {})
out.append(
{
"절": no,
"이름": name,
"쓰임": "가이드" if _guide(no) else "계산 대상",
"판정": "품·표·식" if no in CALC else "규칙·계수만",
"계수": got.get("계수", []),
"로직": got.get("로직", []),
"표지": f"{DEFER[no]} 로 미룸" if no in DEFER else "",
}
)
for no, got in keys.items(): # 원문 목차에 없는 원문번호
out.append(
{
"절": no,
"이름": "",
"쓰임": "목차 밖",
"계수": got.get("계수", []),
"로직": got.get("로직", []),
}
)
return out
def check_toc(files: dict[str, dict], folder: Path = CH1) -> list[str]:
found = []
for r in table(files, folder):
where = f"공통 1장 {r['절']} {r['이름']}"
if r["쓰임"] == "목차 밖":
found.append(f"{where} · 원문 목차에 없는 원문번호 ← {r['계수'] + r['로직']}")
elif r["쓰임"] == "계산 대상" and not r["계수"] and not r["로직"]:
found.append(f"{where} · 통째 빠짐 — 계수 · 로직 키 없음")
elif (
r["쓰임"] == "계산 대상" and r["판정"] == "품·표·식" and not r["로직"] and not r["표지"]
):
found.append(f"{where} · 품·표·식 절인데 로직 없음")
return found