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

This commit is contained in:
2026-09-13 19:16:46 +09:00
11 changed files with 700 additions and 108 deletions
@@ -690,6 +690,11 @@ def build() -> dict[str, Any]:
node["tables"].append(entry) node["tables"].append(entry)
attached += 1 attached += 1
# 부모 모양(명세 2장) — 기본 choose_one, 합산형은 사람이 적은 것만.
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Parents import apply_parent_modes
apply_parent_modes(nodes)
src_meta = { src_meta = {
"dataset_id": data["dataset_id"], "dataset_id": data["dataset_id"],
"effective_date": data["effective_date"], "effective_date": data["effective_date"],
@@ -0,0 +1,70 @@
"""공종 마스터 — 부모 공종의 모양 `parent_mode` (명세 2장 · 2026-09-13 축 C Ⓐ).
아래 절이 있는 공종(부모)은 두 모양이고 규칙이 다르다.
choose_one 갈래 고르기형 — 잎 하나를 고른다. **부모에 금액을 붙이지 않는다.** (기본)
sum_steps 단계 합산형 — 내역 한 줄이 곧 부모이고, 일위대가는 **잎들의 합**으로 조립한다.
⚠ **자동 판정 금지** — 표 모양으로 짐작하면 조용히 틀린다. 합산형은 아래에 **사람이 적은 것**만.
⚠ 가중치(`weight`)도 사람이 적는다 — 원문 절 제목이 준 비율만(「발파 10%」·「깎기(90%)」).
"""
from __future__ import annotations
from typing import Any
PARENT_CHOOSE_ONE = "choose_one"
PARENT_SUM_STEPS = "sum_steps"
#: 단계 합산형 부모 — (잎 코드, 가중치) 차례와 근거. ⚠ 여기 없는 부모는 전부 `choose_one`.
SUM_STEPS: dict[str, dict[str, Any]] = {
"FP-09-04": {
"steps": (("FP-09-04-01", "1"), ("FP-09-04-02", "1")),
"basis": (
"품셈 9-4 암절취 = 9-4-1 암파쇄 + 9-4-2 집토 — 같은 ㎥ 를 깨고 모음"
"(지식DB 토공_수량 「암절취 (암파쇄ㆍ집토) | 9-4」)"
),
},
"FP-09-05": {
"steps": (("FP-09-05-01", "0.1"), ("FP-09-05-02", "0.9"), ("FP-09-05-03", "1")),
"basis": (
"품셈 9-5 발파암 — 절 제목 「9-5-1 육상, 암석 절취(발파 10%)」·「9-5-2 깎기(90%)」·"
"「9-5-3 집토」(지식DB 토공_수량 「발파암 (절취 10 %ㆍ깎기 90 %ㆍ집토) | 9-5」)"
),
},
"FP-12-38": {
"steps": (("FP-12-38-02", "1"), ("FP-12-38-03", "1")),
"basis": (
"품셈 12-38 유로폼 = 12-38-2 사용수량(자재) + 12-38-3 설치 및 해체(품) — "
"12-38-1 사용횟수는 금액 단계가 아니라 사용수량의 잔존율 조건"
),
},
}
def apply_parent_modes(nodes: list[dict[str, Any]]) -> None:
"""부모 공종에 `parent_mode`(합산형이면 `steps`·`steps_basis` 까지)를 단다 — 자리에서.
⚠ 선언한 잎이 그 부모의 자식이 아니면 멈춘다 — 판이 바뀌어 절 번호가 옮겨 간 자리다.
"""
children: dict[str, list[str]] = {}
for node in nodes:
if node.get("parent_code"):
children.setdefault(node["parent_code"], []).append(node["work_item_code"])
for node in nodes:
code = node["work_item_code"]
if code not in children:
continue
declared = SUM_STEPS.get(code)
if declared is None:
node["parent_mode"] = PARENT_CHOOSE_ONE
continue
strays = [step for step, _ in declared["steps"] if step not in children[code]]
if strays:
raise ValueError(f"{code} 합산 단계가 자식이 아닙니다: {strays}")
node["parent_mode"] = PARENT_SUM_STEPS
node["steps"] = [{"code": step, "weight": weight} for step, weight in declared["steps"]]
node["steps_basis"] = declared["basis"]
missing = [code for code in SUM_STEPS if code not in children]
if missing:
raise ValueError(f"합산형으로 적은 부모가 마스터에 없습니다: {missing}")
@@ -177,8 +177,11 @@ def _earthwork_rows(
# 토공 줄은 대개 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양). # 토공 줄은 대개 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
# 매핑이 갈래를 적은 작업 갈래(잡관목제거 → 단목베기 「5m 미만」)만 값을 싣는다. # 매핑이 갈래를 적은 작업 갈래(잡관목제거 → 단목베기 「5m 미만」)만 값을 싣는다.
"variant_axis": (entry or {}).get("variant_axis"), "variant_axis": (entry or {}).get("variant_axis"),
# 암 갈래(연암·보통암·경암)는 줄 자신의 갈래를 넘김 — 9-4·9-5 단계 합산형(축 C Ⓐ).
"variant_value": (entry or {}).get("variant_value") "variant_value": (entry or {}).get("variant_value")
or (variant_inputs or {}).get(str((entry or {}).get("variant_from") or "")) or {**(variant_inputs or {}), "ground_class": ground}.get(
str((entry or {}).get("variant_from") or "")
)
or None, or None,
"secondary_axes": None, "secondary_axes": None,
"spec_class": None, "spec_class": None,
@@ -203,13 +203,28 @@ def _leaf_row(
return row return row
price_code = f"B-{node.code}" price_code = f"B-{node.code}"
if item.variant_value and price_code not in unit_prices.book.titles: if price_code not in unit_prices.book.titles:
# B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다. # B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다.
# 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다. # 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다.
picked = find_variant_code(node.code, item.variant_value, unit_prices) picked = (
find_variant_code(node.code, item.variant_value, unit_prices)
if item.variant_value
else None
)
default = unit_prices.default_variants.get(node.code)
if picked is not None: if picked is not None:
price_code = picked price_code = picked
row.spec = f"{row.spec} {item.variant_value}".strip() row.spec = f"{row.spec} {item.variant_value}".strip()
elif default is not None:
# 표에 없거나 안 준 암질(풍화암·암) — **원문이 정한 갈래**로만 선다(9-4-1 [주]① 평균).
price_code = f"{price_code}#{default[0]}"
row.spec = f"{row.spec} {default[0]}".strip()
given = (
f"{item.variant_value}」은 표에 없는 갈래"
if item.variant_value
else "갈래 미지정"
)
row.add_note("spec", f"{given}{default[1]}")
if price_code not in unit_prices.book.titles: if price_code not in unit_prices.book.titles:
# 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다 # 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다
@@ -225,16 +240,35 @@ def _leaf_row(
) )
or code.startswith(f"{price_code}#") or code.startswith(f"{price_code}#")
) )
if children: mode = unit_prices.parent_modes.get(node.code)
if mode == "sum_steps" and node.code in unit_prices.component_gaps:
# 단계 합산형 — 부모가 곧 내역 줄인데 **어느 단계가 못 서서** 조립이 안 됐다.
gap = unit_prices.component_gaps[node.code]
row.add_note("unit_price_krw", f"단계 합산 단가를 못 세웠습니다 — {gap}")
reason = f"단계 합산 미완 — {gap}"
elif children and mode == "sum_steps":
names = ", ".join(c[2:] for c in children if c.startswith(f"{price_code}#"))
row.add_note(
"unit_price_krw",
f"갈래 「{item.variant_value or '미지정'}」에 맞는 단계 합산 단가가 없습니다"
f" — 있는 갈래: {names}",
)
reason = f"단계 합산 갈래 미일치 — {item.variant_value or '미지정'}"
elif mode == "choose_one":
# ⚠ 갈래 고르기형 부모 — **부모에 금액을 붙이지 않는다**(명세 2장). 조용히 0 원이 되지 않게 막는다.
names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children)
row.add_note(
"unit_price_krw",
"갈래 고르기형 부모 공종이라 잎 하나를 골라야 합니다 — "
+ (f"후보: {names}" if names else "잎 공종 일위대가도 아직 없습니다"),
)
reason = f"갈래 고르기형 부모 — 잎 미선택(후보 {len(children)}건)"
elif children:
names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children) names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children)
row.add_note( row.add_note(
"unit_price_krw", "unit_price_krw",
f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}", f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}",
) )
# 관경이 표 밖이면 **무엇을 정해야 하는지**까지 가리킨다.
diameter_note = pipe_diameter_note(node.code, item.variant_value)
if diameter_note:
row.add_note("spec", diameter_note)
reason = f"일위대가가 하위 공종에 있음(후보 {len(children)}건)" reason = f"일위대가가 하위 공종에 있음(후보 {len(children)}건)"
else: else:
# ⚠ 「아직 안 만든 것」과 「성분이 빠져 못 세운 것」은 **할 일이 다르다**. # ⚠ 「아직 안 만든 것」과 「성분이 빠져 못 세운 것」은 **할 일이 다르다**.
@@ -249,6 +283,10 @@ def _leaf_row(
"unit_price_krw", "일위대가가 아직 없습니다 — 금액을 0 으로 때우지 않습니다." "unit_price_krw", "일위대가가 아직 없습니다 — 금액을 0 으로 때우지 않습니다."
) )
reason = "일위대가 없음" reason = "일위대가 없음"
# 관경이 표 밖이면 **무엇을 정해야 하는지**까지 가리킨다.
diameter_note = pipe_diameter_note(node.code, item.variant_value) if children else ""
if diameter_note:
row.add_note("spec", diameter_note)
result.missing.append( result.missing.append(
{ {
"name": row.name, "name": row.name,
@@ -219,6 +219,25 @@ MACHINE_CHOICES: dict[str, dict[str, Any]] = {
"(2026-09-09 밤에 드러남).", "(2026-09-09 밤에 드러남).",
], ],
}, },
# 암절취·발파암의 집토 — 흙깎기와 같이 기종이 [주] 에만 있다(2026-09-13 축 C Ⓐ, 단계 합산형 부모의 잎).
"FP-09-04-02": {
"work_item_name": "암절취 집토",
"default_code": "0201-0070",
"source": "note",
"basis": [
"산림사업 표준품셈 9-4-2 [주]① 「장비는 무한궤도 굴착기(0.7㎥)를 적용한다」 — "
"표가 아니라 [주] 에 있어 공종 마스터가 못 싣는 값입니다.",
],
},
"FP-09-05-03": {
"work_item_name": "발파암 집토",
"default_code": "0201-0070",
"source": "note",
"basis": [
"산림사업 표준품셈 9-5-3 [주]① 「장비는 무한궤도 굴착기(0.7㎥)를 적용한다」 — "
"표가 아니라 [주] 에 있어 공종 마스터가 못 싣는 값입니다.",
],
},
"FP-09-18": { "FP-09-18": {
"work_item_name": "층따기", "work_item_name": "층따기",
"default_code": "0201-0070", "default_code": "0201-0070",
@@ -0,0 +1,218 @@
"""B09 원가계산 — **단계 합산형 부모**를 잎들의 합으로 조립한다 (명세 2장 · 2026-09-13 축 C Ⓐ).
공종 마스터가 부모마다 `parent_mode` 단다(`B08_Quantity_Build_WorkItemMaster_Parents`).
choose_one 갈래 고르기형 **여기서 세운다.** 내역 줄이 부모를 가리키면 오류로 드러냄
sum_steps 단계 합산형 B-<부모>[#갈래] = Σ B-<잎>[#갈래] × 가중치
흙깎기(리핑암) = 9-4 암절취 = 9-4-1 암파쇄 + 9-4-2 집토 사례다 부모에 일위대가 제목이
아예 없어 금액 0 이던 자리(명세 2 실측).
**암질 갈래** 암파쇄(9-4-1)·깎기(9-5-2) 표는 연암·보통암·경암마다 작업능력 `Q`(/hr) 준다.
갈래마다 따로 세우고, 부모도 갈래로 선다. 내역 줄의 암질이 표에 없으면(풍화암· )
**원문이 평균을 정한 잎만** 평균으로 선다(9-4-1 []). 9-5-2 [] 없어 막는다.
모르는 값을 지어내지 않는다 단계 하나라도 서면 부모를 세우고 **어느 단계가 ** 남긴다.
"""
from __future__ import annotations
import re
from decimal import Decimal
from typing import Any
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle
ROCK_CLASSES = ("연암", "보통암", "경암")
#: 암질을 표에 없는 이름으로 준 줄이 쓸 갈래 — **원문이 평균을 정한 잎만**.
AVERAGE_VARIANT = "평균"
AVERAGE_BASIS: dict[str, str] = {
"FP-09-04-01": "품셈 9-4-1 [주]① 「Q(시간당 작업량 ㎥/hr) =(5.0+3.4+2.6)/3」 — 세 암질 평균",
}
#: 표에 기계 이름이 없는 잎 — [주]가 기종을 적은 자리(마스터가 [주]를 못 싣는다). 원문 그대로.
#: ⚠ 부모(9-5)에서 물려받을 기계는 없다 — 부모엔 표가 없고, 잎의 [주]가 스스로 적었다.
NOTE_MACHINES: dict[str, tuple[tuple[str, ...], str]] = {
"FP-09-05-02": (
("0201-0070", "0230-0007"),
"품셈 9-5-2 [주] 「장비는 대형브레이커와 무한궤도 굴착기(0.7㎥)를 적용한다」",
),
}
def _tight(text: Any) -> str:
return re.sub(r"\s", "", str(text or ""))
def rock_rows(node: dict[str, Any]) -> list[tuple[str, Decimal, Decimal | None]]:
"""(암질, 작업능력 Q, 치즐소모량) — 암질 이름 칸 **바로 뒤** 두 칸을 읽는다."""
from B09_Estimation.B09_Estimation_MachineProductivity import parse_measure
found: list[tuple[str, Decimal, Decimal | None]] = []
for table in node.get("tables", []):
for row in table.get("raw_row") or []:
cells = [str(cell) for cell in row]
index = next((i for i, c in enumerate(cells) if _tight(c) in ROCK_CLASSES), None)
if index is None:
continue
tail = [parse_measure(c) for c in cells[index + 1 : index + 3]] + [None, None]
if tail[0] is not None and tail[0] > 0:
found.append((_tight(cells[index]), tail[0], tail[1]))
return found
def leaf_machines(node: dict[str, Any]) -> tuple[list[tuple[str, str]], str]:
"""(기종 코드·이름 목록, 출처) — 표의 조합 칸(「대형브레이커+ 유압식백호우 …」)이 먼저."""
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
from B09_Estimation.B09_Estimation_MachineProductivity import resolve_machine
from B09_Estimation.B09_Estimation_MachineProductivity_Reference import _machine_by_name
catalog = load_machine_catalog()
for table in node.get("tables", []):
for row in table.get("raw_row") or []:
for cell in row:
parts = [part.strip() for part in str(cell).split("+")]
if len(parts) < 2:
continue
bodies = [resolve_machine(part) for part in parts]
body = next((b for b in bodies if b is not None), None)
if body is None:
continue
spec = str(catalog.machines[body[0]].specification)
picked = [b or _machine_by_name(p, spec) for b, p in zip(bodies, parts)]
if all(picked):
return [
p for p in picked if p
], f"표의 조합 칸 「{' '.join(str(cell).split())}"
code = str(node.get("work_item_code") or "")
if code in NOTE_MACHINES:
codes, source = NOTE_MACHINES[code]
return [(c, catalog.machines[c].name) for c in codes if c in catalog.machines], source
return [], ""
def _add_rock_leaf(build: Any, node: dict[str, Any]) -> None:
"""암질 갈래마다 잎 제목을 세운다 — 기계마다 1/Q 시간."""
code = str(node["work_item_code"])
rows = rock_rows(node)
if not rows or any(title.startswith(f"B-{code}#") for title in build.book.titles):
return
machines, source = leaf_machines(node)
missing = [f"X-{m}" for m, _ in machines if f"X-{m}" not in build.book.titles]
if not machines or missing:
build.component_gaps[code] = (
f"작업능력 표의 기종을 못 골랐습니다 — {source or '표·[주]에 기종 없음'}"
+ (f" (기계 단가 층 없음: {', '.join(missing)})" if missing else "")
)
return
variants = [(rock, q, f"작업능력 Q = {q} ㎥/hr ({rock})") for rock, q, _ in rows]
if code in AVERAGE_BASIS:
average = sum((q for _, q, _ in rows), Decimal(0)) / Decimal(len(rows))
variants.append(
(AVERAGE_VARIANT, average, f"Q = {average:.4f} ㎥/hr — {AVERAGE_BASIS[code]}")
)
for variant, capacity, note in variants:
title_code = f"B-{code}#{variant}"
build.book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{node.get('name', code)} ({variant})",
spec=variant,
unit="",
)
)
for machine_code, _name in machines:
build.book.add_detail(
PriceDetail(
title_code,
f"X-{machine_code}",
Decimal(1) / capacity,
note=f"{note} · {source}",
)
)
build.variants.setdefault(code, []).append(variant)
# 표 줄은 다 읽었다 — 남는 것은 치즐뿐(자재 카탈로그가 없어 금액에 안 붙는 알려진 미결).
build.unattached[code] = ["치즐소모량(본/hr) — 자재 단가 층 없음"] if rows[0][2] else []
def _step_titles(build: Any, step: str) -> dict[str, str]:
"""갈래 → 그 단계의 제목 코드. 갈래 없는 제목은 빈 문자열 키."""
titles = {"": f"B-{step}"} if f"B-{step}" in build.book.titles else {}
for code in build.book.titles:
if code.startswith(f"B-{step}#"):
titles[code.split("#", 1)[1]] = code
return titles
def _assemble(build: Any, node: dict[str, Any], names: dict[str, str]) -> None:
parent = str(node["work_item_code"])
steps = [(str(s["code"]), Decimal(str(s["weight"]))) for s in node.get("steps") or []]
per_step: list[tuple[str, Decimal, dict[str, str]]] = []
for step, weight in steps:
label = f"단계 {step} {names.get(step, '')}".strip()
titles = _step_titles(build, step)
if not titles:
why = build.component_gaps.get(step) or "일위대가가 없습니다"
build.component_gaps[parent] = f"{label} 이 안 섰습니다 — {why}"
return
if step in build.partial_ratio or step in build.basis_missing:
why = build.component_gaps.get(step) or "일부 몫만 섰습니다"
build.component_gaps[parent] = f"{label} 이 일부만 섰습니다 — {why}"
return
per_step.append((step, weight, titles))
varying = [item for item in per_step if set(item[2]) - {""}]
if len(varying) > 1:
build.component_gaps[parent] = "갈래가 두 단계 이상에 걸쳐 조립하지 않았습니다"
return
units = {build.book.titles[t].unit for _, _, ts in per_step for t in ts.values()} - {""}
if len(units) > 1:
build.component_gaps[parent] = f"단계 단위가 서로 다릅니다 — {', '.join(sorted(units))}"
return
variants = [v for v in varying[0][2] if v] if varying else [""]
for variant in variants:
title_code = f"B-{parent}" + (f"#{variant}" if variant else "")
build.book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{names.get(parent, parent)} ({variant})"
if variant
else names.get(parent, parent),
spec=variant or parent,
unit=next(iter(units), ""),
)
)
for step, weight, titles in per_step:
ref = titles.get(variant) or titles[""]
build.book.add_detail(
PriceDetail(
title_code,
ref,
weight,
note=f"단계 합산 × {weight}{node.get('steps_basis', '')}",
)
)
if variant:
build.variants.setdefault(parent, []).append(variant)
if varying and AVERAGE_VARIANT in varying[0][2] and varying[0][0] in AVERAGE_BASIS:
build.default_variants[parent] = (AVERAGE_VARIANT, AVERAGE_BASIS[varying[0][0]])
build.component_gaps.pop(parent, None)
def attach_parent_steps(build: Any, master: dict[str, Any]) -> None:
"""부모 모양을 싣고, 합산형 부모를 조립한다 — `build_unit_prices` 끝(조합 16% 바꿔 달기 전)."""
nodes = master.get("work_items", [])
names = {str(n.get("work_item_code")): str(n.get("name", "")) for n in nodes}
by_code = {str(n.get("work_item_code")): n for n in nodes}
for node in nodes:
mode = node.get("parent_mode")
if mode:
build.parent_modes[str(node["work_item_code"])] = str(mode)
if mode != "sum_steps":
continue
for step in node.get("steps") or []:
leaf = by_code.get(str(step["code"]))
if leaf is not None:
_add_rock_leaf(build, leaf)
_assemble(build, node, names)
@@ -129,6 +129,10 @@ class UnitPriceBuild:
#: ⚠ 금액을 막지 않는다. 「표본이 얇다」는 사실을 화면이 말하게 하는 통로다 #: ⚠ 금액을 막지 않는다. 「표본이 얇다」는 사실을 화면이 말하게 하는 통로다
#: (지식DB `노임단가_적용 §2-3` 「단가 채택 시 플래그 유지 필요」, 2026-09-09에 이음). #: (지식DB `노임단가_적용 §2-3` 「단가 채택 시 플래그 유지 필요」, 2026-09-09에 이음).
labor_reliability: dict[str, tuple[str, str, str]] = field(default_factory=dict) labor_reliability: dict[str, tuple[str, str, str]] = field(default_factory=dict)
#: 부모 공종 → 모양(`choose_one`·`sum_steps`) — 공종 마스터가 단 그대로(명세 2장).
parent_modes: dict[str, str] = field(default_factory=dict)
#: 합산형 부모 → (갈래, 근거) — 내역 줄의 갈래가 표에 없을 때 **원문이 정한** 갈래(9-4-1 [주]① 평균).
default_variants: dict[str, tuple[str, str]] = field(default_factory=dict)
def _add_labor_titles(book: PriceBook, wages: dict[str, Decimal]) -> None: def _add_labor_titles(book: PriceBook, wages: dict[str, Decimal]) -> None:
@@ -921,6 +925,11 @@ def build_unit_prices(
covered += machine_share covered += machine_share
if covered < Decimal(100): if covered < Decimal(100):
build.partial_ratio[work_item_code] = covered build.partial_ratio[work_item_code] = covered
# 단계 합산형 부모(9-4 암절취 = 암파쇄 + 집토) — 잎이 다 선 뒤, 조합 16% 바꿔 달기 전(명세 2장).
from B09_Estimation.B09_Estimation_ParentSteps import attach_parent_steps
attach_parent_steps(build, master)
# 기계 수송비 — 기계경비의 셋째 몫(손료·운전경비·**수송비**). 거리가 있어야 선다. # 기계 수송비 — 기계경비의 셋째 몫(손료·운전경비·**수송비**). 거리가 있어야 선다.
from B09_Estimation.B09_Estimation_Transport import attach_transport from B09_Estimation.B09_Estimation_Transport import attach_transport
@@ -28,7 +28,10 @@
"work_item_code": "FP-09-04", "work_item_code": "FP-09-04",
"basis_unit": "㎥", "basis_unit": "㎥",
"basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L4725·L4738 「Q(시간당 작업량 ㎥/hr)」·「Q=…= ㎥/시간」 — 절 머리에 「(단위: …)」가 없어 마스터가 못 채운 자리(2026-09-08 원문 대조).", "basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L4725·L4738 「Q(시간당 작업량 ㎥/hr)」·「Q=…= ㎥/시간」 — 절 머리에 「(단위: …)」가 없어 마스터가 못 채운 자리(2026-09-08 원문 대조).",
"master_name": "암절취" "master_name": "암절취",
"variant_axis": "ground_class",
"variant_from": "ground_class",
"variant_note": "2026-09-13 축 C Ⓐ — 단계 합산형 부모(9-4·9-5). 암파쇄·깎기 표가 연암·보통암·경암마다 작업능력을 줘 암 갈래 이름을 갈래 값으로 넘김(표에 없는 이름의 처리는 B09 몫)."
}, },
{ {
"group": "흙깎기", "group": "흙깎기",
@@ -36,7 +39,10 @@
"work_item_code": "FP-09-05", "work_item_code": "FP-09-05",
"basis_unit": "㎥", "basis_unit": "㎥",
"basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L4744 「(단위: ㎥당)」 · L4777 「Q=…= ㎥/시간」 — 절 머리에 「(단위: …)」가 없어 마스터가 못 채운 자리(2026-09-08 원문 대조).", "basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L4744 「(단위: ㎥당)」 · L4777 「Q=…= ㎥/시간」 — 절 머리에 「(단위: …)」가 없어 마스터가 못 채운 자리(2026-09-08 원문 대조).",
"master_name": "발파암" "master_name": "발파암",
"variant_axis": "ground_class",
"variant_from": "ground_class",
"variant_note": "2026-09-13 축 C Ⓐ — 단계 합산형 부모(9-4·9-5). 암파쇄·깎기 표가 연암·보통암·경암마다 작업능력을 줘 암 갈래 이름을 갈래 값으로 넘김(표에 없는 이름의 처리는 B09 몫)."
}, },
{ {
"group": "측구터파기", "group": "측구터파기",
@@ -1,7 +1,7 @@
{ {
"schema_version": "1.0", "schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest", "dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-13T17:55:11+09:00", "generated_at": "2026-09-13T18:54:39+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py", "built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": { "source": {
"dataset_id": "pum_forest", "dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [ "files": [
{ {
"file": "work_item_master_2026-01-01.json", "file": "work_item_master_2026-01-01.json",
"sha256": "43b6d48175847e1d4011aad4a0597313008f791ec7eba0429d39d279e3e1fcb3", "sha256": "11c49ecde0a611307a82fd98b3625ee6577c01c2f77db6e1f515b41bf83ce685",
"size_bytes": 856831 "size_bytes": 861050
}, },
{ {
"file": "form_undetermined_2026-01-01.json", "file": "form_undetermined_2026-01-01.json",
@@ -2,7 +2,7 @@
"schema_version": "1.0", "schema_version": "1.0",
"dataset_id": "work_item_master_forest", "dataset_id": "work_item_master_forest",
"effective_date": "2026-01-01", "effective_date": "2026-01-01",
"generated_at": "2026-09-13T17:55:11+09:00", "generated_at": "2026-09-13T18:54:39+09:00",
"dataset_version": { "dataset_version": {
"dataset_id": "pum_forest", "dataset_id": "pum_forest",
"effective_date": "2026-01-01", "effective_date": "2026-01-01",
@@ -111,7 +111,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 256, "sort_order": 256,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-01", "work_item_code": "FP-01-01",
@@ -120,7 +121,8 @@
"level": 2, "level": 2,
"parent_code": "FP-01", "parent_code": "FP-01",
"sort_order": 512, "sort_order": 512,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-01-01", "work_item_code": "FP-01-01-01",
@@ -233,7 +235,8 @@
"level": 2, "level": 2,
"parent_code": "FP-01", "parent_code": "FP-01",
"sort_order": 1536, "sort_order": 1536,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-02-01", "work_item_code": "FP-01-02-01",
@@ -1790,7 +1793,8 @@
"level": 2, "level": 2,
"parent_code": "FP-01", "parent_code": "FP-01",
"sort_order": 3840, "sort_order": 3840,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-03-01", "work_item_code": "FP-01-03-01",
@@ -2167,7 +2171,8 @@
"level": 2, "level": 2,
"parent_code": "FP-01", "parent_code": "FP-01",
"sort_order": 4608, "sort_order": 4608,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-04-01", "work_item_code": "FP-01-04-01",
@@ -3985,7 +3990,8 @@
"level": 2, "level": 2,
"parent_code": "FP-01", "parent_code": "FP-01",
"sort_order": 12032, "sort_order": 12032,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-01-07-01", "work_item_code": "FP-01-07-01",
@@ -4143,7 +4149,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 13312, "sort_order": 13312,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-02-01", "work_item_code": "FP-02-01",
@@ -4519,7 +4526,8 @@
] ]
] ]
} }
] ],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-02-01-01", "work_item_code": "FP-02-01-01",
@@ -6086,7 +6094,8 @@
] ]
] ]
} }
] ],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-02-02-01", "work_item_code": "FP-02-02-01",
@@ -6747,7 +6756,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 20224, "sort_order": 20224,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-03-01", "work_item_code": "FP-03-01",
@@ -9812,7 +9822,8 @@
"level": 2, "level": 2,
"parent_code": "FP-03", "parent_code": "FP-03",
"sort_order": 21248, "sort_order": 21248,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-03-04-01", "work_item_code": "FP-03-04-01",
@@ -10231,7 +10242,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 23296, "sort_order": 23296,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-04-01", "work_item_code": "FP-04-01",
@@ -11718,7 +11730,8 @@
] ]
] ]
} }
] ],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-04-01-01", "work_item_code": "FP-04-01-01",
@@ -12684,7 +12697,8 @@
] ]
] ]
} }
] ],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-04-02-01", "work_item_code": "FP-04-02-01",
@@ -13144,7 +13158,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 26112, "sort_order": 26112,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-01", "work_item_code": "FP-05-01",
@@ -13153,7 +13168,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 26368, "sort_order": 26368,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-01-01", "work_item_code": "FP-05-01-01",
@@ -13860,7 +13876,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 28160, "sort_order": 28160,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-03-01", "work_item_code": "FP-05-03-01",
@@ -15328,7 +15345,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 32768, "sort_order": 32768,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-16-01", "work_item_code": "FP-05-16-01",
@@ -15488,7 +15506,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 33536, "sort_order": 33536,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-17-01", "work_item_code": "FP-05-17-01",
@@ -15716,7 +15735,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 34560, "sort_order": 34560,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-19-01", "work_item_code": "FP-05-19-01",
@@ -15964,7 +15984,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 35840, "sort_order": 35840,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-22-01", "work_item_code": "FP-05-22-01",
@@ -16142,7 +16163,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 37120, "sort_order": 37120,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-23-01", "work_item_code": "FP-05-23-01",
@@ -16257,7 +16279,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 38144, "sort_order": 38144,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-24-01", "work_item_code": "FP-05-24-01",
@@ -16789,7 +16812,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 39168, "sort_order": 39168,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-26-01", "work_item_code": "FP-05-26-01",
@@ -17050,7 +17074,8 @@
"level": 2, "level": 2,
"parent_code": "FP-05", "parent_code": "FP-05",
"sort_order": 40448, "sort_order": 40448,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-05-28-01", "work_item_code": "FP-05-28-01",
@@ -17475,7 +17500,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 42752, "sort_order": 42752,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-06-01", "work_item_code": "FP-06-01",
@@ -17539,7 +17565,8 @@
"level": 2, "level": 2,
"parent_code": "FP-06", "parent_code": "FP-06",
"sort_order": 43264, "sort_order": 43264,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-06-02-01", "work_item_code": "FP-06-02-01",
@@ -17788,7 +17815,8 @@
"level": 2, "level": 2,
"parent_code": "FP-06", "parent_code": "FP-06",
"sort_order": 44544, "sort_order": 44544,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-06-04-01", "work_item_code": "FP-06-04-01",
@@ -18286,7 +18314,8 @@
"level": 2, "level": 2,
"parent_code": "FP-06", "parent_code": "FP-06",
"sort_order": 46336, "sort_order": 46336,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-06-07-01", "work_item_code": "FP-06-07-01",
@@ -18529,7 +18558,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 47616, "sort_order": 47616,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-01", "work_item_code": "FP-07-01",
@@ -18538,7 +18568,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 47872, "sort_order": 47872,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-01-01", "work_item_code": "FP-07-01-01",
@@ -19103,7 +19134,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 49152, "sort_order": 49152,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-04-01", "work_item_code": "FP-07-04-01",
@@ -19305,7 +19337,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 49920, "sort_order": 49920,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-05-01", "work_item_code": "FP-07-05-01",
@@ -19585,7 +19618,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 50944, "sort_order": 50944,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-07-01", "work_item_code": "FP-07-07-01",
@@ -19817,7 +19851,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 51712, "sort_order": 51712,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-08-01", "work_item_code": "FP-07-08-01",
@@ -20102,7 +20137,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 52736, "sort_order": 52736,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-09-01", "work_item_code": "FP-07-09-01",
@@ -20945,7 +20981,8 @@
"level": 2, "level": 2,
"parent_code": "FP-07", "parent_code": "FP-07",
"sort_order": 54784, "sort_order": 54784,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-07-15-01", "work_item_code": "FP-07-15-01",
@@ -21370,7 +21407,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 56320, "sort_order": 56320,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-08-01", "work_item_code": "FP-08-01",
@@ -21379,7 +21417,8 @@
"level": 2, "level": 2,
"parent_code": "FP-08", "parent_code": "FP-08",
"sort_order": 56576, "sort_order": 56576,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-08-01-01", "work_item_code": "FP-08-01-01",
@@ -21563,7 +21602,8 @@
"level": 2, "level": 2,
"parent_code": "FP-08", "parent_code": "FP-08",
"sort_order": 57344, "sort_order": 57344,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-08-02-01", "work_item_code": "FP-08-02-01",
@@ -26590,7 +26630,8 @@
"level": 2, "level": 2,
"parent_code": "FP-08", "parent_code": "FP-08",
"sort_order": 59648, "sort_order": 59648,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-08-06-01", "work_item_code": "FP-08-06-01",
@@ -27436,7 +27477,8 @@
"level": 2, "level": 2,
"parent_code": "FP-08", "parent_code": "FP-08",
"sort_order": 60928, "sort_order": 60928,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-08-08-01", "work_item_code": "FP-08-08-01",
@@ -27762,7 +27804,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 62464, "sort_order": 62464,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-01", "work_item_code": "FP-09-01",
@@ -27823,7 +27866,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 63232, "sort_order": 63232,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-03-01", "work_item_code": "FP-09-03-01",
@@ -27949,7 +27993,19 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 64000, "sort_order": 64000,
"tables": [] "tables": [],
"parent_mode": "sum_steps",
"steps": [
{
"code": "FP-09-04-01",
"weight": "1"
},
{
"code": "FP-09-04-02",
"weight": "1"
}
],
"steps_basis": "품셈 9-4 암절취 = 9-4-1 암파쇄 + 9-4-2 집토 — 같은 ㎥ 를 깨고 모음(지식DB 토공_수량 「암절취 (암파쇄ㆍ집토) | 9-4」)"
}, },
{ {
"work_item_code": "FP-09-04-01", "work_item_code": "FP-09-04-01",
@@ -28089,7 +28145,23 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 64768, "sort_order": 64768,
"tables": [] "tables": [],
"parent_mode": "sum_steps",
"steps": [
{
"code": "FP-09-05-01",
"weight": "0.1"
},
{
"code": "FP-09-05-02",
"weight": "0.9"
},
{
"code": "FP-09-05-03",
"weight": "1"
}
],
"steps_basis": "품셈 9-5 발파암 — 절 제목 「9-5-1 육상, 암석 절취(발파 10%)」·「9-5-2 깎기(90%)」·「9-5-3 집토」(지식DB 토공_수량 「발파암 (절취 10 %ㆍ깎기 90 %ㆍ집토) | 9-5」)"
}, },
{ {
"work_item_code": "FP-09-05-01", "work_item_code": "FP-09-05-01",
@@ -28340,7 +28412,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 65792, "sort_order": 65792,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-06-01", "work_item_code": "FP-09-06-01",
@@ -28394,7 +28467,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 66304, "sort_order": 66304,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-07-01", "work_item_code": "FP-09-07-01",
@@ -28501,7 +28575,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 67072, "sort_order": 67072,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-08-01", "work_item_code": "FP-09-08-01",
@@ -28731,7 +28806,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 68096, "sort_order": 68096,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-10-01", "work_item_code": "FP-09-10-01",
@@ -28832,7 +28908,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 68864, "sort_order": 68864,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-11-01", "work_item_code": "FP-09-11-01",
@@ -28936,7 +29013,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 69376, "sort_order": 69376,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-12-01", "work_item_code": "FP-09-12-01",
@@ -29242,7 +29320,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 70400, "sort_order": 70400,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-13-01", "work_item_code": "FP-09-13-01",
@@ -30742,7 +30821,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 75264, "sort_order": 75264,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-14-01", "work_item_code": "FP-09-14-01",
@@ -30913,7 +30993,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 76032, "sort_order": 76032,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-15-01", "work_item_code": "FP-09-15-01",
@@ -31086,7 +31167,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 76800, "sort_order": 76800,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-16-01", "work_item_code": "FP-09-16-01",
@@ -31303,7 +31385,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 77824, "sort_order": 77824,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-17-01", "work_item_code": "FP-09-17-01",
@@ -31483,7 +31566,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 78848, "sort_order": 78848,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-19-01", "work_item_code": "FP-09-19-01",
@@ -31767,7 +31851,8 @@
"level": 2, "level": 2,
"parent_code": "FP-09", "parent_code": "FP-09",
"sort_order": 79872, "sort_order": 79872,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-09-20-01", "work_item_code": "FP-09-20-01",
@@ -32032,7 +32117,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 81152, "sort_order": 81152,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-01", "work_item_code": "FP-10-01",
@@ -32139,7 +32225,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 81920, "sort_order": 81920,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-03-01", "work_item_code": "FP-10-03-01",
@@ -32452,7 +32539,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 83456, "sort_order": 83456,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-06-01", "work_item_code": "FP-10-06-01",
@@ -32703,7 +32791,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 84480, "sort_order": 84480,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-07-01", "work_item_code": "FP-10-07-01",
@@ -33141,7 +33230,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 85760, "sort_order": 85760,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-08-01", "work_item_code": "FP-10-08-01",
@@ -33351,7 +33441,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 86784, "sort_order": 86784,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-09-01", "work_item_code": "FP-10-09-01",
@@ -33471,7 +33562,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 87296, "sort_order": 87296,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-10-01", "work_item_code": "FP-10-10-01",
@@ -33727,7 +33819,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 88320, "sort_order": 88320,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-12-01", "work_item_code": "FP-10-12-01",
@@ -33890,7 +33983,8 @@
"level": 2, "level": 2,
"parent_code": "FP-10", "parent_code": "FP-10",
"sort_order": 89344, "sort_order": 89344,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-10-13-01", "work_item_code": "FP-10-13-01",
@@ -34054,7 +34148,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 90112, "sort_order": 90112,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-11-01", "work_item_code": "FP-11-01",
@@ -34310,7 +34405,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 91392, "sort_order": 91392,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-01", "work_item_code": "FP-12-01",
@@ -34319,7 +34415,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 91648, "sort_order": 91648,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-01-01", "work_item_code": "FP-12-01-01",
@@ -35377,7 +35474,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 93952, "sort_order": 93952,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-07-01", "work_item_code": "FP-12-07-01",
@@ -35571,7 +35669,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 94976, "sort_order": 94976,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-09-01", "work_item_code": "FP-12-09-01",
@@ -35938,7 +36037,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 96256, "sort_order": 96256,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-11-01", "work_item_code": "FP-12-11-01",
@@ -36919,7 +37019,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 98560, "sort_order": 98560,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-17-01", "work_item_code": "FP-12-17-01",
@@ -37523,7 +37624,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 101120, "sort_order": 101120,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-24-01", "work_item_code": "FP-12-24-01",
@@ -37845,7 +37947,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 102400, "sort_order": 102400,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-24-01", "work_item_code": "FP-12-24-01",
@@ -38211,7 +38314,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 103936, "sort_order": 103936,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-30-01", "work_item_code": "FP-12-30-01",
@@ -38574,7 +38678,8 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 105472, "sort_order": 105472,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-12-34-01", "work_item_code": "FP-12-34-01",
@@ -39018,7 +39123,19 @@
"level": 2, "level": 2,
"parent_code": "FP-12", "parent_code": "FP-12",
"sort_order": 107520, "sort_order": 107520,
"tables": [] "tables": [],
"parent_mode": "sum_steps",
"steps": [
{
"code": "FP-12-38-02",
"weight": "1"
},
{
"code": "FP-12-38-03",
"weight": "1"
}
],
"steps_basis": "품셈 12-38 유로폼 = 12-38-2 사용수량(자재) + 12-38-3 설치 및 해체(품) — 12-38-1 사용횟수는 금액 단계가 아니라 사용수량의 잔존율 조건"
}, },
{ {
"work_item_code": "FP-12-38-01", "work_item_code": "FP-12-38-01",
@@ -39280,7 +39397,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 108544, "sort_order": 108544,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-01", "work_item_code": "FP-13-01",
@@ -39298,7 +39416,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 109056, "sort_order": 109056,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-02-01", "work_item_code": "FP-13-02-01",
@@ -39785,7 +39904,8 @@
] ]
] ]
} }
] ],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-03-01", "work_item_code": "FP-13-03-01",
@@ -39936,7 +40056,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 111104, "sort_order": 111104,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-04-01", "work_item_code": "FP-13-04-01",
@@ -40800,7 +40921,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 112896, "sort_order": 112896,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-05-01", "work_item_code": "FP-13-05-01",
@@ -40959,7 +41081,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 113664, "sort_order": 113664,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-06-01", "work_item_code": "FP-13-06-01",
@@ -41252,7 +41375,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 114688, "sort_order": 114688,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-07-01", "work_item_code": "FP-13-07-01",
@@ -41564,7 +41688,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 115968, "sort_order": 115968,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-10-01", "work_item_code": "FP-13-10-01",
@@ -41921,7 +42046,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 116736, "sort_order": 116736,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-11-01", "work_item_code": "FP-13-11-01",
@@ -42399,7 +42525,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 118016, "sort_order": 118016,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-12-01", "work_item_code": "FP-13-12-01",
@@ -42587,7 +42714,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 118784, "sort_order": 118784,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-13-01", "work_item_code": "FP-13-13-01",
@@ -42922,7 +43050,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 119808, "sort_order": 119808,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-15-01", "work_item_code": "FP-13-15-01",
@@ -43092,7 +43221,8 @@
"level": 2, "level": 2,
"parent_code": "FP-13", "parent_code": "FP-13",
"sort_order": 120832, "sort_order": 120832,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-13-16-01", "work_item_code": "FP-13-16-01",
@@ -43273,7 +43403,8 @@
"level": 1, "level": 1,
"parent_code": null, "parent_code": null,
"sort_order": 121600, "sort_order": 121600,
"tables": [] "tables": [],
"parent_mode": "choose_one"
}, },
{ {
"work_item_code": "FP-14-01", "work_item_code": "FP-14-01",
+93
View File
@@ -0,0 +1,93 @@
"""부모 공종 모양 `parent_mode` (명세 2장 · 2026-09-13 축 C Ⓐ).
지키는
마스터 부모는 기본 choose_one, 합산형(9-4·9-5·12-38) 사람이 적은 단계·가중치를 든다
흙깎기 리핑암 = 9-4 암절취 = 암파쇄(암질별 1/Q) + 집토 부모 제목이 잎들의 합으로 선다
내역 줄의 갈래가 그대로 갈래 값으로 가고, 표에 없는 암질은 9-4-1 [] 평균으로만 선다
단계 하나라도 서면 부모를 세우고 사유 · 갈래 고르기형 부모를 가리키면 오류로 막는다
"""
from __future__ import annotations
from decimal import Decimal
from functools import lru_cache
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
from B09_Estimation.B09_Estimation_ResourceAxis_Sources import load_work_item_master
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices
@lru_cache(maxsize=1)
def _build():
return build_unit_prices()
def _node(code: str) -> dict:
return next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == code)
def test_마스터_부모_모양은_기본_고르기_합산형만_단계를_든다():
assert _node("FP-10-12")["parent_mode"] == "choose_one"
rock = _node("FP-09-05")
assert rock["parent_mode"] == "sum_steps"
assert [(s["code"], s["weight"]) for s in rock["steps"]] == [
("FP-09-05-01", "0.1"),
("FP-09-05-02", "0.9"),
("FP-09-05-03", "1"),
]
assert "parent_mode" not in _node("FP-09-04-01") # 잎
def test_암절취는_암파쇄와_집토의_합으로_선다():
book = _build().book
leaf = book.details["B-FP-09-04-01#연암"]
assert {d.ref_code for d in leaf} == {"X-0201-0070#조합", "X-0230-0007"} # 조합 16 %
assert all(d.quantity == Decimal(1) / Decimal("5.0") for d in leaf)
whole = book.resolve("B-FP-09-04#연암").total
parts = book.resolve("B-FP-09-04-01#연암").total + book.resolve("B-FP-09-04-02").total
assert whole == parts > 0
assert "B-FP-09-04#평균" in book.titles # [주]① 평균
def test_단계가_못_서면_부모를_안_세우고_사유를_남긴다():
build = _build()
assert not any(code.startswith("B-FP-09-05#") for code in build.book.titles)
assert "FP-09-05-01" in build.component_gaps["FP-09-05"] # 발파 착암기 층 없음
assert "FP-12-38-02" in build.component_gaps["FP-12-38"] # 사용수량 미구현
# 깎기 표엔 기계 이름이 없음 — [주] 원문의 기종으로 섬(부모에서 물려받지 않음)
assert "[주]" in build.book.details["B-FP-09-05-02#경암"][0].note
def _bill(rock: str, method: str = "ripping"):
handoff = build_handoff(
summary_table={
"rows": [{"group": "흙깎기", "item": rock, "spec": "", "unit": "", "amount": 10.0}]
},
ground_methods={rock: method},
)
return handoff["work_items"][0], build_bill(handoff, build=_build())
def test_암_갈래가_그대로_단가를_고른다():
row, bill = _bill("보통암")
assert (row["work_item_code"], row["variant_value"]) == ("FP-09-04", "보통암")
priced = next(r for r in bill.rows if not r.is_group and r.name == "흙깎기")
assert priced.unit_price_krw and "보통암" in priced.spec
def test_표에_없는_암질은_주1_평균으로만_선다():
_row, bill = _bill("풍화암")
priced = next(r for r in bill.rows if not r.is_group and r.name == "흙깎기")
assert priced.unit_price_krw and "[주]①" in priced.note
def test_발파암은_단계_사유로_막히고_고르기형_부모는_오류():
_row, bill = _bill("경암", "blasting")
assert any("단계 합산 미완" in m["reason"] for m in bill.missing)
handoff = {
"work_items": [{**_row, "work_item_code": "FP-10-12", "variant_value": None}],
"materials": [],
}
blocked = build_bill(handoff, build=_build())
assert any("갈래 고르기형 부모" in m["reason"] for m in blocked.missing)