Merge branch 'main_laptop_1' of C:/Program_coding/임도설계 및 견적자동화 프로그램 개발 into sub_laptop_3

This commit is contained in:
2026-11-07 18:42:02 +09:00
37 changed files with 816 additions and 183 deletions
+5 -5
View File
@@ -59,8 +59,8 @@ def fill(copy: dict, whole: Any) -> tuple[dict, int, int]:
return {**copy, "로직": rows, "판": skc._fp(rows)}, done, lost
def check(kind: str, path: Path) -> dict[str, Any]:
"""문서 하나 — {상태: 있음 | 채울것없음 | 같음 | 다름 | 못셈, 채움, 못닿음, 사본?}."""
def check(kind: str, path: Path, fill_fn=None) -> dict[str, Any]:
"""문서 하나 — {상태: 있음 | 채울것없음 | 같음 | 다름 | 못셈, 채움, 못닿음, 사본?} · `fill_fn` = 다른 고침(801-5 단위 글)."""
from M02_MasterTemplete import M02_MasterTemplete_Store as store
doc = json.loads(path.read_bytes())
@@ -69,7 +69,7 @@ def check(kind: str, path: Path) -> dict[str, Any]:
try:
with store.at(path.parent.parent):
live = skc.whole_()
new, done, lost = fill(doc[skc.KEY], live)
new, done, lost = (fill_fn or fill)(doc[skc.KEY], live)
if not done:
return {"상태": "채울것없음", "못닿음": lost}
clean = mig._clean(kind, doc, path.stem)
@@ -81,7 +81,7 @@ def check(kind: str, path: Path) -> dict[str, Any]:
return {**row, "사본": new} if before == after else {**row, "전": before, "후": after}
def run(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]:
def run(apply: bool = False, backup: Path | None = None, fill_fn=None) -> list[dict[str, Any]]:
"""마스터 · 회사 · 개인 층 문서마다 {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음만 씀."""
if apply and backup is None:
raise ValueError("--apply 는 --backup 폴더가 있어야 함")
@@ -89,7 +89,7 @@ def run(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]
for where_, kind, path, project in mig._files():
if project:
continue
got = check(kind, path)
got = check(kind, path, fill_fn)
copy = got.pop("사본", None)
row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False}
if apply and copy:
+55
View File
@@ -0,0 +1,55 @@
"""옛 로직 사본 단위 글 새로 쓰기 — 한 번 돌리는 도구(PLAN 801-5 · 요구 663-2).
M01 로직 단위 글을 건설품셈 원문 꼴(`master_unit_text`)로 바꾼 뒤, 사본 줄의 입력 · 중간 `단위` · `결과단위` 를
같은 규칙으로 바꿈(`M02_Copy_Symbols` 와 같은 길 — 금액 전후 같을 때만 씀 · 쓰기 전 `--backup` 사본 ·
사본 `판` 다시 셈 · 프로젝트 층 문서는 안 건드림).
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Units # 보기만
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Units --apply --backup tmp/801_5
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Copy_Symbols as cs
from M02_MasterTemplete import M02_Sangeun_Copy as skc
def fill(copy: dict, whole: Any) -> tuple[dict, int, int]:
"""(새 사본, 바꾼 칸 수, 0) — 줄 단위 글만 고치고 `판` 다시 셈."""
import master_unit_text as ut # M01 이 길을 둔 뒤
done, rows = 0, []
for row in copy["로직"]:
new = {**row}
for part in ("입력", "중간"):
specs = []
for spec in row.get(part) or []:
now = ut.canon(spec.get("단위"))
done += now != (spec.get("단위") or "")
specs.append({**spec, "단위": now} if now != (spec.get("단위") or "") else spec)
if row.get(part):
new[part] = specs
if row.get("결과단위"):
now = ut.canon(row["결과단위"])
done += now != row["결과단위"]
new["결과단위"] = now
rows.append(new)
return {**copy, "로직": rows, "판": skc._fp(rows)}, done, 0
if __name__ == "__main__": # 브레인 지시 뒤만 --apply
args = sys.argv[1:]
at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None
rows = cs.run("--apply" in args, at, fill)
for one in rows:
if one["상태"] not in ("사본없음", "채울것없음", "같음"):
print(json.dumps(one, ensure_ascii=False, default=str))
count: dict[str, int] = {}
for one in rows:
count[one["상태"]] = count.get(one["상태"], 0) + 1
print(json.dumps(count, ensure_ascii=False), "바꿈", sum(r.get("채움", 0) for r in rows))
@@ -4,10 +4,10 @@
(`M02_Sangeun_Calc.calc_doc` = 화면 · 저장 · B09 가 쓰는 한 벌)을 얹어 화면 금액 = SK 금액
(셈은 안 바꿈 · 보이기만):
합계 = 셈 답 `기준`(노 · 재 · 경 · 계 · 1 단위) — 바탕 줄만 셈한 합계 대신
소계 = 셈 답 `줄` 한 줄마다 한 행 {cid, 갈래(자원 | 블록 | 덧줄 | 율), 이름, 상태, 수량,
노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?, 합밖?, 밖금액?} +
끝수(합계 − 행 합 · 까닭 글)
합밖 줄(엔진이 합계에서 뺌) = 행 금액 0 + 밖금액
소계 = 셈 답 `줄` → 돈 묶음(801-3 · `master_equation_group`) — 더한 · 부른 단가산출(갈래 블록) 하나씩
{cid, 이름, 상태, 수량, 노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?} + 직접 줄(인력 · 재료 · 기계 ·
덧줄 · 율) 한 묶음 {갈래 직접, 이름, 노 · 재 · 경 · 계, 낱줄?(더함 · 덮음 줄)} +
끝수(합계 − 묶음 합 · 까닭 글) · 밖(합계 밖 줄 — 엔진이 합계에서 뺌 · 이름 · 밖금액)
상태 = 바탕(그대로) · 덮음(품 · 단가 · 상수 · 표값 · 식 덮개 · 변수줄) ·
더함(더한줄) · 뺌(뺀줄 · 금액 0)
나무 = 더한 기초(더한줄 로직)의 수식(세부 덮개 · 뺀줄 얹은 줄) — 화면은 접힘 블록
@@ -31,6 +31,7 @@ SUMS = (*COSTS, "계")
_ADD = re.compile(r"^add:#(\d+)$")
_HEAD = {"labor": "자원", "material": "자원", "machine": "자원", "logic": "블록", "pct": "덧줄"}
_ADD_KIND = {"인력": "자원", "재료": "자원", "기계": "자원"}
_WORD = {"labor": "인력", "material": "재료", "machine": "기계"} # 직접 묶음 이름(801-3)
def _d(value: Any) -> Decimal:
@@ -69,6 +70,15 @@ def _kind(cid: str, adds: list) -> str:
return _ADD_KIND.get(str(one.get("종류")), "블록")
def _word(cid: str, adds: list) -> str:
"""직접 줄의 자원 갈래 말 — 인력 · 재료 · 기계(그 밖 = 빈 글)."""
hit = _ADD.match(cid)
if not hit:
return _WORD.get(cid.split(":", 1)[0], "")
one = adds[int(hit[1])] if int(hit[1]) < len(adds) else {}
return str(one.get("종류")) if one.get("종류") in _ADD_KIND else ""
def _row(cid: str, kind: str, name: str, state: str, parts: dict | None, **more: Any) -> dict:
cost = {k: _d((parts or {}).get(k)) for k in COSTS}
return {
@@ -112,6 +122,7 @@ def rows(doc: dict, ans: dict, tree: Any, out_ids: set[str] = frozenset()) -> li
if outside
else None,
수량=line.get("수량"),
종류=_word(cid, adds),
덮개=cover,
원래=info.get("원래") if state == "덮음" else None,
까닭=line.get("까닭") or info.get("까닭"),
@@ -209,6 +220,8 @@ def attach(
eq: dict, doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None
) -> dict:
"""수식 `eq` 에 SK 셈 답 합계 · 소계 · 표지를 얹음(제자리) — 답 없음 = `답까닭` 만."""
import master_equation_group as grp # M01 이 길을 둔 뒤
if ans is None:
return eq | {"답까닭": "단가산출 셈 답이 서지 않음 — 바탕 줄 합계를 보임"}
_mark(eq, base, ans, doc)
@@ -219,8 +232,9 @@ def attach(
lambda k: _basic_tree(whole, doc, adds[k], inputs) if k >= 0 else None,
_outside(base),
)
got = grp.bundles(table) # 801-3 묶음 — 부른 · 더한 단가산출 하나씩 + 직접 줄 한 묶음
now = total(ans) or {}
rest = {k: now[k] - sum((r[k] for r in table), Decimal(0)) for k in SUMS}
rest = {k: now[k] - sum((r[k] for r in got["줄"]), Decimal(0)) for k in SUMS}
eq["합계"] = now
eq["소계"] = {"줄": table, "끝수": rest, "끝수까닭": _why(ans)}
eq["소계"] = {"줄": got["줄"], "밖": got["밖"], "끝수": rest, "끝수까닭": _why(ans)}
return eq
Binary file not shown.
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
"""M01 로직 단위 글을 건설품셈 원문 꼴로 (PLAN 801-5 · 요구 663-2) — 글만 바꿈 · 셈 지문 그대로.
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_단위표기_801_5.py [--check | --list 파일.xlsx]
- 대상 = M01 로직 줄의 입력 · 중간 변수 `단위` · `결과단위` — 규칙 · 근거 절 = `master_unit_text.RULES`.
- 계수 · 소요량 표 값칸 · 호표 줄 단위 · 사용자가 이미 원문 꼴로 쓴 칸은 안 건드림(이미 같으면 그대로 — 두 번 돌려도 같음).
- 쓰기 전 지문 대조 = 줄마다 엔진 셈 · 끝수 지문이 전후 같을 때만 씀(다르면 멈춤).
- `--check` = 쓰지 않고 쓸 것이 남았는지만(남았으면 종료 코드 1) · `--list` = 검토 목록 xlsx 만 씀(마스터 안 씀).
"""
from __future__ import annotations
import copy
import hashlib
import json
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[2]))
import master_auto as ma # noqa: E402
import master_cut as mcut # noqa: E402
import master_formula as mf # noqa: E402
import master_unit_text as ut # noqa: E402
from M01_MasterData import M01_MasterData_Store as st # noqa: E402
def plan(files: dict[str, dict]) -> tuple[list[dict], list[dict], dict[str, dict]]:
"""(저장 묶음, 목록 줄들, 새 files) — 읽기 전용."""
batch, rows, new_files = [], [], {}
for name, data in files.items():
if data.get("그룹") != "로직":
new_files[name] = data
continue
changes, new_data = [], copy.deepcopy(data)
for row in new_data.get("줄", []):
hit = False
for kind in ("입력", "중간"):
for spec in row.get(kind) or []:
now = ut.canon(spec.get("단위"))
if now != (spec.get("단위") or ""):
rows.append(_item(row, kind, spec["이름"], spec["단위"], now))
spec["단위"], hit = now, True
if row.get("결과단위"):
now = ut.canon(row["결과단위"])
if now != row["결과단위"]:
rows.append(_item(row, "결과단위", "", row["결과단위"], now))
row["결과단위"], hit = now, True
if hit:
changes.append({"op": "edit", "key": row["키"], "row": row})
new_files[name] = new_data
if changes:
batch.append({"file": name, "version": st.read(name)[1], "changes": changes})
return batch, rows, new_files
def _item(row: dict, kind: str, var: str, before: str, after: str) -> dict:
return {
"로직": row["키"],
"이름": row.get("이름", ""),
"칸": kind,
"변수": var,
"지금": before,
"원문 꼴": after,
"규칙": ut.rule_of(before),
}
def fingerprint(whole: mf.Master, row: dict) -> str:
"""줄 하나의 엔진 셈 · 끝수 지문 — 멈춤도 지문."""
try:
key = str(row["키"])
raw = mf.run(
whole,
key,
{
**ma.values(whole, row),
**{k: v for k, v in ma.defaults(whole, row).items() if v is not None},
},
)
text = json.dumps(raw, default=str, sort_keys=True, ensure_ascii=False)
text += json.dumps(
mcut.engine(whole, key, raw), default=str, sort_keys=True, ensure_ascii=False
)
except Exception as e: # noqa: BLE001
text = f"ERR {type(e).__name__} {e}"
return hashlib.sha256(text.encode()).hexdigest()[:16]
def differing(old: dict[str, dict], new: dict[str, dict]) -> list[str]:
"""지문이 바뀐 로직 키들 — 고친 줄만 대조."""
before, after = mf.Master(old), mf.Master(new)
out = []
for name, data in new.items():
if data.get("그룹") != "로직":
continue
was = {r["키"]: r for r in old[name]["줄"]}
for row in data["줄"]:
if row != was[row["키"]] and fingerprint(before, was[row["키"]]) != fingerprint(
after, row
):
out.append(row["키"])
return out
def write_list(rows: list[dict], path: Path) -> None:
import openpyxl
book = openpyxl.Workbook()
sheet = book.active
sheet.title = "바뀜"
cols = ["로직", "이름", "칸", "변수", "지금", "원문 꼴", "규칙"]
sheet.append(cols)
for r in rows:
sheet.append([r[c] for c in cols])
rule = book.create_sheet("대응표")
rule.append(["규칙", "바꿀 글", "근거 절(건설공사 표준품셈 본문 아래)", "바뀐 칸 수"])
for name, _, to, _, files in ut.RULES:
rule.append([name, to, " · ".join(files), sum(name in r["규칙"] for r in rows)])
path.parent.mkdir(parents=True, exist_ok=True)
book.save(path)
def main() -> None:
files, _ = st.loaded()
batch, rows, new_files = plan(files)
print(f"쓸 것 — 로직 {len({r['로직'] for r in rows})} · 칸 {len(rows)}")
if "--list" in sys.argv:
write_list(rows, Path(sys.argv[sys.argv.index("--list") + 1]))
return
if "--check" in sys.argv:
raise SystemExit(1 if batch else 0)
if batch:
bad = differing(files, new_files)
if bad:
raise SystemExit(f"셈 지문이 달라지는 로직 {len(bad)} — 멈춤: {bad[:10]}")
print(st.save(batch))
if __name__ == "__main__":
main()
@@ -28,6 +28,7 @@ from pathlib import Path
import master_auto as ma
import master_cut as mcut
import master_equation_group as grp
import master_equation_view as view
import master_formula as mf
import master_material as mm
@@ -401,36 +402,30 @@ def _line(ctx: _Ctx, kind: str, key: str, text: str, value, **more) -> dict:
def subtotals(logic: dict, cut: dict, total: dict) -> dict:
"""호표 줄 → 소계(799-1 · 보이기만) — 하위 로직 줄 = 블록마다 · 그 밖 자원 · 덧줄은 한 줄씩 + 끝수.
"""호표 줄 → 소계 묶음(799-1 · 801-3 · 보이기만) — 부른 단가산출 = 묶음 하나씩 · 직접 줄 = 한 묶음 + 끝수.
값 = `master_cut.engine` 이 버린 줄 비목(0.1 원) 그대로 · 합계 = 엔진 비목 합(1 원 버림) ·
끝수 = 합계 − 줄 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함).
끝수 = 합계 − 묶음 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함). 묶음 꼴 = `master_equation_group`.
"""
items, adds = logic.get("호표") or [], logic.get("덧줄") or []
items = logic.get("호표") or []
skip = mcut.ms.skipped(logic) if mcut.ms.uses(logic) else set()
rows: list[dict] = []
pool: dict[str, dict] = {}
for n, line in enumerate(cut.get("줄") or []):
if n in skip:
continue
split = {k: line.get("비목", {}).get(k) or Decimal(0) for k in mf.COST_ITEMS}
if n < len(items) and items[n].get("종류") == "로직":
rows.append({"갈래": "블록", "이름": str(items[n].get("이름") or ""), **split})
continue
kind = "덧줄" if n >= len(items) else "자원"
one = pool.setdefault(
kind, {"갈래": kind, "이름": kind, **dict.fromkeys(mf.COST_ITEMS, Decimal(0))}
item = items[n] if n < len(items) else {}
kind = "블록" if item.get("종류") == "로직" else "자원" if item else "덧줄"
rows.append(
{"갈래": kind, "이름": str(item.get("이름") or ""), "종류": item.get("종류"), **split}
| {"계": sum(split.values(), Decimal(0))}
)
for k in mf.COST_ITEMS:
one[k] += split[k]
rows += [pool[k] for k in ("자원", "덧줄") if k in pool]
for one in rows:
one["계"] = sum((one[k] for k in mf.COST_ITEMS), Decimal(0))
got = grp.bundles(rows)
rest = {
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in rows), Decimal(0))
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in got["줄"]), Decimal(0))
for k in mf.BUNDLE
}
return {"줄": rows, "끝수": rest}
return {"줄": got["줄"], "끝수": rest}
def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
@@ -0,0 +1,68 @@
"""수식 탭 소계 묶음(PLAN 801-3 · 요구 662-3) — 돈 묶음 한 곳 · M01(`master_equation`) · M02(`M02_Sangeun_Equation_Full`) 같은 꼴.
묶음 = 부른 · 더한 단가산출(갈래 블록) 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄 · 율) 전부 한 묶음(갈래 직접).
돈 없는 줄(계 0 · 바탕) · 뺀 줄 = 묶음 아님(식 줄로만 보임) · 합계 밖 줄 = `밖` 으로만.
머리 · 소계 · 합계 · 계 글자 판정 = 화면 부품(`ui_template_equation_sum.ts`) — 묶음 둘 이상 = 머리 · 소계 ·
단가산출(블록) 둘 이상 = 합계 · 아니면 계.
시험 `resources/tester/test_801_3_groups.py`.
"""
from __future__ import annotations
from decimal import Decimal
COSTS = ("노무비", "재료비", "경비")
_WORDS = ("인력", "재료", "기계") # 직접 묶음 이름 차례
def _zero(row: dict) -> bool:
"""돈 없는 바탕 줄 · 뺀 줄 — 묶음 아님."""
if row.get("상태") == "뺌":
return True
return row["계"] == 0 and row.get("상태") in (None, "바탕")
def _name(direct: list[dict]) -> str:
"""직접 묶음 이름 — 자원 갈래 말(인력 · 재료 · 기계) · 율 줄 = 이름 (율 %) · 덧줄뿐 = 비목 말."""
words = [w for w in _WORDS if any(r.get("종류") == w for r in direct)]
words += [
f"{r['이름']} ({r['수량']}%)" if r.get("수량") not in (None, "") else str(r["이름"])
for r in direct
if r["갈래"] == "율"
]
if not words:
words = [k for k in COSTS if any(r[k] != 0 for r in direct)]
return " · ".join(words)
def bundles(rows: list[dict]) -> dict:
"""줄(갈래 블록 | 자원 | 덧줄 | 율 · 비목 칸 · 계) → `{줄: 묶음들, 밖: 합계 밖 줄들}` — 금액은 더하기만(셈 안 바꿈)."""
blocks: list[dict] = []
direct: list[dict] = []
out: list[dict] = []
for row in rows:
if row.get("합밖"):
out.append({"이름": row["이름"], "밖금액": row.get("밖금액")})
elif _zero(row):
continue
elif row["갈래"] == "블록":
blocks.append(row)
else:
direct.append(row)
lines = list(blocks)
if direct:
one = {
"갈래": "직접",
"이름": _name(direct),
**{k: sum((r[k] for r in direct), Decimal(0)) for k in COSTS},
}
one["계"] = sum((one[k] for k in COSTS), Decimal(0))
mine = [ # 더한 · 덮은 줄 — 묶음 속에 한 줄씩(고친 줄을 숨기지 않음)
{k: r[k] for k in ("이름", "상태", "계", "원래") if r.get(k) is not None}
for r in direct
if r.get("상태") in ("더함", "덮음")
]
if mine:
one["낱줄"] = mine
lines.append(one)
return {"줄": lines, "밖": out}
@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""M01 로직 단위 글 → 건설품셈 원문 꼴(PLAN 801-5 · 요구 663-2) — 글만 바꿈 · 셈 · 환산 안 바꿈.
`canon(단위 글)` = 아래 `RULES` 를 차례로 적용(이미 원문 꼴이면 그대로 · 두 번 돌려도 같음).
`RULES` 한 줄 = (이름, 지금 글 정규식, 바꿀 글, 원문 글, 근거 절들) — 근거 절 = `EVIDENCE_ROOT` 아래 파일 ·
그 파일 안에 원문 글이 실제로 있어야 함(시험 `test_801_5_units`). 원문이 두 꼴을 다 쓰는 단위(㎞ · km · 분 · min)는 안 건드림.
단위 칸 중 원문 표를 그대로 옮긴 계수 · 소요량 표 값칸 · 호표 줄 단위(요소 단위)는 대상 아님.
"""
from __future__ import annotations
import re
from pathlib import Path
EVIDENCE_ROOT = Path("resources/knowledge/original/원가계산/건설공사_표준품셈/본문")
_A = r"(?<![A-Za-z])" # 영문 낱말 한가운데는 안 건드림
_Z = r"(?![A-Za-z])"
# (이름, 지금 글 정규식, 바꿀 글, 원문 글, 근거 절들)
RULES: list[tuple[str, str, str, str, list[str]]] = [
(
"시간당 ㎥ · ㎡ → hr",
r"(?<=[㎥㎡])/시간(?![가-힣])",
"/hr",
"/hr",
[
"01_공통부문/제08장_건설기계/8-03-05_[40]콘크리트기계.md", # ㎥/hr
"02_토목부문/제07장_항만공사/7-02_사석.md", # ㎡/hr
],
),
(
"사이클 시간 초 → sec",
r"(?<![가-힣])초(?![가-힣])",
"sec",
"sec",
[
"01_공통부문/제05장_기초공사/5-02_연약지반처리.md", # cm : 1회 싸이클 타임(sec)
"01_공통부문/제08장_건설기계/8-02-02_리퍼(유압식).md",
],
),
(
"mm → ㎜",
rf"{_A}mm{_Z}",
"㎜",
"㎜",
["04_기계설비부문/제13장_플랜트설비공사/13-01_플랜트_배관.md"],
),
(
"cm → ㎝",
rf"{_A}cm{_Z}",
"㎝",
"㎝",
["01_공통부문/제08장_건설기계/8-03-06_[50]골재생산기계_등.md"],
),
("kg → ㎏", rf"{_A}kg{_Z}", "㎏", "㎏", ["01_공통부문/제01장_적용기준/1-02_설계_및_수량.md"]),
(
"km/hr → ㎞/hr",
rf"{_A}km/hr",
"㎞/hr",
"㎞/hr",
["01_공통부문/제08장_건설기계/8-02-09_롤러.md"],
),
(
"TON · t → ton",
rf"{_A}(?:TON|t){_Z}",
"ton",
"ton",
["01_공통부문/제08장_건설기계/8-02-12_크러셔.md"],
),
]
_COMPILED = [(name, re.compile(pat), to, tok, files) for name, pat, to, tok, files in RULES]
def canon(unit) -> str:
"""원문 꼴 단위 글 — 규칙에 안 걸리면 그대로."""
text = str(unit or "")
for _, pat, to, _, _ in _COMPILED:
text = pat.sub(to, text)
return text
def rule_of(before: str) -> str:
"""바뀐 단위가 걸린 규칙 이름들(검토 목록 · 대응표용)."""
out, text = [], str(before or "")
for name, pat, to, _, _ in _COMPILED:
if pat.search(text):
out.append(name)
text = pat.sub(to, text)
return " · ".join(out)
@@ -217,7 +217,7 @@
{ "이름": "인원", "식": "무게 / 운반량", "단위": "인" },
{ "이름": "환산계수", "식": "만약(경사각도 > 0, 찾기(CC000347, 각도=경사각도).환산계수, 1)" },
{ "이름": "환산거리", "식": "환산계수 * 운반거리", "단위": "km" },
{ "이름": "왕복평균속도", "식": "찾기(CC000345, 도로상태=도로상태).왕복평균속도", "단위": "km/hr" },
{ "이름": "왕복평균속도", "식": "찾기(CC000345, 도로상태=도로상태).왕복평균속도", "단위": "㎞/hr" },
{ "이름": "준비작업시간", "식": "찾기(CC000346).준비작업시간", "단위": "분" },
{ "이름": "작업시간", "식": "만약(실작업시간 > 0, 실작업시간, 찾기(CC000342).실작업시간)", "단위": "분" }
],
@@ -280,7 +280,7 @@
"구분": "건설품셈 공통",
"상세구분": "01장 적용기준",
"이름": "건설폐기물 발생량 — 건설 공통 1-5-4",
"결과단위": "TON",
"결과단위": "ton",
"출처": "건설품셈 공통 1-5-4 2",
"개정": [2011, 2014, 2017, 2020],
"소유": "공용",
@@ -293,7 +293,7 @@
{ "이름": "면적", "설명": "단위면적 산출 면적", "단위": "㎡", "출처": "건설품셈 공통 1-5-4 2" },
{ "이름": "증가율", "설명": "폐콘크리트 수량 증가(%) — 주③ 파일 15 · 20% 이내 · 주④ 세륜 개소당 3% 이내 · 0 = 없음", "단위": "%", "출처": "건설품셈 공통 1-5-4 2 주③ ④" }
],
"중간": [{ "이름": "원단위", "식": "찾기(CC000519, 공사=공사, 구조=구조, 폐기물=폐기물).발생량", "단위": "TON/㎡" }, { "이름": "증가", "식": "만약(폐기물 == '폐콘크리트류', 증가율 / 100, 0)" }],
"중간": [{ "이름": "원단위", "식": "찾기(CC000519, 공사=공사, 구조=구조, 폐기물=폐기물).발생량", "단위": "ton/㎡" }, { "이름": "증가", "식": "만약(폐기물 == '폐콘크리트류', 증가율 / 100, 0)" }],
"결과": { "식": "면적 * 원단위 * (1 + 증가)", "출처": "건설품셈 공통 1-5-4 2" },
"끝수": null,
"변수표": {
@@ -12,13 +12,13 @@
"구분": "건설품셈 건축",
"상세구분": "01장 철골공사",
"이름": "철골 공장가공조립(기본철골공수)",
"결과단위": "원/t",
"결과단위": "원/ton",
"출처": "건설품셈 건축 1-1-1",
"개정": [2008, 2013],
"소유": "공용",
"비고": "공수에 비계 · 보조공 포함(주①) · 제경비에 산재보험료 · 간접노무비 · 일반관리비 · 이윤 없음(주③) · 용접은 1-1-3 · 부자재는 1-1-2(현장 세우기 제외) · 서비스볼트 규격은 설계에 따름 · 해석: 작업난이도(1-1-2)를 공수에 곱함",
"입력": [
{ "이름": "강재총사용량", "단위": "t", "출처": "건설품셈 건축 1-1-1" },
{ "이름": "강재총사용량", "단위": "ton", "출처": "건설품셈 건축 1-1-1" },
{ "이름": "부재", "고르기": ["전용접부재", "H형강부재"], "출처": "건설품셈 건축 1-1-1 비고", "비고": "전용접부재(Built up) · H형강부재(Rolled shape)" },
{ "이름": "작업난이도", "단위": "배", "범위": [0.8, 1.2], "출처": "건설품셈 건축 1-1-2", "비고": "구조공별 — 가공부재 종류 적은 구조 0.8∼0.95 · 표준라멘 1.0 · 종류 많은 구조 1.05∼1.2" },
{ "이름": "공수직종", "고르기": ["1011", "1012"], "출처": "건설품셈 건축 1-1-1", "비고": "원문 「인·일」에 직종 없음 — 설계자가 고름(1011 철골공 · 1012 용접공) · 공장제작 노임을 쓰면 관리자가 바꿈" }
@@ -81,13 +81,13 @@
"구분": "건설품셈 건축",
"상세구분": "01장 철골공사",
"이름": "철골 공장용접(기본용접공수)",
"결과단위": "원/t",
"결과단위": "원/ton",
"출처": "건설품셈 건축 1-1-3",
"소유": "공용",
"비고": "해석: 기본용접공수 = 용접공 · 1-1-4 보정계수(강재총사용량)를 공수에 곱함 · 용접재료는 환산용접길이(m/t) × m당 양 · 제경비에 산재보험료 · 간접노무비 · 일반관리비 · 이윤 없음(주③) · 특수 구조물은 기계설비부문 플랜트용접 참조(주⑤)",
"입력": [
{ "이름": "환산용접길이", "단위": "m/t", "출처": "건설품셈 건축 1-1-3 주④", "비고": "용접길이 × 환산계수(필릿 · V·K·X 판두께별 계수표) — 필릿 각장 6㎜ 환산" },
{ "이름": "강재총사용량", "단위": "t", "출처": "건설품셈 건축 1-1-4" },
{ "이름": "환산용접길이", "단위": "m/ton", "출처": "건설품셈 건축 1-1-3 주④", "비고": "용접길이 × 환산계수(필릿 · V·K·X 판두께별 계수표) — 필릿 각장 6㎜ 환산" },
{ "이름": "강재총사용량", "단위": "ton", "출처": "건설품셈 건축 1-1-4" },
{ "이름": "부재", "고르기": ["전용접부재", "H형강부재"], "출처": "건설품셈 건축 1-1-1 비고", "비고": "전용접부재(Built up) · H형강부재(Rolled shape)" },
{ "이름": "용접방식", "고르기": ["수용접", "반자동용접", "자동용접"], "출처": "건설품셈 건축 1-1-4", "비고": "반자동용접이 표준" }
],
@@ -348,7 +348,7 @@
"개정": [2008, 2018, 2023],
"소유": "공용",
"비고": "본조임 · 조임검사 포함 · 철골설계수량 300ton 이상은 보정계수 a를 곱함(주④)",
"입력": [{ "이름": "볼트본수본t", "단위": "본/t", "출처": "건설품셈 건축 1-2-4" }, { "이름": "강재총사용량", "단위": "t", "출처": "건설품셈 건축 1-2-4 주④" }],
"입력": [{ "이름": "볼트본수본t", "단위": "본/ton", "출처": "건설품셈 건축 1-2-4" }, { "이름": "강재총사용량", "단위": "ton", "출처": "건설품셈 건축 1-2-4 주④" }],
"중간": [
{ "이름": "보정a", "식": "만약(강재총사용량 < 300, 1, 찾기(CC000165, '강재총사용량(t)'=강재총사용량, '1t당 볼트본수(본)'=볼트본수본t).'보정계수 a')", "출처": "건설품셈 건축 1-2-4 주④" },
{ "이름": "철골공", "식": "찾기(QC001195, '1t당 볼트본수(본/t)'=볼트본수본t).철골공 * 보정a" },
@@ -1740,7 +1740,7 @@
"개정": [2018],
"소유": "공용",
"비고": "재료량 할증 포함 · 그물망 손율 100% · 보강재 손율 원문 없음 — 재료량 그대로",
"입력": [{ "이름": "결속선량", "단위": "kg/10㎡", "범위": [0.3, 0.4], "출처": "건설품셈 공통 2-8-7 재료량" }],
"입력": [{ "이름": "결속선량", "단위": "㎏/10㎡", "범위": [0.3, 0.4], "출처": "건설품셈 공통 2-8-7 재료량" }],
"중간": [{ "이름": "비계공", "식": "찾기(QC000084, 구분='비계공').수량 / 10" }, { "이름": "보통인부", "식": "찾기(QC000084, 구분='보통인부').수량 / 10" }],
"호표": [
{ "종류": "인력", "요소": "LB000006", "이름": "비계공", "단위": "인", "수량": "비계공", "비목": "노무비" },
@@ -1609,9 +1609,9 @@
"이름": "t1",
"식": "만약(타설깊이 <= 25, 찾기(QC000348, L='25이하').t_1, 만약(타설깊이 <= 30, 찾기(QC000348, L='30이하').t_1, 만약(타설깊이 <= 35, 찾기(QC000348, L='35이하').t_1, 만약(타설깊이 <= 40, 찾기(QC000348, L='40이하').t_1, 만약(타설깊이 <= 45, 찾기(QC000348, L='45이하').t_1, 만약(타설깊이 <= 50, 찾기(QC000348, L='50이하').t_1, 만약(타설깊이 <= 55, 찾기(QC000348, L='55이하').t_1, 0)))))))",
"출처": "건설품셈 공통 5-2-3 준비 및 이동시간",
"단위": "초"
"단위": "sec"
},
{ "이름": "cm", "식": "t1 + 타설깊이 / 찾기(QC000349, N치=N치).V_1 + 타설깊이 / 찾기(QC000349, N치=N치).V_2", "출처": "건설품셈 공통 5-2-3 cm", "단위": "초" },
{ "이름": "cm", "식": "t1 + 타설깊이 / 찾기(QC000349, N치=N치).V_1 + 타설깊이 / 찾기(QC000349, N치=N치).V_2", "출처": "건설품셈 공통 5-2-3 cm", "단위": "sec" },
{ "이름": "Q", "식": "3600 * 타설깊이 * 찾기(QC000347, 구분=현장).효율 / cm", "출처": "건설품셈 공통 5-2-3 Q", "단위": "m/hr" },
{ "이름": "T", "식": "1 / Q" },
{ "이름": "특별인부", "식": "찾기(QC000344, 구분='특별인부').수량 * T / 8" },
@@ -168,7 +168,7 @@
"구분": "건설품셈 토목",
"상세구분": "07장 항만공사",
"이름": "수중고르기 잠수부 1조 시간당 고르기 면적",
"결과단위": "㎡/시간",
"결과단위": "㎡/hr",
"출처": "건설품셈 토목 7-2-5",
"개정": [2021],
"소유": "공용",
@@ -323,7 +323,7 @@
"입력": [
{
"이름": "관경",
"단위": "mm",
"단위": "㎜",
"고르기": ["250이하", "300", "350", "400", "510", "560", "610", "630", "660", "685", "710", "760", "840", "860"],
"출처": "건설품셈 토목 7-4-1"
},
@@ -376,7 +376,7 @@
"소유": "공용",
"비고": "크레인 규격 띄우개 관경 430∼1,400 10톤 · 1,500 이상 15톤(주③ 장비규격 표) · 같은 톤 크롤러·타이어 크레인 가능(주④) · 철거는 본 품 30%까지 감(주①) — 감률 설계자 입력 · 소운반 포함 · 체결부 절단 별도",
"입력": [
{ "이름": "관경", "단위": "mm", "고르기": [430, 500, 600, 700, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600], "출처": "건설품셈 토목 7-4-2" },
{ "이름": "관경", "단위": "㎜", "고르기": [430, 500, 600, 700, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600], "출처": "건설품셈 토목 7-4-2" },
{ "이름": "작업", "고르기": ["설치", "철거"], "출처": "건설품셈 토목 7-4-2 주①" },
{ "이름": "철거감률", "단위": "%", "범위": [0, 30], "출처": "건설품셈 토목 7-4-2 주①" },
{
@@ -419,7 +419,7 @@
"소유": "공용",
"비고": "크레인 규격 관경 200∼710 10톤 · 760 이상 15톤(주④ 장비규격 표) · 해상관 1set = 배송관 1본+고무슬리브 1본+띄우개 1본 · 침설관 1set = 배송관 2본+고무슬리브 1본 · 배송관 예인·침설 제외(주①)",
"입력": [
{ "이름": "배송관관경", "단위": "mm", "고르기": [200, 250, 300, 350, 400, 510, 560, 610, 630, 660, 685, 710, 760, 840, 860], "출처": "건설품셈 토목 7-4-3" },
{ "이름": "배송관관경", "단위": "㎜", "고르기": [200, 250, 300, 350, 400, 510, 560, 610, 630, 660, 685, 710, 760, 840, 860], "출처": "건설품셈 토목 7-4-3" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -1308,13 +1308,13 @@
{ "이름": "협소현장", "설명": "주택가 상하수도관로부설 등 협소현장(주⑦)", "고르기": ["해당 없음", "상하수도관로부설 등 협소현장"], "출처": "건설품셈 공통 8-2-3 2 주⑦" },
{ "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" },
{ "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-3" },
{ "이름": "싸이클입력", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "단위": "초", "범위": [0, 30], "출처": "건설품셈 공통 8-2-3 3" }
{ "이름": "싸이클입력", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "단위": "sec", "범위": [0, 30], "출처": "건설품셈 공통 8-2-3 3" }
],
"중간": [
{ "이름": "K", "식": "찾기(CC000042, 현장조건=버킷조건).K" },
{ "이름": "보정", "식": "만약(E보정 == '해당 없음', 0, 찾기(CC000046, 항목=E보정).보정값)" },
{ "이름": "E", "식": "만약(E입력 > 0, E입력, 만약(협소현장 == '해당 없음', 찾기(CC000043, 현장조건=현장조건, 등급=등급, 토질명=토질명).E, 찾기(CC000044, 등급=등급, 토질명=토질명).E) - 보정)" },
{ "이름": "cm", "식": "만약(싸이클입력 > 0, 싸이클입력, 찾기(CC000045, 규격=규격구분, 각도=선회각도).시간)", "단위": "초" }
{ "이름": "cm", "식": "만약(싸이클입력 > 0, 싸이클입력, 찾기(CC000045, 규격=규격구분, 각도=선회각도).시간)", "단위": "sec" }
],
"결과": { "식": "3600 * 버킷용량 * K * f * E / cm", "출처": "건설품셈 공통 8-2-3" },
"끝수": { "대상": "결과", "자리": 3, "방법": "반올림", "출처": "건설품셈 공통 8-1-4 주①" },
@@ -1385,7 +1385,7 @@
{ "이름": "협소현장", "설명": "주택가 상하수도관로부설 등 협소현장(주⑦)", "고르기": ["해당 없음", "상하수도관로부설 등 협소현장"], "출처": "건설품셈 공통 8-2-3 2 주⑦" },
{ "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" },
{ "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-3" },
{ "이름": "싸이클입력", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "단위": "초", "범위": [0, 30], "출처": "건설품셈 공통 8-2-3 3" },
{ "이름": "싸이클입력", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "단위": "sec", "범위": [0, 30], "출처": "건설품셈 공통 8-2-3 3" },
{
"이름": "기계",
"설명": "굴착기(무한궤도) 규격",
@@ -1528,12 +1528,12 @@
{ "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-5" }
],
"중간": [
{ "이름": "t1", "식": "찾기(CC000047, 기종=기종, 작업방법=작업방법, 현장조건=적재조건).t1", "단위": "초" },
{ "이름": "t1", "식": "찾기(CC000047, 기종=기종, 작업방법=작업방법, 현장조건=적재조건).t1", "단위": "sec" },
{ "이름": "K", "식": "만약(K표 == '산적상태 적재', 찾기(CC000048, 현장조건=K조건).계수, 찾기(CC000049, 현장조건=K조건).계수)" },
{ "이름": "m", "식": "찾기(CC000051, 기종=기종).m", "단위": "초/m" },
{ "이름": "m", "식": "찾기(CC000051, 기종=기종).m", "단위": "sec/m" },
{ "이름": "l", "식": "만약(주행거리 > 0, 주행거리, 찾기(CC000052, 항목='편도주행거리(표준)').값)", "단위": "m" },
{ "이름": "t2", "식": "찾기(CC000052, 항목='기본시간(t2, 기어변화 등)').값", "단위": "초" },
{ "이름": "cm", "식": "m * l + t1 + t2", "단위": "초" },
{ "이름": "t2", "식": "찾기(CC000052, 항목='기본시간(t2, 기어변화 등)').값", "단위": "sec" },
{ "이름": "cm", "식": "m * l + t1 + t2", "단위": "sec" },
{ "이름": "보정", "식": "만약(E보정 == '해당 없음', 0, 찾기(CC000053, 항목=E보정).보정값)" },
{ "이름": "E", "식": "만약(E입력 > 0, E입력, 찾기(CC000050, 현장조건=현장조건, 등급=등급, 토질명=토질명).E - 보정)" }
],
@@ -1907,7 +1907,7 @@
},
"입력": [
{ "이름": "적재용량", "설명": "덤프트럭의 적재용량(T)", "단위": "ton", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "단위중량", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "단위": "t/㎥", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "단위중량", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "단위": "ton/㎥", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "체적변화율", "설명": "체적환산계수에서의 체적변화율(L = 흐트러진/자연)", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "f", "설명": "체적환산계수", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "운반거리", "설명": "운반거리(편도)", "단위": "㎞", "출처": "건설품셈 공통 8-2-8 2" },
@@ -1943,7 +1943,7 @@
{ "이름": "덮개", "설명": "적재함 덮개 설치 · 해체(t5)", "고르기": ["해당 없음", "인력에 의한 경우", "자동덮개시설의 경우"], "출처": "건설품셈 공통 8-2-8 6" },
{ "이름": "세륜", "설명": "세륜기 통과(t6)", "고르기": ["안 함", "함"], "출처": "건설품셈 공통 8-2-8 7" },
{ "이름": "적재방법", "설명": "적재시간(t1) 산출 갈래", "고르기": ["적재기계", "인력", "직접 입력"], "출처": "건설품셈 공통 8-2-8 1 · 8 · 9" },
{ "이름": "적재싸이클", "설명": "적재기계의 1회 싸이클시간(cms)", "단위": "초", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재싸이클", "설명": "적재기계의 1회 싸이클시간(cms)", "단위": "sec", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재버킷", "설명": "적재기계의 버킷용량(q)", "단위": "㎥", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "버킷계수", "설명": "적재기계의 버킷계수(k)", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재효율", "설명": "적재기계의 작업효율(Es)", "출처": "건설품셈 공통 8-2-8 8" },
@@ -2032,7 +2032,7 @@
},
"입력": [
{ "이름": "적재용량", "설명": "덤프트럭의 적재용량(T)", "단위": "ton", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "단위중량", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "단위": "t/㎥", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "단위중량", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "단위": "ton/㎥", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "체적변화율", "설명": "체적환산계수에서의 체적변화율(L = 흐트러진/자연)", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "f", "설명": "체적환산계수", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "운반거리", "설명": "운반거리(편도)", "단위": "㎞", "출처": "건설품셈 공통 8-2-8 2" },
@@ -2068,7 +2068,7 @@
{ "이름": "덮개", "설명": "적재함 덮개 설치 · 해체(t5)", "고르기": ["해당 없음", "인력에 의한 경우", "자동덮개시설의 경우"], "출처": "건설품셈 공통 8-2-8 6" },
{ "이름": "세륜", "설명": "세륜기 통과(t6)", "고르기": ["안 함", "함"], "출처": "건설품셈 공통 8-2-8 7" },
{ "이름": "적재방법", "설명": "적재시간(t1) 산출 갈래", "고르기": ["적재기계", "인력", "직접 입력"], "출처": "건설품셈 공통 8-2-8 1 · 8 · 9" },
{ "이름": "적재싸이클", "설명": "적재기계의 1회 싸이클시간(cms)", "단위": "초", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재싸이클", "설명": "적재기계의 1회 싸이클시간(cms)", "단위": "sec", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재버킷", "설명": "적재기계의 버킷용량(q)", "단위": "㎥", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "버킷계수", "설명": "적재기계의 버킷계수(k)", "출처": "건설품셈 공통 8-2-8 8" },
{ "이름": "적재효율", "설명": "적재기계의 작업효율(Es)", "출처": "건설품셈 공통 8-2-8 8" },
@@ -3431,7 +3431,7 @@
"중간": [
{ "이름": "f", "식": "만약(f입력 > 0, f입력, 찾기(CC000474, 분류=f분류, 상태=f상태, N값=fN값).f)" },
{ "이름": "k", "식": "만약(k입력 > 0, k입력, 찾기(CC000475, 분류=k분류, 상태=k상태, N값=kN값, 버킷용량=버킷용량).k)" },
{ "이름": "cm", "식": "찾기(CC000476, 버킷용량=버킷용량).싸이클시간 + (평균수심 - 찾기(CC000481, 항목='기준 수심').값) * 찾기(CC000481, 항목='수심 1m 증감당 싸이클시간 증감').값", "단위": "초" },
{ "이름": "cm", "식": "찾기(CC000476, 버킷용량=버킷용량).싸이클시간 + (평균수심 - 찾기(CC000481, 항목='기준 수심').값) * 찾기(CC000481, 항목='수심 1m 증감당 싸이클시간 증감').값", "단위": "sec" },
{ "이름": "E", "식": "찾기(CC000477, 구분=E1).E * 찾기(CC000479, 구분=E2).E" }
],
"결과": { "식": "3600 * 버킷용량 * k * f * E / cm", "출처": "건설품셈 공통 8-2-30" },
@@ -744,7 +744,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -805,7 +805,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -866,7 +866,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -927,7 +927,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-1" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -989,7 +989,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-2" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350, 400, 450, 500], "출처": "건설품셈 토목 8-4-2" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -1051,7 +1051,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350], "출처": "건설품셈 토목 8-4-2" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350], "출처": "건설품셈 토목 8-4-2" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -1122,7 +1122,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300, 350], "출처": "건설품셈 토목 8-4-2" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300, 350], "출처": "건설품셈 토목 8-4-2" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -1193,7 +1193,7 @@
"소유": "공용",
"비고": "해머식 착정공법 · 고성능 착정기(엔진 335.70㎾) · 굴착심도 200m 이하(8-4-2 주①) · 해석: 윙비트·에어해머·버튼비트는 원문 한 칸(병합) — 모든 규격에 같은 값 · 착정기 조종원 직종 설계자 고름(원문 운전사 구분에 없음) · 공시에 없는 재료 = 재료_자체",
"입력": [
{ "이름": "규격", "단위": "mm", "고르기": [100, 150, 200, 250, 300], "출처": "건설품셈 토목 8-4-2" },
{ "이름": "규격", "단위": "㎜", "고르기": [100, 150, 200, 250, 300], "출처": "건설품셈 토목 8-4-2" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -181,7 +181,7 @@
"구분": "건설품셈 건축",
"상세구분": "10장 창호 및 유리공사",
"이름": "커튼월 프레임 설치(스틱월)",
"결과단위": "원/kg",
"결과단위": "원/㎏",
"출처": "건설품셈 건축 10-1-8",
"개정": [2014, 2020, 2026],
"소유": "공용",
@@ -770,7 +770,7 @@
{ "이름": "용접공품", "단위": "인", "출처": "건설품셈 기계설비 13-2-2", "비고": "원문 범위 — 예 두께 3㎜ 0.0055∼0.0037" },
{ "이름": "특별인부품", "단위": "인", "출처": "건설품셈 기계설비 13-2-2", "비고": "원문 범위 — 예 두께 3㎜ 0.0027∼0.0019" },
{ "이름": "산소량", "단위": "ℓ", "출처": "건설품셈 기계설비 13-2-2 주④", "비고": "원문 범위 — 예 두께 3㎜ 16.5∼25.1" },
{ "이름": "LPG량", "단위": "kg", "출처": "건설품셈 기계설비 13-2-2 주④", "비고": "원문 범위 — 예 두께 3㎜ 0.016∼0.025" }
{ "이름": "LPG량", "단위": "㎏", "출처": "건설품셈 기계설비 13-2-2 주④", "비고": "원문 범위 — 예 두께 3㎜ 0.016∼0.025" }
],
"중간": [],
"호표": [
@@ -81,7 +81,7 @@
"구분": "산림품셈",
"상세구분": "03장 작업장 관리",
"이름": "임산물 운반로 신설 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 3-4-2 1",
"개정": [2026],
"소유": "공용",
@@ -1078,7 +1078,7 @@
"시험입력": { "토성": "양토", "막갈이깊이": 15 },
"입력": [
{ "이름": "토성", "설명": "흙의 성질", "고르기": ["사토", "양토", "식토"], "출처": "산림품셈 5-26-1" },
{ "이름": "막갈이깊이", "설명": "가는 깊이(표의 값 중)", "단위": "cm", "고르기": [9, 12, 15, 18, 21], "출처": "산림품셈 5-26-1" }
{ "이름": "막갈이깊이", "설명": "가는 깊이(표의 값 중)", "단위": "㎝", "고르기": [9, 12, 15, 18, 21], "출처": "산림품셈 5-26-1" }
],
"중간": [],
"호표": [{ "종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": "찾기(QF000139, 토성=토성, '막갈이깊이(cm)'=막갈이깊이).수량", "비목": "노무비" }],
@@ -1100,7 +1100,7 @@
"시험입력": { "토성": "양토", "막갈이깊이": 15 },
"입력": [
{ "이름": "토성", "설명": "흙의 성질", "고르기": ["사토", "양토", "식토"], "출처": "산림품셈 5-26-2" },
{ "이름": "막갈이깊이", "설명": "가는 깊이(표의 값 중)", "단위": "cm", "고르기": [9, 12, 15, 18, 21], "출처": "산림품셈 5-26-2" }
{ "이름": "막갈이깊이", "설명": "가는 깊이(표의 값 중)", "단위": "㎝", "고르기": [9, 12, 15, 18, 21], "출처": "산림품셈 5-26-2" }
],
"중간": [],
"호표": [{ "종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": "찾기(QF000140, 토성=토성, '막갈이깊이(cm)'=막갈이깊이).수량", "비목": "노무비" }],
@@ -1197,7 +1197,7 @@
"구분": "산림품셈",
"상세구분": "05장 식재 및 녹화",
"이름": "복사이식 굴착기 시간당 작업량(Q1)",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 5-29 주④",
"개정": [2026],
"소유": "공용",
@@ -10,7 +10,7 @@
"구분": "산림품셈",
"상세구분": "06장 생육보조",
"이름": "비료주기",
"결과단위": "원/100kg",
"결과단위": "원/100㎏",
"출처": "산림품셈 6-1",
"개정": [2026],
"소유": "공용",
@@ -44,7 +44,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "토사깍기(기계 · 굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-3-2",
"개정": [2026],
"소유": "공용",
@@ -55,7 +55,7 @@
{ "이름": "K", "식": "찾기(QF000249, 구분='K').적용" },
{ "이름": "f", "식": "찾기(CF000122, 구분='f').적용" },
{ "이름": "E", "식": "E입력" },
{ "이름": "Cm", "식": "찾기(QF000249, 구분='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000249, 구분='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -156,7 +156,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "암절취 집토(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-4-2",
"개정": [2026],
"소유": "공용",
@@ -167,7 +167,7 @@
{ "이름": "K", "식": "찾기(QF000251, 구분='K').적용" },
{ "이름": "f", "식": "찾기(QF000251, 구분='f').적용원문" },
{ "이름": "E", "식": "찾기(QF000251, 구분='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000251, 구분='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000251, 구분='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -297,7 +297,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "발파암 집토(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-5-3",
"개정": [2026],
"소유": "공용",
@@ -308,7 +308,7 @@
{ "이름": "K", "식": "찾기(QF000254, 구분='K').적용" },
{ "이름": "f", "식": "찾기(QF000254, 구분='f').적용원문" },
{ "이름": "E", "식": "찾기(QF000254, 구분='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000254, 구분='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000254, 구분='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -386,7 +386,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "콘크리트 깨기 집토(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-7-1 주⑥",
"개정": [2026],
"소유": "공용",
@@ -396,7 +396,7 @@
{ "이름": "K", "식": "찾기(QF000257, 구분='K').적용" },
{ "이름": "f", "식": "찾기(QF000257, 구분='f').적용" },
{ "이름": "E", "식": "찾기(QF000257, 구분='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000257, 구분='㎝(초)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000257, 구분='㎝(초)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -823,7 +823,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "측구터파기 토사 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-12-1",
"개정": [2026],
"소유": "공용",
@@ -833,7 +833,7 @@
{ "이름": "K", "식": "찾기(QF000267, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='k').적용" },
{ "이름": "f", "식": "찾기(QF000267, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='f').적용" },
{ "이름": "E", "식": "찾기(QF000267, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000267, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000267, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -2026,7 +2026,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "되메우기 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-1",
"개정": [2026],
"소유": "공용",
@@ -2036,7 +2036,7 @@
{ "이름": "K", "식": "찾기(QF000288, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='k').적용" },
{ "이름": "f", "식": "찾기(QF000288, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='f').적용" },
{ "이름": "E", "식": "찾기(QF000288, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000288, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000288, 이름='유압식백호우(무한궤도,0.7㎥)', 항목='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -2078,7 +2078,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "다짐(플레이트콤펙터) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-2",
"개정": [2026],
"소유": "공용",
@@ -2119,7 +2119,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "표토제거 답(畓)구간(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎡/시간",
"결과단위": "㎡/hr",
"출처": "산림품셈 9-15-1",
"개정": [2026],
"소유": "공용",
@@ -2163,7 +2163,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "표토제거 답(畓)외구간(불도저 19ton) 시간당 작업량",
"결과단위": "㎡/시간",
"결과단위": "㎡/hr",
"출처": "산림품셈 9-15-2",
"개정": [2026],
"소유": "공용",
@@ -2222,7 +2222,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "노체포설 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-16-1 주②",
"개정": [2026],
"소유": "공용",
@@ -2232,7 +2232,7 @@
{ "이름": "K", "식": "찾기(CF000097, 구분='K').적용" },
{ "이름": "f", "식": "찾기(CF000097, 구분='f').적용" },
{ "이름": "E", "식": "찾기(CF000097, 구분='E').적용" },
{ "이름": "Cm", "식": "찾기(CF000097, 구분='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(CF000097, 구분='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q0 * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -2268,13 +2268,13 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "노체다짐 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-16-2 주②",
"개정": [2026],
"소유": "공용",
"입력": [],
"중간": [
{ "이름": "V", "식": "찾기(CF000099, 구분='V(다짐속도,km/hr)').적용", "단위": "km/hr" },
{ "이름": "V", "식": "찾기(CF000099, 구분='V(다짐속도,km/hr)').적용", "단위": "㎞/hr" },
{ "이름": "W", "식": "찾기(CF000099, 구분='W(롤러 유효폭,m)').적용", "단위": "m" },
{ "이름": "E", "식": "찾기(CF000099, 구분='E(작업효율)').적용" },
{ "이름": "D", "식": "찾기(CF000099, 구분='D(펴는 흙의 두께,m)').적용", "단위": "m" },
@@ -2339,7 +2339,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "비다짐(정지 · 불도저 19ton) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-17-2",
"개정": [2026],
"소유": "공용",
@@ -2393,7 +2393,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "층따기(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-18",
"개정": [2026],
"소유": "공용",
@@ -2404,7 +2404,7 @@
{ "이름": "K", "식": "찾기(QF000293, 구분='K').적용" },
{ "이름": "f", "식": "찾기(QF000293, 구분='f').적용원문" },
{ "이름": "E", "식": "찾기(QF000293, 구분='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000293, 구분='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000293, 구분='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -2639,7 +2639,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "섞기(굴착기 0.7㎥) 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-22",
"개정": [2026],
"소유": "공용",
@@ -10,7 +10,7 @@
"구분": "산림품셈",
"상세구분": "10장 자재․장비 운반",
"이름": "골재운반 덤프트럭 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 10-3-1 주",
"개정": [2026],
"소유": "공용",
@@ -671,7 +671,7 @@
"구분": "산림품셈",
"상세구분": "10장 자재․장비 운반",
"이름": "불도저 운반 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 10-11 주②",
"개정": [2026],
"소유": "공용",
@@ -778,19 +778,19 @@
{
"이름": "로더cm",
"식": "찾기(CC000051, 기종='타이어식').m * 찾기(CC000052, 항목='편도주행거리(표준)').값 + 찾기(CC000047, 기종='타이어식', 작업방법='산적상태에서 담을 때', 현장조건='용이한 경우').t1 + 찾기(CC000052, 항목='기본시간(t2, 기어변화 등)').값",
"단위": "초",
"단위": "sec",
"출처": "건설품셈 공통 8-2-5 1"
},
{
"이름": "Cm",
"식": "만약(로더 == 1, 로더cm, 만약(적재기계 == '0201-0070', 찾기(QF000336, 구분='㎝(초)').값, 찾기(CC000045, 규격=규격구분, 각도=180).시간))",
"단위": "초",
"단위": "sec",
"출처": "산림품셈 10-12-1 주⑤ · 건설품셈 공통 8-2-3 3"
},
{
"이름": "Cms",
"식": "만약(로더 == 1, 로더cm, 만약(적재기계 == '0201-0070', 20, 찾기(CC000045, 규격=규격구분, 각도=135).시간))",
"단위": "초",
"단위": "sec",
"출처": "산림품셈 10-12-1 2. 운반 · 건설품셈 공통 8-2-3 3"
},
{ "이름": "Es", "식": "만약(로더 == 1, E0, 0.85)", "출처": "산림품셈 10-12-1 2. 운반" },
@@ -91,7 +91,7 @@
"구분": "산림품셈",
"상세구분": "11장 가설",
"이름": "쇄석·혼합석 부설 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 11-4 주①",
"개정": [2026],
"소유": "공용",
@@ -102,7 +102,7 @@
{ "이름": "k", "식": "찾기(QF000349, 구분='k(버킷계수)').적용" },
{ "이름": "f", "식": "찾기(QF000349, 구분='f(체적환산계수)').적용" },
{ "이름": "E", "식": "찾기(QF000349, 구분='E(작업효율)').적용" },
{ "이름": "Cm", "식": "찾기(QF000349, 구분='㎝(sec, 1회 사이클 시간)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000349, 구분='㎝(sec, 1회 사이클 시간)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * k * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
@@ -176,7 +176,7 @@
"소유": "공용",
"비고": "원문 플레이트콤팩트 1.54ton — 기계 요소는 1.5ton 하나(1730-0015)",
"입력": [
{ "이름": "규격", "단위": "mm" },
{ "이름": "규격", "단위": "㎜" },
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
@@ -1321,7 +1321,7 @@
"구분": "산림품셈",
"상세구분": "13장 구조물",
"이름": "토양개량제 부설 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 13-13-2",
"개정": [2026],
"소유": "공용",
@@ -1332,7 +1332,7 @@
{ "이름": "K", "식": "찾기(QF000465, 이름='굴착기(0.2㎥)', 항목='k').적용" },
{ "이름": "f", "식": "찾기(QF000465, 이름='굴착기(0.2㎥)', 항목='f').적용" },
{ "이름": "E", "식": "찾기(QF000465, 이름='굴착기(0.2㎥)', 항목='E').적용" },
{ "이름": "Cm", "식": "찾기(QF000465, 이름='굴착기(0.2㎥)', 항목='㎝(sec)').적용", "단위": "초" }
{ "이름": "Cm", "식": "찾기(QF000465, 이름='굴착기(0.2㎥)', 항목='㎝(sec)').적용", "단위": "sec" }
],
"결과": { "식": "반올림(3600 * q * K * f * E / Cm, 3)", "출처": "건설품셈 공통 8-1-4 주①" },
"끝수": "소수 3자리 사사오입(건설품셈 공통 8-1-4 주①)",
+2 -2
View File
@@ -37,7 +37,7 @@
{ "이름": "상차", "설명": "채집한 돌을 덤프에 싣는지", "고르기": ["포함", "안 함"] },
{ "이름": "덤프", "설명": "소운반 덤프트럭", "고르기": ["0602-0025", "0602-0045", "0602-0060", "0602-0080"], "출처": "건설품셈 공통 8-2-8" },
{ "이름": "운반거리", "설명": "소운반 편도 거리 — 0 이면 소운반 없음", "단위": "㎞", "출처": "건설품셈 공통 8-2-8" },
{ "이름": "쌓기무게", "설명": "쌓기 1㎡ 돌 무게", "단위": "t/㎡" },
{ "이름": "쌓기무게", "설명": "쌓기 1㎡ 돌 무게", "단위": "ton/㎡" },
{ "이름": "적하조건", "설명": "덤프 적하 작업조건", "고르기": ["불량", "보통", "양호"], "출처": "건설품셈 공통 8-2-8 4" },
{
"이름": "지역",
@@ -51,7 +51,7 @@
{ "이름": "규격구분", "식": "만약(굴착기 == '0201-0020' 또는 굴착기 == '0201-0040', '0.12∼0.4', 만약(굴착기 == '0201-0100', '1.0∼1.2', '0.6∼0.8'))" },
{ "이름": "K", "식": "찾기(CC000042, 현장조건='버킷에 넣기 어렵고 불규칙한 공극이 생기는 것으로서 발파 또는 리퍼작업 등에 의하여 얻어진 암과 파쇄암, 호박돌, 역 등인 경우').K" },
{ "이름": "E", "식": "찾기(CC000043, 현장조건='흐트러진 상태', 등급=등급, 토질명='파쇄암').E" },
{ "이름": "cm", "식": "찾기(CC000045, 규격=규격구분, 각도=선회각도).시간", "단위": "초" },
{ "이름": "cm", "식": "찾기(CC000045, 규격=규격구분, 각도=선회각도).시간", "단위": "sec" },
{
"이름": "Q",
"식": "로직(GC001147, 버킷용량=q, 규격구분=규격구분, 선회각도=선회각도, 버킷조건='버킷에 넣기 어렵고 불규칙한 공극이 생기는 것으로서 발파 또는 리퍼작업 등에 의하여 얻어진 암과 파쇄암, 호박돌, 역 등인 경우', 현장조건='흐트러진 상태', 등급=등급, 토질명='파쇄암', E보정='해당 없음', 협소현장='해당 없음', f=1, E입력=0, 싸이클입력=0)",
+3 -3
View File
@@ -352,7 +352,7 @@
}
},
"사본": {
"판": "5bcfbe99102ef55d",
"판": "7a470ae53b9da0de",
"시점": "2026-11-07T15:23:23",
"로직": [
{
@@ -2211,7 +2211,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "되메우기 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-1",
"개정": [
2026
@@ -2702,7 +2702,7 @@
"입력": [
{
"이름": "규격",
"단위": "mm"
"단위": "㎜"
},
{
"이름": "지역",
+3 -3
View File
@@ -940,7 +940,7 @@
}
},
"사본": {
"판": "5bcfbe99102ef55d",
"판": "7a470ae53b9da0de",
"시점": "2026-11-07T15:23:23",
"로직": [
{
@@ -2799,7 +2799,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "되메우기 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-1",
"개정": [
2026
@@ -3290,7 +3290,7 @@
"입력": [
{
"이름": "규격",
"단위": "mm"
"단위": "㎜"
},
{
"이름": "지역",
+2 -2
View File
@@ -247,7 +247,7 @@
}
},
"사본": {
"판": "4066584ac7f0910f",
"판": "90e75b6182c18dcb",
"시점": "2026-11-07T15:23:23",
"로직": [
{
@@ -858,7 +858,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "노체포설 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-16-1 주②",
"개정": [
2026
+3 -3
View File
@@ -508,7 +508,7 @@
}
},
"사본": {
"판": "bf62fc43e50cd058",
"판": "69839ebda29c8cc5",
"시점": "2026-11-07T15:23:24",
"로직": [
{
@@ -1622,7 +1622,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "되메우기 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-1",
"개정": [
2026
@@ -1955,7 +1955,7 @@
"입력": [
{
"이름": "규격",
"단위": "mm"
"단위": "㎜"
},
{
"이름": "지역",
+3 -3
View File
@@ -1060,7 +1060,7 @@
}
},
"사본": {
"판": "5bcfbe99102ef55d",
"판": "7a470ae53b9da0de",
"시점": "2026-11-07T15:23:24",
"로직": [
{
@@ -2919,7 +2919,7 @@
"구분": "산림품셈",
"상세구분": "09장 토공",
"이름": "되메우기 굴착기 시간당 작업량",
"결과단위": "㎥/시간",
"결과단위": "㎥/hr",
"출처": "산림품셈 9-14-1",
"개정": [
2026
@@ -3410,7 +3410,7 @@
"입력": [
{
"이름": "규격",
"단위": "mm"
"단위": "㎜"
},
{
"이름": "지역",
@@ -41,7 +41,11 @@ def _read(folder, doc, inputs=None):
def _rows(eq) -> dict:
return {r["cid"]: r for r in eq["소계"]["줄"]}
"""묶음(801-3) — 더한 · 부른 단가산출 = cid 로 · 직접 줄 묶음 속 더함 · 덮음 줄 = 이름으로."""
got = {r["cid"]: r for r in eq["소계"]["줄"] if "cid" in r}
for r in eq["소계"]["줄"]:
got |= {x["이름"]: x for x in r.get("낱줄", [])}
return got
def _closes(eq, ans) -> None:
@@ -80,21 +84,19 @@ def test_A_added_removed_covered_total_equals_sk(made):
_closes(eq, ans)
rows = _rows(eq)
# 덮음 — 형틀목공 품 덮개 · 원래 = 고침 없는 SK 의 같은 줄 금액
cover = rows["labor:LB000007#0"]
cover = rows["형틀목공"] # 직접 묶음 속 덮은 줄(이름)
was = next(x for x in bare["줄"] if x["cid"] == "labor:LB000007#0")["금액"]
assert cover["상태"] == "덮음" and "품" in cover["덮개"]
assert cover["상태"] == "덮음"
assert _d(cover["원래"]) == _d(was)
assert cover["노무비"] == D("137895") # 품 0.5 × 단가 275,790
# 뺌 — 지운 줄 = 금액 0 · 원래 금액만
gone = rows["labor:LB000002#0"]
assert gone["상태"] == "뺌" and gone["계"] == 0
assert _d(gone["원래"]) == _d(next(x for x in bare["줄"] if x["cid"] == gone["cid"])["금액"])
# 더함 — 기초(굴착기 641 × 2) · 인력 · 기초는 수식 나무
basic, man = rows["add:#0"], rows["add:#1"]
assert (
basic["상태"] == man["상태"] == "더함" and basic["갈래"] == "블록" and man["갈래"] == "자원"
)
assert basic["계"] > 0 and man["노무비"] > 0
assert cover["계"] == D("137895") # 품 0.5 × 단가 275,790
# 뺌 — 지운 줄 = 묶음 아님(식 줄 표지로만 · 아래 `out["뺌"]`)
assert "labor:LB000002#0" not in rows and "보통인부" not in rows
# 더함 — 기초(굴착기 641 × 2) = 묶음 · 인력 = 직접 묶음 속 더한 줄 · 기초는 수식 나무
basic = rows["add:#0"]
man = next(x for r in eq["소계"]["줄"] for x in r.get("낱줄", []) if x["상태"] == "더함")
assert basic["상태"] == man["상태"] == "더함" and basic["갈래"] == "블록"
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["블록", "직접"]
assert basic["계"] > 0 and man["계"] > 0
assert basic["나무"]["열쇠"] == "GC001148" and basic["나무"]["줄"]
# 나무 합계 × 수량 = 행(같은 입력)
assert abs(basic["나무"]["합계"]["계"] * 2 - basic["계"]) <= 2
@@ -106,7 +108,8 @@ def test_A_added_removed_covered_total_equals_sk(made):
assert out["뺌"] is True and out["표지"] == []
# 고침 없는 SK(plain)는 표지도 더함도 없음 — 합계 = 같은 바탕 금액
flat = _read(me, _doc(me, plain))
assert {r["상태"] for r in flat["소계"]["줄"]} == {"바탕"}
assert {r.get("상태", "바탕") for r in flat["소계"]["줄"]} == {"바탕"}
assert [r["갈래"] for r in flat["소계"]["줄"]] == ["직접"] # 801-3 한 묶음 · 낱줄 없음
assert flat["합계"]["계"] == _d(bare["기준"]["계"])
assert not any("표지" in x for x in flat["줄"])
@@ -151,9 +154,9 @@ def test_B_unit_cover_rate_and_reference(world):
eq = _read(me, doc)
_closes(eq, ans)
rows = _rows(eq)
price = rows["labor:LB000002#0"]
assert price["상태"] == "덮음" and "단가" in price["덮개"] and price["원래"] is not None
assert rows["add:#0"]["갈래"] == "율" and rows["add:#0"]["경비"] > 0
pool = next(r for r in eq["소계"]["줄"] if r["갈래"] == "직접")
price = next(x for x in pool["낱줄"] if x["상태"] == "덮음")
assert price["원래"] is not None and "sub2임시 율" in pool["이름"] and pool["경비"] > 0
assert rows["add:#1"]["갈래"] == "블록" and rows["add:#1"]["상태"] == "더함"
assert _line(eq, "호표", price["이름"])["표지"] == ["단가"]
@@ -171,7 +174,7 @@ def test_C_imported_sk_same_rule(world):
eq = _read(me, doc)
_closes(eq, ans)
rows = _rows(eq)
assert rows["add:#0"]["상태"] == "더함" and rows[cid]["상태"] == "덮음"
assert [x["상태"] for x in eq["소계"]["줄"][-1]["낱줄"]] == ["덮음", "더함"], rows
def test_plain_sk_has_answer_table(world):
@@ -183,7 +186,7 @@ def test_plain_sk_has_answer_table(world):
def test_D_imported_outside_total_line_not_in_sum(world):
"""D 가져온 식 `합밖` 줄 = 합계에 안 듦 → 행 0 · 밖금액 · 끝수에 안 섞임."""
"""D 가져온 식 `합밖` 줄 = 합계에 안 듦 → 묶음 아님 · `밖` 글 · 끝수에 안 섞임."""
me = world["personal"]
row = _imported_row()
row["호표"][1] = {**row["호표"][1], "합밖": True}
@@ -193,6 +196,6 @@ def test_D_imported_outside_total_line_not_in_sum(world):
ans = _calc(me, doc)
eq = _read(me, doc)
_closes(eq, ans)
out = [r for r in eq["소계"]["줄"] if r.get("합밖")]
assert len(out) == 1 and out[0]["계"] == 0 and out[0]["밖금액"] > 0
out = eq["소계"]["밖"]
assert len(out) == 1 and out[0]["밖금액"] > 0
assert abs(eq["소계"]["끝수"]["계"]) < 1
+1 -1
View File
@@ -163,7 +163,7 @@ def test_표본_기호_단위_원문_같음():
"작업효율",
"체적환산계수",
)
assert (t["cm"]["이름"], t["cm"]["단위"]) == ("1회 싸이클시간", "초")
assert (t["cm"]["이름"], t["cm"]["단위"]) == ("1회 싸이클시간", "sec") # 801-5 원문 표 머리 「싸이클시간(Sec)」 꼴
carry = (BOOK / "제01장_적용기준/1-02_설계_및_수량.md").read_text(encoding="utf-8")
for line in (
+3 -3
View File
@@ -62,10 +62,10 @@ def test_굴착기_블록_한_줄_끝수():
def test_거푸집_자원_덧줄_지게_자원():
eq = _export("GC000211")
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원", "덧줄"]
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"] # 801-3 직접 줄 한 묶음
_closes(eq)
eq = _export("GC001140")
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원"]
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"]
_closes(eq)
@@ -96,7 +96,7 @@ def test_결과_로직은_소계_없음():
def test_화면_부품이_서버_칸을_읽음():
ts = Path("ui_template/ui_template_equation_sum.ts").read_text(encoding="utf-8")
for name in ("노무비", "재료비", "경비", "끝수", "합계", "자원", "덧줄"):
for name in ("노무비", "재료비", "경비", "끝수", "합계"):
assert name in ts
line = Path("M01_MasterData/M01_MasterData_UI_LogicLab_Equation.ts").read_text(encoding="utf-8")
assert "createEqSum(eq, open" in line # 796-2e 덧단추 자리 인자가 뒤에 붙음
+117
View File
@@ -0,0 +1,117 @@
"""801-3 수식 탭 소계 묶음(요구 662-3) 지킴 — 금액 셈은 그대로 · 묶음만 다시.
① 묶음 = 부른 단가산출(갈래 블록) 하나씩 + 직접 줄 한 묶음(갈래 직접) · 돈 없는 줄 = 묶음 아님 · 안쪽 말 0
② M01 전수 — 묶음 합 + 끝수 = 합계(엔진 비목 합 그대로) · 직접 묶음 하나뿐
③ 부품 — 묶음 하나 = 머리 없음 · 계 / 둘 이상 = 머리 · 소계 / 단가산출 둘 이상 = 합계 · 안쪽 말 글 0
"""
from __future__ import annotations
import importlib
from decimal import Decimal as D
from pathlib import Path
from M01_MasterData import M01_MasterData_Store as store
mf = importlib.import_module("master_formula")
meq = importlib.import_module("master_equation")
grp = importlib.import_module("master_equation_group")
COSTS = ("노무비", "재료비", "경비", "계")
BAD = ("자기", "자원", "덧줄", "블록") # 코드 안쪽 말 — 화면 글에 0
def _w():
return store.loaded()[1]
def _export(key: str) -> dict:
w = _w()
return meq.export(w, key, meq.given_of(w, w.logic(key), None))
def _row(kind, name, cost=(0, 0, 0), **more):
got = dict(zip(COSTS[:3], (D(x) for x in cost), strict=True))
return {"갈래": kind, "이름": name, **got, "계": sum(got.values(), D(0))} | more
def test_묶음_함수_하나뿐_직접_한_묶음():
got = grp.bundles(
[
_row("블록", "굴착기", (1, 2, 3)),
_row("블록", "쉬는 단가산출"), # 돈 0 = 묶음 아님
_row("자원", "보통인부", (5, 0, 0), 종류="인력"),
_row("자원", "시멘트", (0, 7, 0), 종류="재료"),
_row("덧줄", "공구손료", (0, 0, 1)),
]
)
assert [r["갈래"] for r in got["줄"]] == ["블록", "직접"]
assert got["줄"][0]["이름"] == "굴착기" and got["줄"][1]["이름"] == "인력 · 재료"
assert (got["줄"][1]["노무비"], got["줄"][1]["재료비"], got["줄"][1]["경비"]) == (5, 7, 1)
only = grp.bundles([_row("덧줄", "공구손료", (0, 0, 1))])["줄"]
assert [r["이름"] for r in only] == ["경비"] and not grp.bundles([_row("블록", "빈")])["줄"]
def test_M02_상태_줄():
got = grp.bundles(
[
_row("자원", "보통인부", (5, 0, 0), 상태="더함", 종류="인력"),
_row("자원", "형틀목공", (4, 0, 0), 상태="덮음", 원래=D(9), 종류="인력"),
_row("자원", "지운 줄", 상태="뺌", 종류="인력"),
_row("자원", "밖 줄", 합밖=True, 밖금액=D(8)),
]
)
assert [x["이름"] for x in got["줄"][0]["낱줄"]] == ["보통인부", "형틀목공"]
assert got["밖"] == [{"이름": "밖 줄", "밖금액": D(8)}] and got["줄"][0]["계"] == 9
def test_거푸집_한_묶음_굴착기_한_묶음():
eq = _export("GC000211")
assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"]
assert eq["소계"]["줄"][0]["이름"] == "인력 · 재료"
eq = _export("GC001148")
rows = eq["소계"]["줄"]
assert [(r["갈래"], r["이름"]) for r in rows] == [("블록", "굴착기(무한궤도)")]
assert eq["합계"]["계"] == D("641.0") and eq["소계"]["끝수"]["계"] == D("-0.3")
def test_M01_전수_묶음_합_끝수_합계_안쪽_말_0():
w = _w()
many = seen = 0
for tid, rows in sorted(w.index.items()):
if mf.mk.GROUP_OF.get(tid) != "로직":
continue
for logic in rows.values():
if not logic.get("호표"):
continue
try:
eq = _export(logic["키"])
except mf.FormulaError:
continue
sub = eq.get("소계")
if not sub:
continue
seen += 1
lines = sub["줄"]
assert sum(r["갈래"] == "직접" for r in lines) <= 1, logic["키"]
assert all(r["계"] != 0 for r in lines), logic["키"]
for r in lines:
assert not any(b in r["이름"] for b in BAD), (logic["키"], r["이름"])
for k in COSTS:
got = sum((r[k] for r in lines), D(0)) + sub["끝수"][k]
assert got == eq["합계"][k], (logic["키"], k)
many += sum(r["갈래"] == "블록" for r in lines) >= 2
assert seen > 400 and many >= 10, (seen, many)
def test_부품_머리_소계_합계_규칙_안쪽_말_0():
ts = Path("ui_template/ui_template_equation_sum.ts").read_text(encoding="utf-8")
assert "sub.줄.length >= 2" in ts # 묶음 하나 = 머리 · 소계 없음
assert '"합계" : "계"' in ts and '=== "블록").length >= 2' in ts # 합계 = 단가산출 둘 이상
ui = "".join(
line
for line in ts.splitlines(keepends=True)
if not line.lstrip().startswith(("*", "/*", "//", "/**"))
)
for word in ("자기 자원", "덧줄", "KIND"):
assert word not in ui, word
+138
View File
@@ -0,0 +1,138 @@
"""801-5 M01 로직 단위 글 = 건설품셈 원문 꼴(요구 663-2) 지킴 — 글만 바꿈 · 금액 그대로.
① 대응표 근거 — 규칙마다 근거 절 파일이 있고 그 안에 원문 글이 실제로 있음(+ 지금 글은 같은 꼴로 안 쓰임)
② 섞임 0 — M01 로직 입력 · 중간 · 결과단위가 원문 꼴로 다 바뀌어 있음(규칙이 더 바꿀 것 없음 · 두 번 돌려도 같음)
③ 금액 같음 — 변수 단위 글을 지워도 엔진 셈 · 끝수 지문이 같음(단위 글은 셈에 안 듦)
④ M02 사본 도구 — 같은 규칙으로 사본 줄 단위만 바뀌고 `판` 다시 셈
"""
from __future__ import annotations
import copy
import importlib
import re
import pytest
from M01_MasterData import M01_MasterData_Store as store
ut = importlib.import_module("master_unit_text")
mf = importlib.import_module("master_formula")
build = importlib.import_module("build_단위표기_801_5")
def _rows():
for data in store.loaded()[0].values():
if data.get("그룹") == "로직":
yield from data["줄"]
def test_대응표_근거_절에_원문_글이_있음():
assert len(ut.RULES) >= 7
for name, _, to, token, files in ut.RULES:
assert files and to == token, name
for rel in files:
path = ut.EVIDENCE_ROOT / rel
assert path.is_file(), (name, rel)
assert token in path.read_text(encoding="utf-8"), (name, rel)
def test_규칙_예():
got = {
u: ut.canon(u)
for u in ("㎥/시간", "㎡/시간", "㎢/시간", "원/시간", "초", "초/m", "mm", "TON/㎡")
}
assert got == {
"㎥/시간": "㎥/hr",
"㎡/시간": "㎡/hr",
"㎢/시간": "㎢/시간", # 원문 9-5-4 가 쓰는 꼴 — 안 건드림
"원/시간": "원/시간",
"초": "sec",
"초/m": "sec/m",
"mm": "㎜",
"TON/㎡": "ton/㎡",
}
for u in got.values():
assert ut.canon(u) == u # 두 번 돌려도 같음
def test_섞임_0_원문_꼴로_다_바뀜():
left = []
for row in _rows():
units = [row.get("결과단위")] + [
s.get("단위") for k in ("입력", "중간") for s in row.get(k) or []
]
left += [(row["키"], u) for u in units if u and ut.canon(u) != u]
assert not left, left[:5]
def test_굴착기_Cm_sec_Q_hr():
row = store.loaded()[1].logic("GF000088") # 토사깍기(굴착기 0.7㎥) 시간당 작업량
assert {s["단위"] for s in row["중간"] if s["이름"] == "Cm"} == {"sec"}
assert row["결과단위"] == "㎥/hr"
_OLD = ( # 원문 꼴 → 옛 글(되돌려 본 꼴) — 두 꼴로 셈이 같음을 대조
(re.compile(r"(?<![가-힣A-Za-z])sec(?![A-Za-z])"), "초"),
(re.compile(r"(?<=[㎥㎡])/hr"), "/시간"),
(re.compile("㎜"), "mm"),
(re.compile("㎝"), "cm"),
(re.compile("㎏"), "kg"),
(re.compile("㎞/hr"), "km/hr"),
)
def _legacy(text: str) -> str:
for pat, old in _OLD:
text = pat.sub(old, text)
return text
def test_금액_같음_옛_글로_되돌려도_셈_지문_같음():
files = store.loaded()[0]
old = copy.deepcopy(files)
for data in old.values():
if data.get("그룹") == "로직":
for row in data["줄"]:
for k in ("입력", "중간"):
for s in row.get(k) or []:
s["단위"] = _legacy(s.get("단위") or "")
row["결과단위"] = _legacy(row.get("결과단위") or "")
now, was = mf.Master(files), mf.Master(old)
keys = [r["키"] for r in _rows()]
hit = {
r["키"]
for r in _rows()
if any(
_legacy(s.get("단위") or "") != (s.get("단위") or "")
for k in ("입력", "중간")
for s in r.get(k) or []
)
}
assert len(hit) >= 40
for key in sorted(hit) + keys[::25]:
assert build.fingerprint(now, now.logic(key)) == build.fingerprint(was, was.logic(key)), key
def test_M02_사본_도구():
cu = importlib.import_module("M02_MasterTemplete.M02_Copy_Units")
old = {
"로직": [
{
"키": "GC000001",
"결과단위": "㎥/시간",
"입력": [{"이름": "a", "단위": "mm"}],
"중간": [{"이름": "Cm", "단위": "초"}],
}
],
"판": "x",
}
new, done, _ = cu.fill(old, None)
row = new["로직"][0]
assert done == 3 and row["결과단위"] == "㎥/hr" and row["입력"][0]["단위"] == "㎜"
assert row["중간"][0]["단위"] == "sec" and new["판"] != "x"
assert cu.fill(new, None)[1] == 0 # 두 번째 = 바꿀 것 없음
if __name__ == "__main__":
pytest.main([__file__, "-q"])
+10 -6
View File
@@ -45,7 +45,7 @@ export interface EqNode {
사슬?: boolean;
}
/** 단가산출 소계 줄(799-1) — 갈래 블록 | 자원 | 덧줄 | 율 · M02 SK(796-2c) 는 셈 답 한 줄마다 + 상태 */
/** 단가산출 소계 묶음(799-1 · 801-3) — 갈래 블록(부른 · 더한 단가산출) | 직접(인력 · 재료 · 기계 · 덧줄 한 묶음) · M02 SK 는 상태 */
export interface EqSumRow {
갈래: string;
이름: string;
@@ -61,9 +61,8 @@ export interface EqSumRow {
/** 덮음 · 뺌 — 고치기 전 금액 */
원래?: unknown;
까닭?: string;
/** 합계 밖 줄(엔진이 비목 합에서 뺌) — 행 금액은 0 · 밖금액에 원래 금액 */
합밖?: boolean;
밖금액?: unknown;
/** 직접 묶음 속 더한 · 덮은 줄(M02 SK) */
낱줄?: { 이름: string; 상태: string; 계: unknown; 원래?: unknown }[];
/** 더한 기초의 수식(접힘 블록) */
나무?: EqTree | null;
/** 더한 기초 차례(M02 SK 더한줄 · 연결 끊기 796-2e) */
@@ -86,8 +85,13 @@ export interface EqTree {
줄: EqLine[];
변수표: EqVarRow[];
합계: Record<string, unknown>;
/** 799-1 — 하위 로직 블록마다 노 · 재 · 경 소계 + 끝수(합계 − 줄 합) · 결과 로직은 없음 */
소계?: { 줄: EqSumRow[]; 끝수: Record<string, unknown>; 끝수까닭?: string };
/** 799-1 · 801-3 — 묶음마다 노 · 재 · 경 소계 + 끝수(합계 − 묶음 합) · 합계 밖 줄 · 결과 로직은 없음 */
소계?: {
줄: EqSumRow[];
끝수: Record<string, unknown>;
끝수까닭?: string;
밖?: { 이름: string; 밖금액: unknown }[];
};
/** M02 SK — 셈 답이 안 서서 바탕 줄 합계를 보일 때의 까닭 */
답까닭?: string;
/** 800-2 — 오른쪽 변수 정의 영역(고른 옵션 갈래만 · 같은 기호 한 줄 · 값 · 단위 · DB 열쇠 없음) */
+39 -31
View File
@@ -1,11 +1,11 @@
/* =============================================================================
* ui_template_equation_sum.ts
* 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 요구 655-1 · 661 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
* 묶음 = 기준 단가산출마다(하위 로직 블록 · 자기 자원 · 덧줄) 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계(노 · 재 · 경 · 계 칸)
* 맨 끝 「소 계」 묶음 = 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) → 끝수(합계 − 묶음 합 · 0 이면 줄 없음) → 합계(엔진 비목 합)
* 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` = SK 셈 답) · 소계가 없으면(결과 로직) 아무것도 안 그림
* M02 SK 묶음 = 상태 알약(더함 · 덮음 · 뺌) · 덮음 갈래 · 원래 금액 · 수량 · 더한 기초는 접힘 수식 블록 · 뺌 = 흐리게 · 금액 0
* 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`inline` · 줄 열쇠 = 블록 이름 · `createEqChain` 의 `after`) · 나머지(자기 자원 · 덧줄 · 더한 기초) · 맨 끝 소 계 · 끝수 · 합계는 `tail`
* 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 801-3 · 요구 655-1 · 661 · 662-3 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
* 묶음 = 부른 · 더한 단가산출 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄) 한 묶음 — 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` · 꼴 = `master_equation_group`) · 소계가 없으면(결과 로직) 아무것도 안 그림
* 묶음 하나뿐 = 머리 없음 · 노무비 · 재료비 · 경비 줄 → 「계」 한 줄(끝수가 있으면 끝수 줄 먼저)
* 묶음 둘 이상 = 묶음마다 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계 · 맨 끝 묶음 소계를 더한 식 → 끝수 → 합계(단가산출이 둘 이상) 아니면 계
* M02 SK = 상태 알약(더함 · 덮음) · 원래 금액 · 더한 기초는 접힘 수식 블록(더한 기초 하나뿐이어도 머리 줄은 둠 — 연결 끊기 단추 자리) · 합계 밖 줄은 글 한 줄
* 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`inline` · 줄 열쇠 = 블록 이름 · `createEqChain` 의 `after`) · 나머지(직접 줄 · 더한 기초) · 맨 끝 합계 · 끝수는 `tail`
* 쓰는 법 = `createEqSum(tree, open?, hooks?, lineKeys?)` → { inline, tail } | null — `lineKeys` = 수식 영역 호표 줄 열쇠들 · `open` = 펼친 블록 이름표 모음 ·
* `hooks` = 페이지가 더한 기초 줄 머리 단추(`head`) · 접힘 속(`body`)을 바꿔 끼움(796-2e 연결 끊기)
* ========================================================================== */
@@ -18,7 +18,6 @@ import { createEqChain } from "./ui_template_equation_line";
const COLS = ["노무비", "재료비", "경비", "계"] as const;
const PARTS = ["노무비", "재료비", "경비"] as const;
type Col = (typeof COLS)[number];
const KIND: Record<string, string> = { 자원: "자기 자원", 덧줄: "덧줄" };
const TONE: Record<string, BadgeTone> = { 더함: "success", 덮음: "warning", 뺌: "neutral" };
const ORDER = "가나다라마바사아자차카타파하";
@@ -66,17 +65,23 @@ const part = (label: string, text: string): HTMLElement => {
};
/** 기준 단가산출 묶음 하나 — 「가. 이름」 머리(+ 상태 · 접힘 수식) → 노 · 재 · 경 줄 → 소 계 */
function group(r: EqSumRow, open: Set<string>, at: number, hooks?: EqSumHooks): HTMLElement {
function group(
r: EqSumRow,
open: Set<string>,
at: number,
heads: boolean,
hooks?: EqSumHooks,
): HTMLElement {
const box = node("div", "ui-eq__grp");
box.dataset.eqSumItem = r.갈래;
box.dataset.eqSumState = r.상태 ?? "바탕";
const name = node("div", "ui-eq__grp-head");
const title = r.갈래 === r.이름 ? (KIND[r.갈래] ?? r.이름) : r.이름;
const title = r.이름;
const tree = r.나무;
const id = `sum:${at}:${r.이름}`;
const body = node("div", "ui-eq__sum-tree");
body.hidden = true;
name.append(node("span", "ui-eq__grp-mark", `${mark(at)}.`));
if (heads) name.append(node("span", "ui-eq__grp-mark", `${mark(at)}.`));
if (tree) {
const head = createButton({
label: title,
@@ -107,35 +112,32 @@ function group(r: EqSumRow, open: Set<string>, at: number, hooks?: EqSumHooks):
name.append(createBadge(`${r.상태}${detail}`, TONE[r.상태] ?? "neutral", { size: 18 }));
}
name.append(...(hooks?.head?.(r, at) ?? []));
const out = !!r.합밖;
if (out) name.append(createBadge("합 밖", "neutral", { size: 18 }));
const notes: string[] = [];
if (out) notes.push(`합계에 안 듦 ${eqText(r.밖금액) || "0"}`);
if (r.상태 === "더함" && r.수량 !== undefined && r.수량 !== null)
notes.push(`× ${eqText(r.수량)}`);
if (r.원래 !== undefined && r.원래 !== null && r.상태 !== "더함")
notes.push(`원래 ${eqText(r.원래)}`);
if (r.까닭) notes.push(r.까닭);
if (notes.length) name.append(node("span", "ui-eq__sum-was", notes.join(" · ")));
box.append(name, body);
const shown = heads || !!tree || notes.length > 0 || (!!r.상태 && r.상태 !== "바탕");
if (shown) box.append(name, body);
for (const k of PARTS) if (num(r[k]) !== 0) box.append(part(k, eqText(r[k])));
const total = row("소계", "소 계", (k) => r[k]);
total.classList.add("ui-eq__grp-total");
if (out) total.dataset.eqSumOut = "1";
box.append(total);
for (const x of r.낱줄 ?? []) {
const was = x.원래 !== undefined && x.원래 !== null ? ` · 원래 ${eqText(x.원래)}` : "";
box.append(part(`${x.이름} · ${x.상태}`, `${eqText(x.계) || "0"}${was}`));
}
if (heads) {
const total = row("소계", "소 계", (k) => r[k]);
total.classList.add("ui-eq__grp-total");
box.append(total);
}
return box;
}
/** 맨 끝 「소 계」 묶음 — 묶음 소계를 더한 식 */
function final(rows: EqSumRow[], at: number): HTMLElement {
/** 맨 끝 — 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) */
function final(rows: EqSumRow[]): HTMLElement {
const box = node("div", "ui-eq__grp");
box.dataset.eqSumItem = "소계";
const head = node("div", "ui-eq__grp-head");
head.append(
node("span", "ui-eq__grp-mark", `${mark(at)}.`),
node("span", "ui-eq__sum-label", "소 계"),
);
box.append(head);
box.dataset.eqSumItem = "합";
for (const k of PARTS) {
const adds = rows.map((r) => num(r[k])).filter((v) => v !== 0);
if (!adds.length) continue;
@@ -160,21 +162,27 @@ export function createEqSum(
lineKeys: string[] = [],
): EqSumParts | null {
const sub = tree.소계;
if (!sub?.줄.length) return null;
if (!sub) return null;
const inline = new Map<string, HTMLElement>();
const box = node("div", "ui-eq__sum");
box.dataset.eqSum = "";
const heads = sub.줄.length >= 2; // 묶음 하나뿐 = 머리 · 소계 없음
const word = sub.줄.filter((r) => r.갈래 === "블록").length >= 2 ? "합계" : "계"; // 합계 = 단가산출 둘 이상
sub.줄.forEach((r, at) => {
const one = group(r, open, at, hooks);
const one = group(r, open, at, heads, hooks);
if (r.갈래 === "블록" && lineKeys.includes(r.이름) && !inline.has(r.이름))
inline.set(r.이름, one);
else box.append(one);
});
box.append(final(sub.줄, sub.줄.length));
if (heads) box.append(final(sub.줄));
const rest = sub.끝수;
const odd = COLS.some((k) => Number(rest[k] ?? 0) !== 0);
if (odd) box.append(row("끝수", "끝수", (k) => rest[k]));
box.append(row("합계", "합계", (k) => tree.합계[k]));
box.append(row(word, word, (k) => tree.합계[k]));
for (const x of sub.밖 ?? [])
box.append(
node("div", "ui-eq__sum-note", `합계에 안 듦 · ${x.이름} ${eqText(x.밖금액) || "0"}`),
);
const note = [odd ? sub.끝수까닭 : "", tree.답까닭].filter(Boolean).join(" · ");
if (note) box.append(node("div", "ui-eq__sum-note", note));
return { inline, tail: box };