diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index ae4ec6d8b..526138242 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -73,14 +73,13 @@ from B06_Section.B06_Section_Schema import ( SectionRegenerateRequest, SectionSummaryResponse, ) -from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for +from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for, haul_limits_for from common_util.common_util_auth import verify_session from common_util.common_util_storage import resolve_stored_project_path from common_util.common_util_surface_confirmation import get_surface_confirmation_params from common_util.common_util_workflow_state import get_workflow_state from config.config_db import get_db_pool, run_with_connection from config.config_system import ( - EARTHWORK_HAUL_EQUIPMENT_LIMITS_M, FOREST_ROAD_MIN_WIDTH_M, NATURAL_SPOIL_MIN_GROUND_SLOPE, SECTION_VERTICAL_EXAGGERATION, @@ -149,7 +148,7 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON earthwork_conversion=await conversion_factors_for(project_id), haul_equipment_limits=[ HaulEquipmentLimit(key=key, max_distance_m=limit) - for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M + for key, limit in await haul_limits_for(project_id) ], natural_spoil_min_ground_slope=NATURAL_SPOIL_MIN_GROUND_SLOPE, ) diff --git a/B06_Section/B06_Section_Router_HaulPlan.py b/B06_Section/B06_Section_Router_HaulPlan.py index 4c5994567..fcb3c5854 100644 --- a/B06_Section/B06_Section_Router_HaulPlan.py +++ b/B06_Section/B06_Section_Router_HaulPlan.py @@ -27,6 +27,7 @@ from B06_Section.B06_Section_Server_Calc_Prebuild import ( _mass_haul_context, conversion_factors_for, haul_inputs_for, + haul_limits_for, ) from common_util.common_util_node_bundle import run_bundle_json @@ -66,6 +67,8 @@ async def compute_haul_plan( haul_inputs = await haul_inputs_for(project_id) # 곡선이 쓰는 계수도 프로젝트가 고른 값으로 — 토적표·운반표와 같은 값이어야 한다. factors = await conversion_factors_for(project_id) + # 장비 거리 경계도 프로젝트 값으로 — 도쟈 한계거리를 고쳤으면 배분이 그 값으로 선다. + limits = await haul_limits_for(project_id) try: output = await asyncio.to_thread( run_bundle_json, @@ -73,7 +76,7 @@ async def compute_haul_plan( _NPM_SCRIPT, { "haul_plan_for": result, - "context": _mass_haul_context(haul_inputs, factors), + "context": _mass_haul_context(haul_inputs, factors, limits), }, ) except Exception: diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 6c0913cdd..f87df21bb 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -40,6 +40,7 @@ from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs from common_util.common_util_node_bundle import run_bundle_json from common_util.common_util_project_settings import ( earthwork_conversion_factors, + haul_equipment_limits, quantity_settings, ) from common_util.common_util_storage import resolve_stored_project_path @@ -98,9 +99,21 @@ async def conversion_factors_for(project_id: Any) -> dict[str, dict[str, float]] return earthwork_conversion_factors(quantity_settings(root)) +async def haul_limits_for(project_id: Any) -> list[tuple[str, float | None]]: + """이 프로젝트가 쓸 운반장비 거리 경계(도쟈 한계거리를 고쳤으면 그 값). 못 읽으면 기본값.""" + try: + stored_path = await run_with_connection(get_project_storage_relative_path, project_id) + root = resolve_stored_project_path(stored_path) + except Exception: + logger.warning("B06 프로젝트 경로를 못 찾음 — 기본 경계로 진행: project_id=%s", project_id) + return list(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M) + return haul_equipment_limits(quantity_settings(root)) + + def _mass_haul_context( haul_inputs: dict[str, Any] | None = None, factors: dict[str, dict[str, float]] | None = None, + limits: list[tuple[str, float | None]] | None = None, ) -> dict[str, Any]: """유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다. @@ -118,9 +131,10 @@ def _mass_haul_context( # 프로젝트가 고른 계수가 있으면 그것, 없으면 정본 기본값. "earthwork_conversion": factors or EARTHWORK_CONVERSION_FACTORS, "natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE, + # 프로젝트가 도쟈 한계거리를 고쳤으면 그 값, 없으면 정본 기본값. "haul_equipment_limits": [ {"key": key, "max_distance_m": limit} - for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M + for key, limit in (limits or EARTHWORK_HAUL_EQUIPMENT_LIMITS_M) ], # ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다. # 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다. @@ -220,7 +234,9 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: { "detail": detail, "context": _mass_haul_context( - haul_inputs, earthwork_conversion_factors(quantity_settings(project_root)) + haul_inputs, + earthwork_conversion_factors(quantity_settings(project_root)), + haul_equipment_limits(quantity_settings(project_root)), ), }, ) diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py index e87898d8b..e3a9afa3b 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py @@ -33,6 +33,8 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any +from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import judged_form + ROOT = Path(__file__).resolve().parent.parent SOURCE = ROOT / "resources" / "data_cost_input_value" / "pum_forest_2026.json" OUT_DIR = ROOT / "resources" / "data_work_item_master" @@ -276,8 +278,9 @@ BASIS_RE = re.compile(r"(\d[\d,.]*)\s*(㎥|m3|㎡|m2|㏊|ha|km|㎞|m|인|본|개 # ⚠ **「당」 또는 「단위:」 가 있어야 밑수다.** 둘 다 없으면 규격일 뿐이다 — # 만들다 실제로 걸렸다: `(무한궤도,0.7㎥)` 를 「0.7㎥당」으로 읽어 5건이 잘못 잡혔다. #: ⚠ `(단위: 인/㎡당)` 꼴 — **분모가 밑수**다. 값의 단위(인)를 밑수로 읽으면 뜻이 뒤집힌다. +#: ⚠ 「단위:」 없이 `(인/100m당)` 으로만 적은 표가 22 곳이다(조림 5장·단끊기 5-16-1 등, 2026-09-13). SOURCE_BASIS_RATIO_RE = re.compile( - r"[((]\s*단위\s*[::]\s*[^))/]+/\s*([\d,]*\.?\d*)\s*" + r"[((]\s*(?:단위\s*[::]\s*)?[^))/::]+/\s*([\d,]*\.?\d*)\s*" r"(㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대)\s*당\s*[))]" ) @@ -563,16 +566,6 @@ def capacity_formula_pending(table: dict[str, Any]) -> bool: return bool((keys | cells) & CAPACITY_SYMBOLS) -def variant_axis(table: dict[str, Any]) -> list[str]: - """행이 갈리는 축 — 표의 첫 열 값들(토질·암종·규격). 값 열은 뺀다.""" - seen: list[str] = [] - for row in table.get("rows", []): - key = norm(row[0]) if row else "" - if key and key not in seen: - seen.append(key) - return seen[:24] - - def basis_quantity_is_grouped(quantity: float | None) -> bool: """「10㎡당」처럼 **묶음 기준**인가. 1 이 아니면 곱셈이 그만큼 갈린다.""" return quantity is not None and abs(quantity - 1.0) > 1e-9 @@ -580,6 +573,10 @@ def basis_quantity_is_grouped(quantity: float | None) -> bool: def build() -> dict[str, Any]: data = json.loads(SOURCE.read_text(encoding="utf-8")) + # 사람 판정 선언(표 형태·부모 모양)은 절 번호로 적혀 판에 묶임 — 판이 다르면 멈춤(명세 17장). + from common_util.common_util_pum_edition import require_code_edition + + require_code_edition(data["effective_date"], "공종 마스터 빌드 — 표 형태·부모 모양 판정") tables = data["variables"]["pum"]["tables"] # 원문을 함께 연다 — 밑수가 표 밖(본문)에 있기 때문이다. 못 열면 밑수 없이 간다. source_lines: list[str] = [] @@ -608,6 +605,8 @@ def build() -> dict[str, Any]: number = section_number(section) chapter = number.split("-")[0] if number else None form, why = detect_form(table, chapter) + # 사람이 가른 형태가 자동 판정을 이긴다 — 표마다 까닭 한 줄(미판정 표 판정, 2026-09-13). + form, why = judged_form(table["table_id"], number) or (form, why) # ⚠ **본문이 정본이다.** 표 안을 긁는 쪽은 보조 — 비고에 적힌 다른 기준 # (「10㎡당」 같은 참고 문구)을 그 표의 밑수로 잘못 물어 온다. # 실제로 13-3-1 이 본문 `(단위: ㎥당)` 인데 표 안 긁기가 `10㎡` 를 물어 왔다. @@ -669,7 +668,8 @@ def build() -> dict[str, Any]: "special_glyphs": special_glyphs(table), # 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다. "capacity_formula_here": capacity_formula_pending(table), - "variant_key": variant_axis(table), + # 갈래 키 — 부모 모양을 단 뒤 표 읽기가 가른 대로 채움(`_Variants`). 못 가르면 빈칸. + "variant_key": [], "condition_note": [norm(h) for h in table.get("headers", []) if norm(h)], "raw_row": table.get("rows", []), # 원문 셀 — B09 자원 축이 읽는다. } @@ -694,6 +694,10 @@ def build() -> dict[str, Any]: from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Parents import apply_parent_modes apply_parent_modes(nodes) + # 갈래 키(명세 14장 정정) — 표 모양 가르기는 B09 표 읽기 한 벌을 그대로 돌려 씀. + from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Variants import attach_variant_keys + + attach_variant_keys(nodes, data["effective_date"]) src_meta = { "dataset_id": data["dataset_id"], @@ -706,6 +710,8 @@ def build() -> dict[str, Any]: "schema_version": SCHEMA_VERSION, "dataset_id": "work_item_master_forest", "effective_date": data["effective_date"], + # 공종 코드(`FP-*` 절 번호)가 묶인 품셈 판 — 자원 축·인계 줄이 이 값을 싣는다. + "pum_edition": data["effective_date"], "generated_at": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), "dataset_version": src_meta, "policy": { diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py new file mode 100644 index 000000000..6e0f4f07d --- /dev/null +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py @@ -0,0 +1,158 @@ +"""공종 마스터 — **사람이 가른 표 형태** (2026-09-13 축 C 1장 · 미판정 표 판정). + +자동 판정(`detect_form`)이 못 가르거나(undetermined 76표) 잘못 가른 표만 싣는다. +표마다 **왜 그렇게 갈랐는지 한 줄** — 원문 `(산림청고시 제2025-82호) 산림사업 표준품셈.md`. + +⚠⚠ 뒤집히면 20배 틀림 — `productivity`(1÷값) / `requirement`(값÷밑수)는 **표 머리와 표 바로 위 + 본문의 단위 표현**으로만 가른다. 행 이름·비고의 직종 이름에 끌려 읽지 않는다. +⚠ **못 가른 표는 싣지 않는다** — undetermined 로 남는다(막갈이 깊이표·드론운반 인원 등 밑수 불명). +⚠ `coefficient`·`reference` 로 가른 표는 일위대가 줄로 안 읽힌다(자원 축이 건너뜀) — 시공능력 + 공식 계수는 형태와 상관없이 공식 읽기가 표를 직접 본다. +⚠ 표 번호는 원천 차례로 매긴 것이라 **절 번호를 함께 대조**한다 — 어긋나면 빌드를 멈춘다. +""" + +from __future__ import annotations + +#: 표 번호 → (절 번호, 형태, 까닭). +FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = { + # ── 제2장 재료 기준 ───────────────────────────────────────────── + "F0044": ( + "2-1-1", + "reference", + "기계별 주연료(ℓ/일·대)·잡품(%) — 행이 자원이 아니라 기계, 재료비 산출 기준", + ), + "F0045": ("2-1-1", "reference", "위와 같은 모양(2드럼 케이블윈치)"), + "F0047": ( + "2-1-2", + "reference", + "기계별 경유(ℓ/일·대)·잡품(%) — [주]③ 「1일당 소요량」, 행이 기계", + ), + "F0048": ("2-1-3", "reference", "머리 「소요비용(원)」 — 품이 아니라 교체 1회당 금액"), + "F0054": ("2-1-5", "requirement", "본문 「(본당)」 + 행 「소금처리량(g)」 — 본당 재료 소요량"), + "F0063": ( + "2-1-13", + "reference", + "「소모율 0.00125개/hr」 — 기계 시간당 소모품, 공종 밑수가 아님", + ), + # ── 제5장 조림·녹화 ───────────────────────────────────────────── + "F0100": ("5-3-1", "reference", "수종별 묘목 규격(소·중·대묘) 표"), + "F0101": ("5-3-1", "reference", "간장·근원경 규격 기준 표"), + "F0105": ("5-3-4", "coefficient", "지주목 미설치 감률 「인력품의 10%·20%」"), + "F0107": ("5-3-5", "coefficient", "위와 같은 감률(흉고직경 표)"), + "F0137": ("5-24-2", "reference", "머리 「종자배합비율(%)」 조견표"), + "F0138": ("5-24-2", "reference", "위와 같은 조견표(기타 적용지역)"), + "F0146": ("5-29", "coefficient", "행이 K(버킷계수)·f·E·Cm — 굴착기 작업량 공식 계수"), + # ── 제7장 수확·운재 ───────────────────────────────────────────── + "F0187": ("7-9-2", "coefficient", "본문 「(단위 : %)」 — 집재재적·거리별 수집량 증가율"), + "F0189": ("7-11", "productivity", "표 바로 위 「(㎥/1대, 1일)」 — 대당 일 작업량"), + "F0190": ("7-11", "productivity", "위와 같음(최대집재거리 80m 이하)"), + "F0194": ("7-13", "reference", "검척 산출 예시(말구직경×개수) 표"), + "F0196": ( + "7-15-1", + "productivity", + "표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량, 「회수」 줄은 참고", + ), + "F0197": ( + "7-15-2", + "productivity", + "표 바로 위 「(㎥/1대, 1일)」 — 「운재량」 줄이 작업량, [주]① 1대 1일", + ), + "F0198": ("7-16", "productivity", "표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량"), + "F0199": ("7-17", "productivity", "표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량"), + "F0200": ("7-18", "productivity", "표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량"), + # ── 제8장 방제 ────────────────────────────────────────────────── + "F0205": ("8-2-1", "reference", "약제 이름·제형 목록"), + "F0208": ("8-2-2", "reference", "선정약제 목록 + 원액 주입량(㎖/㎝) 기준 — 품 표는 따로 있음"), + "F0212": ("8-2-3", "reference", "위와 같은 선정약제 목록"), + "F0216": ("8-2-4", "reference", "위와 같은 선정약제 목록"), + "F0218": ("8-2-5", "reference", "위와 같은 선정약제 목록"), + "F0453": ( + "8-2-3", + "reference", + "〔부록 2〕 산림사업별 적용 가능 공종 목록 — 절 경계가 밀려 붙음", + ), + "F0220": ( + "8-3", + "productivity", + "머리 「벌목조재 (㎥/인)·무더기 훈증 (RM/인)·그루터기 박피 (본/인)」 — 인당 작업량", + ), + "F0221": ("8-3", "coefficient", "본문 「(단위: %)」 — 수종·직경별 지조율"), + "F0222": ("8-3", "coefficient", "머리 「비율(수간재적의 %)」 — 근주재적 산정율"), + "F0227": ("8-6-1", "reference", "기종별 희석배수별 약제량(L, 1회) — 살포 계산표"), + "F0231": ("8-6-3", "reference", "희석배수별 약제소요량(㎖) — 물의 양 대비 계산표"), + # ── 제9장 토공 — 시공능력 공식 계수 ──────────────────────────── + "F0281": ("9-15-1", "coefficient", "행이 K·f·E·Cm — [주]② Q1=3600×q×K×f×E/Cm"), + "F0282": ("9-15-2", "coefficient", "행이 T·L·E·q — 불도저 작업량 공식 계수"), + "F0284": ("9-16-2", "coefficient", "행이 V·W·E·D — [주]② Q=1000×V×W×E×D×f/N"), + "F0285": ("9-16-3", "coefficient", "흡입준비·운반·흡입·대기 시간 — 살수차 사이클 시간"), + "F0287": ("9-18", "coefficient", "행이 K·f·E·Cm — [주]① Q1=3600×q×K×f×E/Cm"), + "F0295": ("9-22", "coefficient", "행이 K·f·E·Cm — [주]③ Q1=3600×q×K×f×E/Cm"), + # ── 제10장 운반 ───────────────────────────────────────────────── + "F0300": ("10-3-3", "reference", "「운반비·하차비」 적용 항목 — 값이 없는 구성 안내"), + "F0301": ("10-4", "coefficient", "t1·t2·t3·t4 — 트레일러 운반 사이클 시간"), + "F0308": ("10-7-4", "coefficient", "1사이클당 운반량 q — 모노레일 공식 계수"), + "F0309": ("10-7-4", "coefficient", "본문 「(단위: 분/1회당)」 — 적재·짐부리기 시간"), + "F0316": ("10-9-1", "reference", "규격별 1회당 표준 운반량(kg·개·㎡) — 적재 기준"), + "F0319": ("10-11", "coefficient", "행이 L·E·q0 — [주]② q=3.2×e, Cm 공식 계수"), + "F0320": ("10-12-1", "coefficient", "행이 K·E0·Cm — 굴착기 적재 공식 계수"), + "F0321": ("10-12-1", "coefficient", "적재·t2·적하 시간 — 덤프트럭 사이클 공식 계수"), + "F0324": ( + "10-13-2", + "reference", + "드론운반 단가산출 항목(준비시간·왕복비행시간 식) — [주] 일본 산출 예", + ), + # ── 제11·12장 ─────────────────────────────────────────────────── + "F0328": ("11-4", "coefficient", "행이 q·k·f·E — [주]① Q=3600×q×k×f×E/Cm"), + "F0358": ("12-2", "reference", "시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명"), + "F0360": ("12-2", "reference", "현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명"), + "F0388": ("12-34-4", "reference", "「재료비 JOINT FILLER」 항목만 — 값이 없는 구성 안내"), + "F0390": ("12-36", "reference", "제작비·운송비·설치비 「견적처리」"), + "F0392": ("12-38-1", "coefficient", "사용횟수별 잔존율(12회 25%·25회 10%)"), + "F0394": ("12-38-2", "coefficient", "부자재 요율 「주자재비의 24%·52%·79%」"), + "F0396": ("12-38-3", "reference", "설치 유형(복잡·보통·간단) 적용 시설 설명"), + # ── 제13장 구조물 ─────────────────────────────────────────────── + "F0400": ("13-2-3", "reference", "큰돌 직경별 뒷길이 기준(㎝)"), + "F0406": ( + "13-3", + "reference", + "〈돌쌓기의 개수 및 중량의 표준〉 — [주] 인력 돌쌓기·돌붙임 기준", + ), + "F0408": ( + "13-4-3", + "reference", + "고임돌 소요량(㎥/㎡) — 자재 원단위표(B08 고임돌표가 씀). 품으로 세우면 자재를 두 번 셈", + ), + "F0410": ( + "13-4-4", + "reference", + "[주]① 채움 콘크리트 소요량(㎥/㎡) — 자재 원단위표(B08 채움표가 씀). 품이면 두 번 셈", + ), + "F0411": ("13-4-4", "reference", "[주]⑨ 성토 뒤채움 조약돌 두께(㎝) 기준"), + "F0412": ( + "13-4-4", + "reference", + "[주]⑩ 뒷길이 표준(㎝) — 자동 판정이 「(m)」에 걸려 소요량형으로 오판", + ), + "F0413": ( + "13-4-4", + "reference", + "[주]⑪ 표준경사(1:n) — 자동 판정이 소요량형으로 오판, 찰쌓기(인력)를 막음", + ), + "F0429": ("13-10-2", "coefficient", "[주]⑥ 관입률별 계수"), + "F0439": ("13-12-2", "coefficient", "[주]⑦ 높이·기울기 할증(%)"), + "F0446": ("13-15-2", "reference", "작업조건(어려운·쉬운) 구분 기준"), +} + + +def judged_form(table_id: str, number: str | None) -> tuple[str, str] | None: + """사람이 가른 `(형태, 까닭)`. 없으면 `None`. 절 번호가 어긋나면 멈춘다.""" + found = FORM_JUDGMENTS.get(table_id) + if found is None: + return None + section, form, why = found + if section != number: + raise ValueError( + f"표 형태 판정 {table_id} 의 절 {section} 이 원천의 {number} 와 다릅니다 — " + "원천 표 차례가 바뀌었으니 판정을 다시 대조할 것" + ) + return form, f"사람 판정 — {why}" diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Variants.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Variants.py new file mode 100644 index 000000000..1585fbaaa --- /dev/null +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Variants.py @@ -0,0 +1,79 @@ +"""공종 마스터 — **갈래 키(`variant_key`)를 표 읽기가 실제로 가른 대로** 싣는다 (명세 14장 정정). + +종전 `variant_axis` 는 표 첫 열 값 24개만 담아, 단가표 갈래 제목 294 중 86 만 맞았다 +(2026-09-13 브레인 실측). 머리행 갈래(벌목 「5m이상~8m미만」) · 두 단 머리 · 첫 열이 자원 +이름인 표를 못 가렸다. + +⚠ 표 모양을 여기서 **다시 판정하지 않는다** — 첫 열 · 열이 자원 · 뭉친 이름+갈래 열 · 축 셋 · + 두 단 머리 · 작업조 · 공정 합산 · 고르기형 등 모양 가르기는 B09 표 읽기에 이미 있다. + 두 벌로 짜면 한쪽만 고쳐진다. 그 읽기를 **메모리의 이 마스터**에 돌려 표마다 가른 갈래 + 이름을 원문 표기 그대로 싣는다(키 비교는 받는 쪽이 `normalize_variant_key` 로). +⚠ 못 가른 표는 **빈칸** — 자원을 못 맞춘 표 · 계수 · 참조 · 미판정 표에 갈래를 지어내지 않는다. + +공종마다 `variant_keys` — 표들의 갈래 + 원문이 정했으나 자원 줄로 안 서는 갈래 +(불도저 운반 공식 갈래 · 합산형 단계 잎의 암질 행 · 9-4-1 [주]① 평균) · 합산형 부모는 단계 갈래. +""" + +from __future__ import annotations + +from typing import Any + + +def _add(labels: list[str], seen: set[str], label: str, normalize: Any) -> None: + key = normalize(label) + if key and key not in seen: + seen.add(key) + labels.append(label) + + +def attach_variant_keys(nodes: list[dict[str, Any]], edition: str) -> None: + """표마다 `variant_key`, 공종마다 `variant_keys` 를 채운다(자리에서).""" + from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import extract_dozer_factors + from B09_Estimation.B09_Estimation_ParentSteps import AVERAGE_BASIS, AVERAGE_VARIANT, rock_rows + from B09_Estimation.B09_Estimation_ResourceAxis import build_resource_axis + from B09_Estimation.B09_Estimation_ResourceAxis_Sources import load_combined_catalog + from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key as normalize + + axis = build_resource_axis( + {"effective_date": edition, "work_items": nodes}, load_combined_catalog() + ) + by_table: dict[tuple[str, str], tuple[list[str], set[str]]] = {} + for row in axis.rows: + if row.variant: + labels, seen = by_table.setdefault((row.work_item_code, row.pum_table_id), ([], set())) + _add(labels, seen, row.variant, normalize) + + steps = { + str(step["code"]) + for node in nodes + if node.get("parent_mode") == "sum_steps" + for step in node.get("steps") or [] + } + by_code: dict[str, list[str]] = {} + for node in nodes: + code = str(node.get("work_item_code") or "") + keys: list[str] = [] + seen: set[str] = set() + for table in node.get("tables") or []: + labels, table_seen = by_table.get((code, str(table.get("pum_table_id"))), ([], set())) + found = extract_dozer_factors(code, table) + for label in found if isinstance(found, dict) else {}: + _add(labels, table_seen, label, normalize) + if code in steps: + for rock, _, _ in rock_rows({"tables": [table]}): + _add(labels, table_seen, rock, normalize) + table["variant_key"] = labels + for label in labels: + _add(keys, seen, label, normalize) + if code in AVERAGE_BASIS and keys: + _add(keys, seen, AVERAGE_VARIANT, normalize) + by_code[code] = keys + for node in nodes: + code = str(node.get("work_item_code") or "") + keys = list(by_code.get(code) or []) + if node.get("parent_mode") == "sum_steps": + seen = {normalize(k) for k in keys} + for step in node.get("steps") or []: + for label in by_code.get(str(step["code"])) or []: + _add(keys, seen, label, normalize) + node["variant_keys"] = keys diff --git a/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py b/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py index fa7f6d26a..63b9639e2 100644 --- a/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py +++ b/B08_Quantity/B08_Quantity_Engine_EarthworkSummary.py @@ -72,6 +72,20 @@ class SummaryInput: rock_classes: list[str] = field(default_factory=list) rock_ratios_pct: dict[str, float] = field(default_factory=dict) application_ratios: dict[str, float] = field(default_factory=dict) + # 노체다짐 — 기본 꺼짐(2026-09-13 판정). 켜면 성토 밑에 별도 줄이 선다. + subgrade_compaction_enabled: bool = False + + +#: 노체다짐 줄 — ⭐ 2026-09-13 판정 「별도 줄 · 칸으로 켜고 끔 · 기본 꺼짐」. +#: 성토 단가(포설)에 묻지 않는다 — 묻으면 왜 그 금액인지 설명이 안 되고 켜고 끈 것이 안 보인다. +#: ⚠ [주]③ 「굴착기와 진동롤러 조합」은 작업 방식 설명 — 굴착기는 성토(포설 9-16-1) 줄이 이미 셈. +#: 이 줄에 굴착기를 또 넣으면 두 번 셈(2026-09-14 「#조합」 철회). +SUBGRADE_COMPACTION_SPEC = "진동롤러(자주식 10ton)" +SUBGRADE_COMPACTION_NOTE = ( + "산출 조건에서 켬 — 품셈 9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용」" + " · 수량 = 성토량(포설과 같은 밑수 ㎥)" + " · [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 셈" +) def _ratio(source: SummaryInput, key: str) -> float: @@ -121,6 +135,16 @@ def build_rows(source: SummaryInput) -> list[SummaryRow]: rows.append(SummaryRow(group="보정량계", amount=earth.get("adjusted_total_m3", 0.0))) rows.append(SummaryRow(group="성토", amount=earth.get("fill_volume_m3", 0.0))) + if source.subgrade_compaction_enabled: + # ⚠ 끄면 줄 자체를 안 낸다 — 빈 줄은 「세야 함」으로 읽힌다(임목파쇄와 같은 자리). + rows.append( + SummaryRow( + group="노체다짐", + spec=SUBGRADE_COMPACTION_SPEC, + amount=earth.get("fill_volume_m3", 0.0), + note=SUBGRADE_COMPACTION_NOTE, + ) + ) # ── 운반 — 수단별. 무대는 집계에 오르되 내역 줄이 아니다 ──────── rows.extend(_haul_rows(source)) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index 090e4b1ab..e3b7f4467 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -112,8 +112,13 @@ def build_handoff( bench_cut_depth_m: float | None = None, structure_trench_water: str | None = None, stand_volume_class: str | None = None, + priced_sheets: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: - """B09 가 그대로 받는 모양. 없는 표는 건너뛰되 **빈 표와 구별해 적는다**.""" + """B09 가 그대로 받는 모양. 없는 표는 건너뛰되 **빈 표와 구별해 적는다**. + + `priced_sheets` — 양식 일위대가로 셀 구조물도 장(`StructurePriceLink.priced_sheets`). + 그 장의 구조물은 호표 `AX-ST` 줄 하나로 가고, 일위대가가 품은 줄은 따로 안 감. + """ table = mapping or load_mapping() work_items: list[dict[str, Any]] = [] unmatched: list[str] = [] @@ -135,7 +140,7 @@ def build_handoff( # 띠·이동에서만 운반이 만들어져 사토 잔량이 어디에도 안 실렸다. work_items.extend(spoil_haul_rows(haul_table, table)) if unit_quantity_table: - rows, misses = _structure_rows(unit_quantity_table, table) + rows, misses = _structure_rows(unit_quantity_table, table, priced_sheets) work_items.extend(rows) unmatched.extend(misses) # 구조물이 낸 터파기·되메우기·잔토 — **공종 축으로 올린다.** @@ -145,7 +150,7 @@ def build_handoff( work_items.extend(rows) unmatched.extend(misses) # 기초잡석 — 버림이 선 구조물에 함께 서는 공종(확정 3차 ②). 묶음 구조물은 제외한다. - rows, misses = rubble_base_rows(unit_quantity_table, table) + rows, misses = rubble_base_rows(unit_quantity_table, table, priced_sheets) work_items.extend(rows) unmatched.extend(misses) @@ -208,6 +213,14 @@ def build_handoff( result["ratio_math_warnings"] = verify_ratio_math(result) result["material_code_warnings"] = verify_no_code_on_materials(result) result["bill_flag_warnings"] = verify_bill_flags(result) + # ⚠ 이중계상 경계(명세 6장) — 갈 곳 칸으로 판정한 어긴 자리. B09 내역서가 여기서 멈춘다. + from B08_Quantity.B08_Quantity_Engine_Handoff_Boundaries import ( + verify_double_count_boundaries, + ) + + result["double_count_violations"] = verify_double_count_boundaries( + unit_quantity_table, material_table, table, priced_sheets, work_items + ) # ⚠ 보내는 단위가 **품셈 밑수**와 같은가 — 받는 쪽이 그대로 곱하는 자리다(2026-09-08). result["basis_unit_warnings"] = verify_unit_matches_basis( work_items, extra=table.declared_units() @@ -218,6 +231,10 @@ def build_handoff( (unit_quantity_table or {}).get("collected_stone_deduction_m3") or 0.0 ) result["placing_notes"] = placing_notes + # ⚠ 품셈 판(명세 17장) — 줄마다 코드가 본 판을 싣는다. 판이 다르면 B09 가 값을 안 씀. + result["pum_edition"] = table.pum_edition or None + for row in work_items: + row.setdefault("pum_edition", table.pum_edition or None) return result diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Boundaries.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Boundaries.py new file mode 100644 index 000000000..c4e0ad5be --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Boundaries.py @@ -0,0 +1,159 @@ +"""B08 → B09 인계 — **이중계상 경계를 갈 곳(`destination`) 칸으로 판정** (명세 6장 · 2026-09-13). + +주석 경고뿐이던 규칙 셋을 수로 걸리는 검사로 세운다. 원문은 옛 계획서 8-7절(㉠·㉢)과 명세 6장. + + ① 구조물 터파기·되메우기는 **토공집계로만** — 갈 곳이 `earthwork` 인 성분을 다른 공종 줄이 + 또 집으면 같은 물량이 두 공종 코드에 붙음. 터파기·되메우기가 `earthwork` 밖으로 가도 오류 + ② 배합 성분(시멘트·모래·자갈)은 **B09 일위대가만 쪼갬** — 자재총괄에 뜨면 오류(㉢) + ③ 자재 할증은 **자재총괄 한 곳** — 원단위표가 이미 할증을 붙였으면 오류(㉠). + 자재총괄 합계의 할증 한 번·일위대가 재료비의 할증 전은 B09 가드가 금액 자리에서 봄 + +⚠ 새 얼개를 만들지 않는다 — 성분마다 붙은 `destination` 과, 인계 줄이 성분을 + **이름으로 집는 자리**(구조물 줄 `billing_component` · 묶음 조각 `from_components` · + 콘크리트 타설 · 기초잡석)만 대조한다. +⚠ 여기는 목록만 낸다. 멈추는 것은 B09 `build_bill`(`DoubleCountError`) — 가드가 모인 한 자리. +""" + +from __future__ import annotations + +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import WorkItemMapping +from B08_Quantity.B08_Quantity_Engine_Handoff_Rows import PLACING_TARGET_NAMES +from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import RUBBLE_GROUP +from B08_Quantity.B08_Quantity_Engine_MaterialSummary import verify_single_surcharge +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import MIX_COMPONENTS + +#: 토공집계로만 가야 하는 성분 — 전개·양식·관측 원단위가 같은 이름을 쓴다. +EARTHWORK_ONLY = ("터파기", "되메우기") +#: 공종 줄이 값으로 매기면 안 되는 갈 곳 — 이미 다른 자리가 세거나(토공·사토 공제) 참고값이다. +NOT_BILLED_BY_WORK_ITEM = { + "earthwork": "토공집계", + "haul_deduction": "유토곡선 사토 공제", + "reference": "참고 줄", +} + + +def _pickers(structure: dict[str, Any], mapping: WorkItemMapping) -> list[tuple[str, set[str]]]: + """이 구조물의 성분을 **이름으로 집는** 공종 줄 — (자리, 이름들). 집는 조건은 각 빌더와 같다.""" + type_id = str(structure.get("type_id") or "") + entry = mapping.for_structure(type_id) or {} + composite = mapping.composite_for(type_id) + found: list[tuple[str, set[str]]] = [] + if entry.get("billing_component"): + found.append(("구조물 줄", {str(entry["billing_component"])})) + if composite and not entry.get("work_item_code"): + for part in composite.get("parts") or []: + if isinstance(part, dict): + label = f"묶음 조각 「{part.get('name') or part.get('code')}」" + found.append((label, {str(name) for name in part.get("from_components") or []})) + if not composite: + # 묶음 구조물은 타설·기초잡석 줄이 건너뛴다(조각이 이미 셈) — 빌더와 같은 조건. + found.append(("콘크리트 타설 줄", set(PLACING_TARGET_NAMES))) + found.append(("기초잡석 줄", {RUBBLE_GROUP})) + return found + + +def verify_double_count_boundaries( + unit_quantity_table: dict[str, Any] | None, + material_table: dict[str, Any] | None, + mapping: WorkItemMapping, + priced_sheets: list[dict[str, Any]] | None = None, + work_items: list[dict[str, Any]] | None = None, +) -> list[str]: + """어긴 자리를 사람 말로 — 비면 경계가 지켜진 것.""" + found: list[str] = [] + for structure in (unit_quantity_table or {}).get("structures") or []: + label = str(structure.get("name") or structure.get("type_id") or "구조물") + destinations: dict[str, set[str]] = {} + for component in structure.get("components") or []: + if float(component.get("amount") or 0.0) <= 0: + continue + name = str(component.get("name") or "").strip() + destination = str(component.get("destination") or "") + destinations.setdefault(name, set()).add(destination) + if name in EARTHWORK_ONLY and destination != "earthwork": + found.append( + f"① {label} 「{name}」 갈 곳이 {destination or '(없음)'} — " + "구조물 터파기·되메우기는 토공집계(earthwork)로만 감" + ) + for where, names in _pickers(structure, mapping): + for name in sorted(names): + for destination in sorted( + destinations.get(name, set()) & set(NOT_BILLED_BY_WORK_ITEM) + ): + found.append( + f"① {label} 「{name}」은 {NOT_BILLED_BY_WORK_ITEM[destination]}" + f"({destination})로 가는데 {where}이 또 셈 — 같은 물량이 두 자리에 붙음" + ) + for row in (material_table or {}).get("rows") or []: + name = str(row.get("name") or "").strip() + if name in MIX_COMPONENTS: + found.append( + f"② 자재총괄에 배합 성분 「{name}」 — 시멘트·모래·자갈은 B09 일위대가만 쪼갬(㉢)" + ) + found.extend(f"③ {warning} (㉠)" for warning in verify_single_surcharge(unit_quantity_table)) + found.extend(_verify_conserved(unit_quantity_table, mapping, priced_sheets, work_items)) + return found + + +#: 여러 구조물의 성분을 **이름으로 모아 한 줄**로 세우는 인계 줄 — (줄 이름, 모으는 성분 이름들). +#: ⚠ 이 줄들은 구조물마다 「누구 것을 뺐나」를 안 남긴다 — 그래서 물량 보존으로 본다. +SUMMED_ROWS = ( + (RUBBLE_GROUP, frozenset({RUBBLE_GROUP})), + ("콘크리트 타설", frozenset(PLACING_TARGET_NAMES)), +) +_TOLERANCE_M3 = 1e-6 + + +def _verify_conserved( + unit_quantity_table: dict[str, Any] | None, + mapping: WorkItemMapping, + priced_sheets: list[dict[str, Any]] | None, + work_items: list[dict[str, Any]] | None, +) -> list[str]: + """④ 양식 호표·묶음 조각이 품은 성분을 모음 줄이 또 셈 — **물량 보존**으로 판정(2026-09-13). + + 모음 줄 + 호표가 품은 몫 + 묶음 조각 ≤ 원단위 성분 합. 넘치면 같은 물량을 두 자리에서 셈 + (검증 프로젝트 찰쌓기: 호표가 품은 기초잡석이 인계 줄로도 나가 8.3㎥ ← 실제 3.5㎥). + ⚠ 빌더마다 「무엇을 건너뛰나」를 따라 짜지 않는다 — 건너뛰기를 빠뜨린 빌더를 잡는 검사다. + """ + if not unit_quantity_table or work_items is None: + return [] + covered = { + sid: set(entry.get("covered") or []) + for entry in priced_sheets or [] + for sid in entry.get("structure_ids") or [] + } + found: list[str] = [] + for row_name, names in SUMMED_ROWS: + available = held = 0.0 + for structure in unit_quantity_table.get("structures") or []: + taken = covered.get(str(structure.get("structure_id")), set()) + for component in structure.get("components") or []: + name = str(component.get("name") or "").strip() + if name not in names or component.get("unit") != "㎥": + continue + amount = float(component.get("amount") or 0.0) + available += amount + held += amount if name in taken else 0.0 + in_parts = sum( + float(part.get("quantity") or 0.0) + for row in work_items + for part in row.get("composite_parts") or [] + if isinstance(part, dict) + and part.get("from_components") + and set(part["from_components"]) <= names + ) + summed = sum( + float(row.get("quantity") or 0.0) + for row in work_items + if row.get("name") == row_name and not row.get("composite_parts") + ) + if summed + held + in_parts > available + _TOLERANCE_M3: + found.append( + f"④ 「{row_name}」 줄 {summed:g}㎥ + 양식 호표가 품은 {held:g}㎥ + 묶음 조각 " + f"{in_parts:g}㎥ 가 원단위 합 {available:g}㎥ 를 넘음 — 호표·조각이 품은 성분을" + " 모음 줄이 또 셈" + ) + return found diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py index 28f674a4c..96598873c 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py @@ -192,6 +192,8 @@ class WorkItemMapping: """수량 줄 → 공종 마스터 코드. 못 찾으면 `None` 을 돌려주고 부른 쪽이 목록에 남긴다.""" effective_date: str = "" + #: 매핑의 공종 코드가 본 품셈 판(명세 17장) — 인계 줄마다 이 값이 `pum_edition` 으로 실린다. + pum_edition: str = "" earthwork: list[dict[str, Any]] = field(default_factory=list) haul: list[dict[str, Any]] = field(default_factory=list) structure: list[dict[str, Any]] = field(default_factory=list) @@ -284,6 +286,7 @@ def load_mapping(path: Path | None = None) -> WorkItemMapping: variant_aliases = [row for row in load_aliases("variant") if row["pum_edition"] == edition] return WorkItemMapping( effective_date=str(payload.get("effective_date") or ""), + pum_edition=edition, earthwork=list(payload.get("earthwork") or []), haul=list(payload.get("haul") or []), structure=list(payload.get("structure") or []), diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py index 1e2197913..b0fe9ba54 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py @@ -342,13 +342,19 @@ def _component_billing( def _structure_rows( - unit_quantity_table: dict[str, Any], mapping: WorkItemMapping + unit_quantity_table: dict[str, Any], + mapping: WorkItemMapping, + priced_sheets: list[dict[str, Any]] | None = None, ) -> tuple[list[dict[str, Any]], list[str]]: """구조물 줄 — **작업 공종 하나**로 선다. ⚠ 전개 성분(터파기·야면석·모르터)은 여기 오지 않는다. 구조물 한 기가 내역 한 줄이고, 그 전개는 토공 합산·자재총괄·일위대가로 갈린다(8-2 이중계상 함정). + ⭐ 양식 일위대가로 셀 장(`priced_sheets`)에 든 구조물은 **호표 `AX-ST` 줄**로 감(PLAN 6장 ②). """ + from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import by_structure, template_row + + priced = by_structure(priced_sheets or []) rows: list[dict[str, Any]] = [] unmatched: list[str] = [] for structure in unit_quantity_table.get("structures") or []: @@ -400,7 +406,11 @@ def _structure_rows( {"axis": axis, "value": (structure.get("options") or {}).get(axis)} for axis in entry.get("secondary_axes") or [] ] - if code is None and composite is None: + # 양식 일위대가로 셀 구조물 — 막힘은 일위대가 표가 가림(전개 갈래 사유는 안 셈). + sheet_entry = priced.get(str(structure.get("structure_id"))) + if sheet_entry is not None: + pass + elif code is None and composite is None: unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다") elif entry.get("class_from") in ("back_length", "bond") and class_key is None: unmatched.append(f"{wording_type_label(type_id)} — {class_basis}") @@ -476,6 +486,8 @@ def _structure_rows( "origin": ORIGIN_STRUCTURE, } ) + if sheet_entry is not None: + rows[-1] = template_row(rows[-1], structure, sheet_entry) return rows, unmatched diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py index 1c1f3b2f5..0e7ac65d9 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py @@ -149,18 +149,28 @@ def _row(**fields: Any) -> dict[str, Any]: def rubble_base_rows( - unit_quantity_table: dict[str, Any], mapping: WorkItemMapping + unit_quantity_table: dict[str, Any], + mapping: WorkItemMapping, + priced_sheets: list[dict[str, Any]] | None = None, ) -> tuple[list[dict[str, Any]], list[str]]: """기초잡석(품셈 12-25) — 구조물 전개가 낸 성분을 **한 줄로** 올린다. ⚠ **묶음으로 서는 구조물은 건너뛴다** — 옹벽 묶음에 이미 `FP-12-25` 조각이 있어 여기서 또 세우면 같은 잡석을 두 번 센다(타설 줄에서 이미 겪은 자리). + ⚠ **양식 일위대가가 기초잡석을 품은 구조물도 건너뛴다**(PLAN 6장 ② 브레인 판정) — + 호표 `AX-ST` 금액에 이미 들어 있음. """ + from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import by_structure + + priced = by_structure(priced_sheets or []) total = 0.0 bases: list[str] = [] for structure in unit_quantity_table.get("structures") or []: if mapping.composite_for(str(structure.get("type_id") or "")): continue + sheet_entry = priced.get(str(structure.get("structure_id"))) + if sheet_entry is not None and RUBBLE_GROUP in sheet_entry["covered"]: + continue for component in structure.get("components") or []: if str(component.get("name") or "") != RUBBLE_GROUP: continue diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index ed1d57ece..743111f4e 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -10,7 +10,7 @@ 「토공집계 지장목제거로 이미 섬」이라고 가리키기만** 한다. ⚠ 값이 없는 줄의 사유를 적는다 - · 표토제거(9-15) — 면적은 사면적에서 나오나 **두께·대상 구간이 설계로 안 정해져 있다**. + · 표토제거(9-15-2) — **노면 + 절토 사면 ㎡** · 노면 폭을 못 세면 안 섬(2026-09-13 「실무대로」). · 제근·뿌리다듬기(9-20~21) — 단위가 **「개」(그루 수)**인데 입목 본수를 우리가 안 든다. · 규준틀(11-2) — **개소** 산정 기준(구조물·절성토 구간별 몇 개소)이 안 정해져 있다. @@ -110,13 +110,15 @@ def preparation_rows( topsoil_thickness_m: float | None = None, topsoil_haul_distance_m: float | None = None, stand_volume_class: str | None = None, + road_surface_area_m2: float | None = None, + topsoil_road_only: bool = False, ) -> list[dict[str, Any]]: """준비공 줄 — 값이 서는 것과 안 서는 것을 **한 목록에** 낸다.""" slope = slope_totals or {} tree_area = float(slope.get("tree_removal_fill", 0.0)) + float( slope.get("tree_removal_cut", 0.0) ) - topsoil = _topsoil_row(slope, topsoil_thickness_m) + topsoil = _topsoil_row(slope, road_surface_area_m2, topsoil_road_only) return [ { "group": "준비공", @@ -141,7 +143,7 @@ def preparation_rows( }, topsoil, # ⚠ 법이 요구하는 **운반·적치** — 제거 물량이 곧 밑수다(별표2). - _topsoil_haul_row(topsoil, topsoil_haul_distance_m), + _topsoil_haul_row(topsoil, topsoil_thickness_m, topsoil_haul_distance_m), _root_removal_row(slope, stand_volume_class), *_root_steps_rows(slope, stand_volume_class), _batter_frame_row(list(slope_rows)), @@ -149,53 +151,59 @@ def preparation_rows( ] -def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str, Any]: - """표토제거 — **두께는 품셈이 아니라 설계가 정한다**(9-15 [주]② 「T : 표토두께(m)」). +def _topsoil_row( + slope: dict[str, float], road_area_m2: float | None, road_only: bool = False +) -> dict[str, Any]: + """표토제거 — **밑수 ㎡ · 대상 · 20m 압토 셋이 한 묶음**(2026-09-13 판정 · PLAN 10장). - ⚠ 두께를 안 넣으면 **0 으로 때우지 않고** 물량을 안 낸다. 대상 면적은 절·성토 사면적을 - 쓴다(사면 계열의 면고르기 면적과 같은 자리). + ① 밑수 **㎡** — 품셈 9-15-2 가 T=0.2 m 를 **품 조건**으로 둔 면적 축(실무 영월 「표토제거 + 답외구간 M2」). 두께는 여기 안 곱한다 — 운반 부피에만 쓴다. + ② 대상 **노면 + 절토대상지** — 별표2 Ⅰ.2.차.(6) 문언. 성토대상지는 (7) 「제거·정리」라 안 셈. + `road_only` 면 노면만(영월 한 건 관측 · 설계자 선택 · 2026-09-14 판정). + ③ **20 m 압토는 품에 듦** — 운반 줄은 그 넘는 몫만(`_topsoil_haul_row`). + ⚠ 하나만 고치면 합계가 그럴듯해 보이면서 틀린다 — 셋을 함께 바꿨다. """ - area = float(slope.get("face_dressing_fill", 0.0)) + float(slope.get("face_dressing_cut", 0.0)) - # ⚠ 면적이 0 이면 **0 ㎥ 를 「값 있음」으로 내지 않는다** — 0 은 「없음」과 구별이 안 되고, - # 받는 쪽이 「표토가 없는 노선」으로 읽는다. 절·성토 사면적이 0 인 임도는 성립하지 - # 않으므로 이 자리는 사실상 **사면표가 아직 안 선 것**이다(2026-09-09 감사에서 잡음). + cut = 0.0 if road_only else float(slope.get("face_dressing_cut", 0.0)) + base = {"group": "준비공", "item": "표토제거", "unit": "㎡", "work_item_code": TOPSOIL_CODE} + if road_area_m2 is None: + return { + **base, + "amount": None, + "status": STATUS_PENDING, + "reason": ( + "노면 면적을 못 셉니다 — 횡단 설계에 노체 끝(노면 폭)이 없는 측점이 있음. " + f"대상은 노면 + 절토대상지(별표2) · 절토 사면 {cut:,.1f}㎡ 만으로는 세우지 않음" + ), + "reference_amount": cut, + } + area = float(road_area_m2) + cut + # ⚠ 면적이 0 이면 **0 ㎡ 를 「값 있음」으로 내지 않는다** — 받는 쪽이 「표토가 없는 노선」으로 + # 읽는다(2026-09-09 감사에서 잡음). 노면이 0 인 임도는 성립하지 않으므로 표가 안 선 것이다. if area <= 0: return { - "group": "준비공", - "item": "표토제거", - "unit": "㎥", + **base, "amount": None, "status": STATUS_PENDING, "reason": ( - "대상 면적이 0 ㎡ 입니다 — 사면표(면고르기 면적)가 아직 서지 않았습니다. " - "0 ㎥ 로 내면 「표토가 없는 노선」으로 읽히므로 값을 세우지 않습니다" + "대상 면적이 0 ㎡ 입니다 — 횡단·사면표가 아직 서지 않았습니다. " + "0 ㎡ 로 내면 「표토가 없는 노선」으로 읽히므로 값을 세우지 않습니다" ), - "work_item_code": "FP-09-15", } - if thickness_m is None or float(thickness_m) <= 0: + if road_only: return { - "group": "준비공", - "item": "표토제거", - "unit": "㎥", - "amount": None, - "status": STATUS_PENDING, - "reason": ( - "표토 두께가 아직 입력되지 않았습니다 — 두께는 설계가 정하는 값입니다. " - f"{TOPSOIL_ORIGINAL_APPLIED} " - f"산출 조건에서 두께를 넣으면 값이 섭니다 (대상 면적 {area:,.1f}㎡)" - ), - "reference_amount": area, - "work_item_code": "FP-09-15", + **base, + "amount": area, + "status": STATUS_READY, + "reason": f"노면 {area:,.1f}㎡ (노면만 — 산출 조건에서 고름)" + f" — {TOPSOIL_ROAD_ONLY_BASIS}", } - thickness = float(thickness_m) return { - "group": "준비공", - "item": "표토제거", - "unit": "㎥", - "amount": area * thickness, + **base, + "amount": area, "status": STATUS_READY, - "reason": (f"사면적 {area:,.1f}㎡ × 두께 {thickness:g}m (품셈 9-15) · {TOPSOIL_AREA_GAP}"), - "work_item_code": "FP-09-15", + "reason": ( + f"노면 {float(road_area_m2):,.1f}㎡ + 절토 사면 {cut:,.1f}㎡ — {TOPSOIL_TARGET_BASIS}" + ), } @@ -213,19 +221,22 @@ TOPSOIL_ORIGINAL_APPLIED = ( " — 값을 자동으로 넣지 않되, 원문에 있는 값이므로 참고할 것" ) -#: ⚠⚠ **대상 면적이 법 문언과 어긋난다**(2026-09-09 준비공 축 감사에서 잡음). -#: 별표2 Ⅰ.2.차.(6) 은 대상을 **「노면·절토대상지」**로 못박고, 성토대상지는 (7) 에서 -#: 「표토 등은 **제거·정리**한다」로 따로 두어 **운반·적치 의무를 안 건다.** -#: 우리는 `face_dressing_cut + face_dressing_fill` 로 **절토·성토 사면적을 다 더하고** -#: **노면(연장 × 노폭)은 안 센다** — 두 방향으로 어긋난 셈이다. -#: ㉡ 계열(「또는」을 다 더함) — 성토 사면까지 운반·적치 물량에 실림 -#: 빠짐 — 노면 표토가 아예 안 섬(법이 첫째로 든 대상) -#: ⚠ **값을 임의로 고치지 않는다** — 실무가 사면적 한 덩이로 잡는지, 노면을 따로 잡는지가 -#: 사용자·실무자 확인 사항이다. 지금은 **사유로 드러내기만** 한다. -TOPSOIL_AREA_GAP = ( - "⚠ 대상 면적 확인 필요 — 별표2 는 「노면·절토대상지」로 두는데 우리는 절토·성토" - " **사면적을 다 더하고 노면은 안 셈**. 성토대상지는 별표2 (7) 이 「제거·정리」로만 두어" - " 운반·적치 의무를 안 검. 실무 확인 뒤 대상을 확정할 것" +#: 표토제거 공종 — **답외구간**(9-15-2). 임도는 논(畓)이 아니고 실무 영월도 「답외구간」으로 셈. +TOPSOIL_CODE = "FP-09-15-02" +#: ⭐ 2026-09-13 판정 「실무대로」 — 대상·밑수·압토 셋의 근거(화면 사유에 그대로 보임). +#: 종전(2026-09-09 감사)엔 절토·성토 **사면적을 다 더하고 노면은 안 세** 법 문언과 두 방향으로 +#: 어긋나 있었고 사유로만 드러내고 있었다. +TOPSOIL_TARGET_BASIS = ( + "대상은 별표2 Ⅰ.2.차.(6) 「노면·절토대상지」 · 성토대상지는 (7) 「제거·정리」라 안 셈" + " · 밑수 ㎡(품셈 9-15-2 가 T=0.2m 를 품 조건으로 둔 면적 축" + " · 실무 영월 「표토제거 답외구간 M2」)" + " · 20m 압토는 제거 품에 듦(2026-09-13 판정 「실무대로」)" +) +#: 노면만 — 설계자가 고른 경우의 근거. ⚠ 법 문언이 아니라는 사실을 사유에 함께 적는다. +TOPSOIL_ROAD_ONLY_BASIS = ( + "⚠ 법 문언(별표2 Ⅰ.2.차.(6))은 「노면·절토대상지」라 기본은 노면 + 절토" + " · 노면만은 실무 영월 한 건(1m당 3.34㎡) 관측이라 설계자 선택" + " · 밑수 ㎡ · 20m 압토는 제거 품에 듦" ) #: 표토 운반·적치 — ⚠ **법이 요구하는데 우리가 제거만 세고 있던 자리**(2026-09-09). @@ -237,10 +248,11 @@ TOPSOIL_AREA_GAP = ( TOPSOIL_HAUL_LAW = ( "⚠ 법정 의무 — 시행규칙 별표2 「표토는 전량 제거한 후 … 최고 홍수위보다 높은 장소로" " 운반하고 쌓아두어야 한다」. 제거만 세면 운반이 빠짐" - " · ⚠⚠ **다만 제거 품(9-15-2)에 이미 L=20m 압토가 들어 있음** — 실무 내역서도" - " 「표토제거 답외구간 / M2」 한 줄뿐이고 운반 줄이 따로 없음. 이 줄은 **20m 를 넘는" - " 몫**일 때만 새 줄이며, 그 가름을 아직 안 함(확인 필요)" + " · 제거 품(9-15-2)에 L=20m 압토가 들어 있어 이 줄은 **20m 를 넘는 몫**만 셈" + " (실무 영월 「표토제거 답외구간 / M2」 한 줄 · 운반 줄 없음)" ) +#: 제거 품이 품은 압토 거리(m) — 품셈 9-15-2 원문 적용값 `L(운반거리) 20m`. +TOPSOIL_PUSH_INCLUDED_M = 20.0 #: 제근 — ⚠ **본수가 아니라 임목축적으로 갈린다**(2026-09-09 원문 확인). @@ -401,26 +413,56 @@ def _root_steps_rows( ] -def _topsoil_haul_row(topsoil: dict[str, Any], distance_m: float | None) -> dict[str, Any]: - """표토 운반 — 물량은 제거 물량 그대로, 거리는 설계 입력. +def _topsoil_haul_row( + topsoil: dict[str, Any], thickness_m: float | None, distance_m: float | None +) -> dict[str, Any]: + """표토 운반 — 부피는 **제거 면적 × 두께**, 거리는 설계 입력에서 **20m 압토를 뺀 몫**. - ⚠ 제거 줄이 안 서면(두께 미입력) 운반도 안 선다 — 밑수가 그 줄이기 때문이다. + ⚠ 제거 줄이 안 서거나 두께가 비면 운반도 안 선다 — 0 으로 때우지 않는다. """ - amount = topsoil.get("amount") + area = topsoil.get("amount") distance = None if distance_m is not None: try: distance = float(distance_m) except (TypeError, ValueError): distance = None - if amount is None: + if area is None: return { "group": "준비공", "item": "표토 운반·적치", "unit": "㎥", "amount": None, "status": STATUS_PENDING, - "reason": f"{TOPSOIL_HAUL_LAW} · 제거 물량이 아직 안 서서 운반도 못 셈(두께 먼저)", + "reason": f"{TOPSOIL_HAUL_LAW} · 제거 면적이 아직 안 서서 운반도 못 셈", + "work_item_code": None, + } + if thickness_m is None or float(thickness_m) <= 0: + return { + "group": "준비공", + "item": "표토 운반·적치", + "unit": "㎥", + "amount": None, + "status": STATUS_PENDING, + "reason": ( + f"{TOPSOIL_HAUL_LAW} · 운반 부피 = 제거 면적 × 표토 두께 — 두께가 아직 입력되지" + f" 않았습니다. {TOPSOIL_ORIGINAL_APPLIED} (제거 면적 {float(area):,.1f}㎡)" + ), + "reference_amount": float(area), + "work_item_code": None, + } + amount = float(area) * float(thickness_m) + if distance is not None and 0 < distance <= TOPSOIL_PUSH_INCLUDED_M: + return { + "group": "준비공", + "item": "표토 운반·적치", + "unit": "㎥", + "amount": None, + "status": STATUS_COUNTED_ELSEWHERE, + "reason": ( + f"{TOPSOIL_HAUL_LAW} · 운반거리 {distance:g}m 는 제거 품의 20m 압토 안 — 따로 안 셈" + ), + "reference_amount": amount, "work_item_code": None, } if distance is None or distance <= 0: @@ -438,19 +480,22 @@ def _topsoil_haul_row(topsoil: dict[str, Any], distance_m: float | None) -> dict "reference_amount": float(amount), "work_item_code": None, } + beyond = distance - TOPSOIL_PUSH_INCLUDED_M return { "group": "준비공", "item": "표토 운반·적치", "unit": "㎥", - "amount": float(amount), + "amount": amount, "status": STATUS_READY, "reason": ( - f"{TOPSOIL_HAUL_LAW} · 제거 물량 그대로 · 운반거리 {distance:g}m(설계 입력)" + f"{TOPSOIL_HAUL_LAW} · 제거 면적 {float(area):,.1f}㎡ × 두께 {float(thickness_m):g}m" + f" · 운반거리 {distance:g}m(설계 입력) − 압토 {TOPSOIL_PUSH_INCLUDED_M:g}m" + f" = {beyond:g}m" " · 갈래 「토사」로 보냄(표토는 토사임 — 덤프 운반 10-12 는 토사/암절취/발파암으로" " 갈리고 부모 코드에는 품이 없음)" ), "work_item_code": "FP-10-12", - "haul_distance_m": distance, + "haul_distance_m": beyond, # ⚠ `FP-10-12` 는 **부모**다 — 품이 붙은 것은 잎(10-12-1 토사 · -2 암절취 · -3 발파암) # 뿐이라 갈래를 안 보내면 B09 가 「후보 3건」만 보이고 금액이 안 선다. 표토가 토사인 # 것은 다툼이 없으므로 여기서 갈래를 실어 보낸다(2026-09-09 마스터 대조에서 잡음). @@ -620,6 +665,8 @@ def build_table( stand_volume_class: str | None = None, chipping_enabled: Any = False, chipping_volume_m3: Any = None, + road_surface_area_m2: float | None = None, + topsoil_road_only: bool = False, ) -> dict[str, Any]: """화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**""" rows = ( @@ -629,6 +676,8 @@ def build_table( topsoil_thickness_m, topsoil_haul_distance_m, stand_volume_class, + road_surface_area_m2, + topsoil_road_only, ) + erosion_rows(structures, names) + chipping_rows(chipping_enabled, chipping_volume_m3) diff --git a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py index 5594d5782..5256c5261 100644 --- a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py +++ b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py @@ -69,6 +69,9 @@ class StationSlope: # ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은 # 가장 높은 쪽이 기준이고, 양쪽을 더하면 실제보다 두 배가 된다. fill_height_m: float = 0.0 + #: 노면 폭(m) — 노체 끝(`road_edges`) 좌우 사이. 표토제거 대상 「노면」 면적의 밑수(별표2). + #: 노체 끝이 저장에 없으면 `None` — 0 으로 때우지 않는다. + roadbed_width_m: float | None = None segments: tuple[SlopeSegment, ...] = () # 사면이 샘플 범위 끝까지 원지반을 못 만나 **면적이 잘린** 측점. # 설계 엔진이 `slope_unclosed` 로 이미 경고하는 값을 그대로 물고 온다. 잘린 측점은 @@ -207,6 +210,25 @@ def _material(design: dict[str, Any], ratio: float) -> str | None: return "rock" if abs(ratio - rock) < abs(ratio - soil) else "soil" +def _roadbed_width(design: dict[str, Any]) -> float | None: + """노면 폭 — 노체 끝 좌(+)·우(−) 오프셋 사이. 한쪽이라도 없으면 `None`.""" + edges = design.get("road_edges") or {} + left = _num((edges.get("left") or {}).get("offset_m")) + right = _num((edges.get("right") or {}).get("offset_m")) + return None if left is None or right is None else abs(left - right) + + +def road_surface_area(slopes: Iterable[StationSlope]) -> float | None: + """노면 면적(㎡) — 사면과 같은 평균단면적법. 노면 폭이 빈 측점이 하나라도 있으면 `None`.""" + ordered = sorted(slopes, key=lambda s: s.chainage_m) + if not ordered or any(s.roadbed_width_m is None for s in ordered): + return None + return sum( + (float(a.roadbed_width_m) + float(b.roadbed_width_m)) / 2.0 * (b.chainage_m - a.chainage_m) + for a, b in zip(ordered, ordered[1:]) + ) + + def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope: """측점 하나의 사면길이. 좌우를 합쳐 절토·성토 각각의 총 사면길이를 낸다.""" ratios = _ratios(design) @@ -225,6 +247,7 @@ def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope: # ⚠ 없는 측점은 0 이다 — 성토 사면길이로 **대신 채우지 않는다**(면이 다름). bench_cut_length_m=_num(design.get("bench_cut_length_m")) or 0.0, fill_height_m=max(fill_by_side.values(), default=0.0), + roadbed_width_m=_roadbed_width(design), berm_width_m=_num(berm.get("width_m")) or 0.0, segments=tuple(segments), unclosed=bool(design.get("slope_unclosed")), diff --git a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py index 6a89f027f..e23e3f9ac 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py @@ -111,11 +111,17 @@ def _write(folder: Path, item: dict[str, Any]) -> None: (folder / f"{item['code']}.json").write_text(text, encoding="utf-8") -def save_personal(folder: Path, template: dict[str, Any], overrides: dict[str, Any] | None) -> str: - """[내 라이브러리에 저장] — 양식 + 프로젝트에서 고친 식을 **개인 단에 한 벌**로 씀. 코드. +def save_personal( + folder: Path, + template: dict[str, Any], + overrides: dict[str, Any] | None, + unit_price_rows: list[dict[str, Any]] | None = None, +) -> str: + """[내 라이브러리에 저장] — 양식 + 고친 식·줄 조합을 **개인 단에 한 벌**로 씀. 코드. ⚠ 반대 방향(작업본 → 개인 단)이라 프로젝트는 안 바꿈(브레인 판정 Ⓑ). ⚠ 개인 단에 같은 종류가 있으면 **그 코드로 덮어씀** — 지금은 종류당 하나(판정 Ⓐ). + ⛔ 수동 단가는 안 실음 — 프로젝트의 값이라 양식에 실으면 남의 프로젝트로 감(브레인 판정). """ from B08_Quantity.B08_Quantity_Engine_StructureTemplate import overridden_rows @@ -129,6 +135,8 @@ def save_personal(folder: Path, template: dict[str, Any], overrides: dict[str, A for row in overridden_rows(template, overrides) ] item = {k: v for k, v in template.items() if k != "imported_from"} + if unit_price_rows is not None: + item["unit_price"] = {**(item.get("unit_price") or {}), "rows": unit_price_rows} _write(folder, {**item, "code": code, "library_tier": "personal", "rows": rows}) return code diff --git a/B08_Quantity/B08_Quantity_Engine_StructurePriceLink.py b/B08_Quantity/B08_Quantity_Engine_StructurePriceLink.py new file mode 100644 index 000000000..af6fc4f6f --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_StructurePriceLink.py @@ -0,0 +1,169 @@ +"""구조물도 일위대가 → B09 내역 **받는 문** (PLAN 6장 첫 일감 ② · 2026-09-13 브레인 판정). + +⭐ 종전엔 구조물도가 세운 일위대가(`B-AX-ST-*`)가 인계로 **한 번도 안 나갔음** — 돌쌓기는 + `FP-13-04-05` ㎡ 단가만 가고 모르터·기초잡석은 따로거나 빠졌음. 사슬의 진짜 빈칸. + +한 벌로 두는 것 — 인계(B08)와 내역(B09)이 **같은 목록·같은 키**를 봄 + 양식 일위대가로 셀 장 → `priced_sheets` (장마다 코드 · 제원 키 · 구성원 · 품은 줄) + 인계 줄 → `template_row` (코드 AX-ST · 갈래 = 제원 키 · 수량 = 연장 m) + 내역 금액 → `price_sheets` (B09 단가표로 B08 일위대가 엔진을 돌림 — 코드 한 벌) + +⚠ 호표 코드 `B-AX-ST-<8hex>#<키>` — 키는 **사람이 읽는 제목이 아니라** 양식 제원 칸 값을 칸 이름 + 차례로 이은 것(연장 L 뺌) + `normalize_variant_key`. 제목 글자가 바뀌어도 코드는 안 바뀜. +⚠ 양식 일위대가가 품은 원단위 줄(돌쌓기·모르터·기초잡석)은 **따로 인계하지 않음** — `covered`. +⚠ 금액 정본은 B09 — 여기 `price_sheets` 는 B09 가 넘긴 단가표로만 셈. 수동 단가도 입력으로 씀. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import Any + +#: 인계 줄 갈래 축 이름 — 「구조물도 장 제원」. +VARIANT_AXIS = "structure_sheet" + + +def _key_value(value: Any) -> str: + if value is None: + return "" + if isinstance(value, (int, float)) and not isinstance(value, bool): + return format(float(value), ".6g") + return str(value).strip() + + +def sheet_variant_key(template: dict[str, Any], sheet: dict[str, Any]) -> str: + """장의 **제원 키** — 양식 칸 이름 차례로 `이름=값` 을 `|` 로 이음(연장 칸 뺌: m당).""" + values = (sheet.get("formula_sheet") or {}).get("vars") or {} + specs = template.get("vars") or {} + return "|".join( + f"{name}={_key_value(values.get(name))}" + for name in sorted(specs) + if specs[name].get("source") != "length_m" + ) + + +def structure_price_code(code: str, key: str) -> str: + """호표 코드 `B-AX-ST-<8hex>#<정규화 키>` — 인계·내역이 이것 하나로 만남.""" + from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key + + return f"B-{code}#{normalize_variant_key(key)}" + + +def priced_sheets( + sheets: Iterable[dict[str, Any]], + templates: dict[str, dict[str, Any]], + row_edits: dict[str, Any] | None = None, +) -> list[dict[str, Any]]: + """양식 일위대가로 셀 장 — 양식(고친 줄 조합 얹음)에 일위대가 줄과 코드가 있는 장만.""" + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import with_rows + + found = [] + for sheet in sheets: + type_id = str(sheet.get("type_id") or "") + base = templates.get(type_id) + if not base or not base.get("code") or not sheet.get("formula_sheet"): + continue + template = with_rows(base, (row_edits or {}).get(type_id)) + rows = template["unit_price"]["rows"] + if not rows: + continue + names = {row.get("seq"): str(row.get("name") or "") for row in template.get("rows") or []} + found.append( + { + "type_id": type_id, + "code": str(template["code"]), + "name": str(template.get("name") or type_id), + "title": str(sheet.get("title") or ""), + "key": sheet_variant_key(template, sheet), + "structure_ids": [ + str(member["structure_id"]) + for member in sheet.get("members") or [] + if member.get("structure_id") + ], + # 일위대가가 품은 원단위 줄 이름 — 따로 인계하면 두 번 셈. + "covered": sorted( + {names[r["from_row"]] for r in rows if r.get("from_row") in names} + ), + "template": template, + "sheet": sheet, + } + ) + return found + + +def by_structure(entries: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {sid: entry for entry in entries for sid in entry["structure_ids"]} + + +def template_row(row: dict[str, Any], structure: dict[str, Any], entry: dict[str, Any]) -> dict: + """전개 인계 줄(같은 칸 29개 계약)을 **양식 일위대가 줄**로 바꿔 씀 — 코드·갈래·수량·단위만.""" + length = float(structure.get("length_m") or 0.0) + in_bill = length > 0 + return { + **row, + "work_item_code": entry["code"], + "name": entry["name"], + "spec": entry["title"], + "spec_detail": entry["title"], + "unit": "m", + "quantity": length, + "composite_parts": None, + "composite_not_ready": None, + "structure_kind": None, + "variant_axis": VARIANT_AXIS, + "variant_value": entry["key"], + "secondary_axes": None, + "spec_class": None, + "spec_class_basis": f"구조물도 양식 일위대가 — 품은 줄: {'·'.join(entry['covered'])}", + # 막힘은 일위대가 표가 정함(B09 가 셈) — 전개 쪽 갈래 사유는 여기 안 실음. + "blocked_kind": None if in_bill else row.get("blocked_kind"), + "blocked_reason": "" if in_bill else row.get("blocked_reason") or "연장이 0 이라 안 세움", + "in_bill": in_bill, + "in_bill_reason": "" if in_bill else "연장이 0 이라 내역에 안 세움", + } + + +def price_sheets( + entries: Iterable[dict[str, Any]], + book: Any, + find_variant: Callable[[str, str], str | None], + library: Iterable[dict[str, Any]], + manual_all: dict[str, Any] | None = None, +) -> dict[str, dict[str, Any]]: + """B09 내역이 쓸 호표 금액 `{B-AX-ST-…#키: {money, total, blocked, unconfirmed, reasons}}`. + + ⚠ `money` 는 **호표 표의 성분 소계**(칸마다 0.1원 절사한 합) · `total` 은 계금(1원 절사) — + 명세 7장 「호표 안 성분 소계는 절사」. 안 자른 값을 쓰면 구조물도 화면(192,429)과 + 내역 단가(192,430)가 1원 갈림(2026-09-13 검증 프로젝트 실측). + """ + from decimal import Decimal + + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import unit_price_money + from B09_Estimation.B09_Estimation_PriceBook import Money3 + + prices: dict[str, dict[str, Any]] = {} + library = list(library) + for entry in entries: + assembled = unit_price_money( + entry["template"], + entry["sheet"], + book, + find_variant, + library, + (manual_all or {}).get(entry["type_id"]) or {}, + ) + if assembled is None: + continue + table, _exact = assembled + prices[structure_price_code(entry["code"], entry["key"])] = { + "money": Money3(*(Decimal(str(table[k])) for k in ("material", "labor", "expense"))), + "total": Decimal(str(table["total"])), + "blocked": table["blocked"], + "unconfirmed": table["unconfirmed"], + "reasons": [ + f"{row['name']}: {row['reason']}" + for row in table["rows"] + if row["reason"] and not row["skipped"] and "total" not in row + ], + } + return prices diff --git a/B08_Quantity/B08_Quantity_Engine_StructureSummary.py b/B08_Quantity/B08_Quantity_Engine_StructureSummary.py new file mode 100644 index 000000000..3020f0579 --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_StructureSummary.py @@ -0,0 +1,358 @@ +"""B08 **구조물 집계표** — 측점별 구조물 한 줄 · 종류별 표 (PLAN 2장 · 사용자 확정 ④). + +한 줄 = 측점 + 종류 + 실치수(제원 칸) + 개소·연장. 실무 「돌-골막이치수」 치수조서가 +실치수를 들고 구조도 탭이 끌어 쓰는 모양을 **한 탭으로 겸함**. + +값이 어디서 오나 — 칸마다 출처를 붙임 + auto 정본에 적힌 값 — `structures.json`(B05) · `pipe_points.json`(계곡 통과 시설) · + 관 연장은 B06 횡단 `design.pipe_length_m` + user 이 표에서 사람이 고쳐 **정본에 적은** 값 — 산출 조건에 「손댄 칸」 표만 둠(브레인 판정) + library 정본 칸이 비어 **양식 기본값**으로 선 칸(구조물도 양식 `vars.*.default`) + empty 정본도 양식도 값이 없음 — 계산 쪽이 막히거나 기준값으로 돎 +⚠ 사용자 손 값은 덮개층이 아니라 **정본에 씀** — 도면·수량이 한 값(지침 5장 「쪼개지 않음」). +⚠ 손댄 칸의 정본 값이 뒤에 B05 에서 바뀌면 그 칸은 자동으로 돌아가되 **조용히 말고** 줄에 알림. +⚠ 값을 여기서 짓지 않음 — 읽어 줄 세우기만. 연장이 없는 관은 빈칸(0 으로 안 채움). +⚠ 관 지점 정본 타입(`managed_by`)은 `structures.json` 에 있어도 안 셈 — `pipe_points.json` 이 주인. +⛔ 고칠 수 있는 칸은 **상세 칸(`phase: detail`)만** — 자리·길이·높이·관경 같은 놓기 칸(`b05`)은 + 시·종점·횡단 설계와 **한 벌로 움직여**(B05 에서 길이 = 전 + 후, 시·종점이 따라 섬) 여기서 한 칸만 + 고치면 쪼개짐(지침 5장). 그 칸은 구조물 놓기(B05)에서 고침. +""" + +from __future__ import annotations + +import math +from collections.abc import Iterable +from typing import Any + +SOURCE_AUTO = "auto" +SOURCE_USER = "user" +SOURCE_LIBRARY = "library" +SOURCE_EMPTY = "empty" +#: 산출 조건 자리 — 손댄 칸 `{줄 id: {칸: {"value": 적은 값, "was": 고치기 전 정본 값}}}`. +USER_CELLS_KEY = "structure_summary_user_cells" + +#: 계곡 통과 시설 `facility` → 레지스트리 종류. 빈 값은 배관(`PipePoint` 기본). +FACILITY_TYPES = { + "pipe": "pipe", + "box_culvert": "box_culvert", + "ford_pavement": "ford_pavement", + "ford_bridge": "ford_bridge", + "revetment": "revetment", +} + + +def _blank(value: Any) -> bool: + return value is None or value == "" + + +def _number(value: Any) -> float | None: + if isinstance(value, bool) or _blank(value): + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _defaults(template: dict[str, Any] | None) -> dict[str, Any]: + """양식 제원 칸 기본값 `{옵션 키: 기본값}` — 정본 칸이 비었을 때 계산이 쓰는 값.""" + return { + str(spec["option"]): spec["default"] + for spec in ((template or {}).get("vars") or {}).values() + if spec.get("option") and "default" in spec and not _blank(spec["default"]) + } + + +def _same(left: Any, right: Any) -> bool: + """정본 값과 적은 값이 같은가 — 숫자는 3.0 과 "3" 을 같게 봄.""" + a, b = _number(left), _number(right) + return a == b if a is not None and b is not None else left == right + + +def _cells( + keys: Iterable[str], + options: dict[str, Any], + defaults: dict[str, Any], + marks: dict[str, Any], +) -> dict: + cells = {} + for key in keys: + current = None if _blank(options.get(key)) else options[key] + mark = marks.get(key) + if mark is not None and not _same(current, mark.get("value")): + # 사람이 적은 값을 B05 가 바꿈 — 자동으로 돌아가되 옛 값을 함께 실어 화면이 알림. + cells[key] = { + "value": current, + "source": SOURCE_EMPTY if current is None else SOURCE_AUTO, + "replaced_user_value": mark.get("value"), + } + elif mark is not None: + cells[key] = {"value": current, "source": SOURCE_USER, "was": mark.get("was")} + elif current is not None: + cells[key] = {"value": options[key], "source": SOURCE_AUTO} + elif key in defaults: + cells[key] = {"value": defaults[key], "source": SOURCE_LIBRARY} + else: + cells[key] = {"value": None, "source": SOURCE_EMPTY} + return cells + + +def build_summary( + structures: Iterable[dict[str, Any]], + pipe_points: Iterable[dict[str, Any]], + types: dict[str, Any], + templates: dict[str, dict[str, Any]] | None = None, + pipe_lengths: dict[float, float] | None = None, + user_cells: dict[str, dict[str, Any]] | None = None, +) -> dict[str, Any]: + """종류별 표 목록(레지스트리 차례) — 표마다 칸 정의·줄·개소·연장 합·평균치수. + + `types` 는 `structure_type_map()` · `templates` 는 종류별 양식(프로젝트에 박힌 것 → 기본) · + `pipe_lengths` 는 B06 횡단의 `{측점: 관 연장}`(`pipe_lengths_from_designs`) · + `user_cells` 는 이 표에서 손댄 칸(`USER_CELLS_KEY` 모양). + """ + # 관 길이 찾기는 배수관 물량과 **같은 규칙**(허용 0.5m) — 두 표가 같은 관에 다른 연장을 안 적게. + from B08_Quantity.B08_Quantity_Engine_Pipe import _nearest + + templates = templates or {} + lengths = pipe_lengths or {} + rows_by_type: dict[str, list[dict[str, Any]]] = {} + notes: list[str] = [] + + def add(type_id: str, row: dict[str, Any]) -> None: + definition = types.get(type_id) + if definition is None: + notes.append(f"{type_id}: 레지스트리에 없는 종류라 안 셈") + return + options = row.pop("options") + keys = [field.key for field in definition.options] + marks = (user_cells or {}).get(str(row["id"])) or {} + row["cells"] = _cells(keys, options, _defaults(templates.get(type_id)), marks) + replaced = [key for key, cell in row["cells"].items() if "replaced_user_value" in cell] + if replaced: + labels = {field.key: field.label for field in definition.options} + row["replaced"] = [labels[key] for key in replaced] + if definition.design_owner: + row["note"] = f"{definition.design_owner}가 수량을 셈 — 여기선 자리만" + elif definition.reference_only: + row["note"] = "전문 상세설계 대상 — 배치까지만" + rows_by_type.setdefault(type_id, []).append(row) + + for item in structures: + type_id = str(item.get("type_id") or "") + definition = types.get(type_id) + if definition is not None and definition.managed_by: + continue # 관 지점 정본이 주인 — 옛 저장분이 남아 있어도 두 번 안 셈 + options = dict(item.get("options") or {}) + start, end = item.get("start_m"), item.get("end_m") + span = abs(float(end) - float(start)) if start is not None and end is not None else None + stated = _number(options.get("length_m")) + add( + type_id, + { + "id": item.get("structure_id"), + "origin": "structures", + "chainage_m": item.get("chainage_m"), + "start_m": start, + "end_m": end, + "count": 1, + # 원단위 전개와 같은 규칙 — 제원 연장이 있으면 그것, 없으면 시·종점 거리. + "length_m": stated if stated else span, + "length_basis": "제원 연장" if stated else ("시·종점" if span else ""), + "memo": item.get("memo") or "", + "options": options, + }, + ) + + for point in pipe_points: + facility = str(point.get("facility") or "pipe") + type_id = FACILITY_TYPES.get(facility, facility) + chainage = float(point.get("chainage_m") or 0.0) + length = _nearest(lengths, chainage) if type_id == "pipe" else None + add( + type_id, + { + "id": pipe_row_id(point), + "origin": "pipe_points", + "chainage_m": chainage, + "start_m": point.get("start_m"), + "end_m": point.get("end_m"), + "count": 1, + "length_m": length, + "length_basis": "B06 횡단 관 연장" if length else "", + "memo": "", + "options": dict(point.get("options") or {}), + }, + ) + + tables = [] + for type_id, definition in types.items(): + rows = rows_by_type.get(type_id) + if not rows: + continue + rows.sort(key=lambda row: float(row.get("chainage_m") or row.get("start_m") or 0.0)) + columns = [ + { + "key": f.key, + "label": f.label, + "unit": f.unit or "", + "input": f.input, + "choices": list(f.choices), + "editable": f.phase == "detail" and f.enabled, + } + for f in definition.options + ] + averages = {} + for column in columns: + values = [_number(row["cells"][column["key"]]["value"]) for row in rows] + present = [v for v in values if v is not None] + if present and column["input"] == "number": + averages[column["key"]] = sum(present) / len(present) + replaced = sum(1 for row in rows if row.get("replaced")) + if replaced: + notes.append( + f"{definition.name} {replaced}줄 — 이 표에서 고친 값을 B05 가 바꿔 " + "자동값으로 돌아감" + ) + measured = [row["length_m"] for row in rows if row["length_m"]] + # 연장이 뜻 있는 표만 빈 연장을 셈 — 반사경·표지판 같은 점 시설은 연장이 없는 것이 정상. + needs_length = definition.placement == "interval" or type_id == "pipe" + tables.append( + { + "type_id": type_id, + "name": definition.name, + "group": definition.group, + "placement": definition.placement, + "columns": columns, + "rows": rows, + "count": len(rows), + "length_total_m": sum(measured) if measured else None, + "length_missing": len(rows) - len(measured) if needs_length else 0, + "averages": averages, + } + ) + return {"tables": tables, "notes": notes} + + +def pipe_row_id(point: dict[str, Any]) -> str: + """계곡 통과 시설 줄 id — 관 지점엔 식별자가 없어 기준점(㎜ 자리)으로 가름.""" + return f"pipe@{float(point.get('chainage_m') or 0.0):.3f}" + + +def _type_of(target: dict[str, Any], is_pipe: bool) -> str: + if is_pipe: + facility = str(target.get("facility") or "pipe") + return FACILITY_TYPES.get(facility, facility) + return str(target.get("type_id") or "") + + +def coerce(field: Any, value: Any, name: str) -> Any: + """칸 값 꼴 맞추기 — B05 저장 관문과 같은 규칙(수 칸은 0 이상 수 · 고르기 칸은 보기 안). + + 빈 값(None·"")은 `None` = 그 칸을 지움. 틀리면 `ValueError`. + """ + if _blank(value): + return None + if field.input == "number": + number = _number(value) + if number is None or not math.isfinite(number) or number < 0: + raise ValueError(f"{name} {field.label}: 0 이상의 수여야 함 — {value!r}") + return int(number) if number.is_integer() else number + # 수로 온 보기 값(뒷길이 35 → 35.0)은 보기 글과 같은 꼴로 — 「35.0」은 보기 「35」와 안 맞음. + whole = isinstance(value, float) and value.is_integer() + text = str(int(value)) if whole else str(value).strip() + if field.input == "select" and field.choices and text not in field.choices: + raise ValueError(f"{name} {field.label}: 보기에 없는 값 — {text}") + return text + + +def apply_edits( + structures: list[dict[str, Any]], + points: list[dict[str, Any]], + types: dict[str, Any], + edits: Iterable[dict[str, Any]], + marks: dict[str, dict[str, Any]], +) -> tuple[list[str], dict[str, dict[str, Any]]]: + """손 고침을 **정본 사본에 바로** 얹음(제자리 바꿈) — (바뀐 줄 id, 새 손댄 칸 표). + + 고치기 전 값(`was`)은 처음 손댄 때의 정본 값을 지킴 — 그 값으로 되돌리면 표에서 빠져 「자동」. + 놓기 칸·없는 줄·틀린 값은 `LookupError`/`ValueError` — 부르는 쪽이 아무것도 안 씀. + """ + from B08_Quantity.B08_Quantity_Engine_StructureSheet_Edit import EDITABLE_KEYS as SHEET_KEYS + from B08_Quantity.B08_Quantity_Engine_StructureSheet_Edit import clean_spec + + rows = {str(item.get("structure_id")): (item, False) for item in structures} + rows |= {pipe_row_id(point): (point, True) for point in points} + marks = {row_id: dict(cells) for row_id, cells in marks.items()} + changed: list[str] = [] + for edit in edits: + row_id, key = str(edit.get("id")), str(edit.get("key")) + if row_id not in rows: + raise LookupError(f"집계표 줄을 못 찾음: {row_id}") + target, is_pipe = rows[row_id] + definition = types.get(_type_of(target, is_pipe)) + field = next((f for f in definition.options if f.key == key), None) if definition else None + if field is None: + raise ValueError(f"{row_id}: 그 종류에 없는 칸 — {key}") + if field.phase != "detail": + raise ValueError( + f"{definition.name} {field.label}: 자리·길이·높이 같은 놓기 칸은 " + "시·종점과 한 벌이라 구조물 놓기(B05)에서 고침" + ) + value = coerce(field, edit.get("value"), definition.name) + if value is not None and key in SHEET_KEYS: + # 구조물도 [제원 저장]과 **같은 꼴**로 적음(뒷길이는 정수 등) — 두 길의 꼴이 같아야 함. + # ⚠ 검사(`coerce`)를 먼저 — `clean_spec` 은 못 읽은 값을 빼 버려 「지우기」로 읽힘. + value = clean_spec({key: value})[0].get(key, value) + options = dict(target.get("options") or {}) + old = None if _blank(options.get(key)) else options[key] + if (value is None and old is None) or (value is not None and _same(old, value)): + continue + if value is None: + options.pop(key, None) + else: + options[key] = value + target["options"] = options + cells = marks.setdefault(row_id, {}) + mark = cells.get(key) + # 옛 손 표가 정본과 같을 때만 그 「고치기 전」을 이어 씀 — B05 가 바꾼 뒤면 지금 값이 기준. + was = mark.get("was") if mark and _same(mark.get("value"), old) else old + if _same(was, value): + cells.pop(key, None) + else: + cells[key] = {"value": value, "was": was} + if not cells: + marks.pop(row_id) + if row_id not in changed: + changed.append(row_id) + return changed, marks + + +def prune_marks( + marks: dict[str, dict[str, Any]], + structures: Iterable[dict[str, Any]], + points: Iterable[dict[str, Any]], +) -> dict[str, dict[str, Any]]: + """[저장] 때 손댄 칸 표 정리 — 없어진 줄과 **B05 가 이미 바꾼 칸**(알림을 본 뒤)은 뺌.""" + current = {str(item.get("structure_id")): item for item in structures} + current |= {pipe_row_id(point): point for point in points} + kept: dict[str, dict[str, Any]] = {} + for row_id, cells in marks.items(): + if row_id not in current: + continue + options = current[row_id].get("options") or {} + alive = { + key: mark + for key, mark in cells.items() + if _same(None if _blank(options.get(key)) else options[key], mark.get("value")) + } + if alive: + kept[row_id] = alive + return kept + + +def pipe_lengths_from_designs(designs: Iterable[dict[str, Any]]) -> dict[float, float]: + """저장된 횡단 설계 → `{측점: 관 연장}` — 배수관 물량과 같은 읽기.""" + from B08_Quantity.B08_Quantity_Engine_Pipe import _length_by_chainage + + return _length_by_chainage(list(designs or []), "pipe_length_m") diff --git a/B08_Quantity/B08_Quantity_Engine_StructureUnitPrice.py b/B08_Quantity/B08_Quantity_Engine_StructureUnitPrice.py index 686719202..562d72005 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureUnitPrice.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureUnitPrice.py @@ -8,6 +8,10 @@ 일위대가를 먼저 세우고 그 단위당 금액을 씀. 레시피 150/350 이 2단 이상(명세 16장). 깊이 **5단**까지(PLAN 10장 판정) · 돌면 막힘. B-FP·X·L 쪽 재귀는 단가표가 이미 함. ⛔ 하위 양식은 **프로젝트에 박힌 것과 프로그램 기본**에서만 찾음 — 개인·회사 단은 안 읽음(4장 Ⓑ). +⚠ **저장 자리는 둘**(브레인 판정 2026-09-13) — 줄 더하기·빼기·고르개로 고친 + **줄 조합은 양식+프로젝트**(`ROWS_KEY`, 종류별 · [내 라이브러리에 저장] 때 양식에 실림) · + **수동 단가는 프로젝트만**(`MANUAL_KEY` · 양식에 실으면 내 단가가 남의 프로젝트로 감). + 수동 단가 줄은 「미확정」으로 셈. """ from __future__ import annotations @@ -21,6 +25,15 @@ from typing import Any SUB_STRUCTURE_PREFIX = "B-AX-ST-" #: 재귀 깊이 한도 — 맨 윗 표가 1단(PLAN 10장 「재귀 깊이 = 5단」). MAX_DEPTH = 5 +#: 산출 조건 자리 — 고친 줄 조합 `{type_id: [rows]}` · 수동 단가 `{type_id: {seq: 값}}`. +ROWS_KEY = "structure_unit_price_rows" +MANUAL_KEY = "structure_manual_prices" +#: 고르개 갈래 — 품셈(일위대가·단가산출) · 자원(자재·노임·시간당 중기). +SEARCH_KINDS = { + "work": ("unit_price", "price_basis"), + "resource": ("material", "labor", "machine_hourly"), +} +_MONEY_KEYS = ("material", "labor", "expense") _UNIT_ALIASES = {"m2": "㎡", "m3": "㎥", "M2": "㎡", "M3": "㎥", "M": "m"} @@ -34,6 +47,8 @@ class _Context: book: Any find_variant: Callable[[str, str], str | None] library: dict[str, dict[str, Any]] + #: 맨 윗 표의 수동 단가 `{seq: 값}` — 하위 양식에는 안 씀(그 종류의 줄이 아님). + manual: dict[str, dict[str, Any]] @dataclass @@ -153,6 +168,7 @@ def _assemble( sums = {"material": Decimal(0), "labor": Decimal(0), "expense": Decimal(0)} exact = Money3() blocked = 0 + unconfirmed = 0 for row in spec.get("rows") or []: out: dict[str, Any] = { "seq": row.get("seq"), @@ -165,6 +181,15 @@ def _assemble( "reason": "", } source = sheet_rows.get(row.get("from_row")) if "from_row" in row else None + # 이중계상 경계 ①(명세 6장) — 토공집계·사토 공제·참고로 가는 줄을 일위대가에 또 넣지 않음. + from B08_Quantity.B08_Quantity_Engine_Handoff_Boundaries import NOT_BILLED_BY_WORK_ITEM + + owner = NOT_BILLED_BY_WORK_ITEM.get(str((source or {}).get("destination") or "")) + if owner: + out["reason"] = f"이중계상 — 원단위 줄 {row['from_row']} 은 {owner}로 가는 줄" + blocked += 1 + rows.append(out) + continue if source is not None and (source.get("skipped") or source.get("error")): # 원단위 줄이 안 선 장이면 일위대가 줄도 안 섬 — 막힘이 아님(버림 「안 넣음」 등). out.update(skipped=bool(source.get("skipped")), reason=source.get("reason") or "") @@ -193,7 +218,18 @@ def _assemble( ref, why = _ref_of(row, values, ctx.find_variant) out["ref_code"] = ref or "" priced = None - if ref: + manual = ctx.manual.get(str(row.get("seq"))) if depth == 1 else None + if manual: + # 수동 단가가 단가표보다 이김 — 사람이 일부러 넣은 값. 대신 「미확정」으로 셈. + money = Money3(*(Decimal(str(manual.get(key) or 0)) for key in _MONEY_KEYS)) + priced = _Priced(money, out["name"], out["spec"], out["unit"]) + out.update( + manual=True, + manual_source=manual.get("source") or "", + manual_entered_at=manual.get("entered_at") or "", + ) + unconfirmed += 1 + elif ref: try: priced = _price(ref, row, ctx, depth, seen) if out["unit"] and priced.unit and priced.unit != out["unit"]: @@ -238,6 +274,8 @@ def _assemble( "total": float(round_at(sum(sums.values()), OutputPlace.UNIT_PRICE_TOTAL)), "blocked": blocked, "complete": blocked == 0, + #: 수동 단가로 선 줄 수 — 화면 「미확정 N건」 배지. + "unconfirmed": unconfirmed, } return table, exact @@ -248,14 +286,104 @@ def unit_price_table( book: Any, find_variant: Callable[[str, str], str | None], library: Iterable[dict[str, Any]] = (), + manual: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any] | None: """장 하나의 일위대가 표. 양식에 `unit_price` 가 없으면 `None`. 수량 — `from_row`(원단위 줄 차례 → 그 줄의 단위당 값) 또는 박힌 `quantity`. 코드 — `ref_code`(단가표 코드 그대로) 또는 `work_item_code` + `variant_from`(제원 칸 → 갈래). `library` — 하위 구조물 일위대가(`B-AX-ST-*`)를 찾을 양식들(프로젝트에 박힌 것 + 프로그램 기본). + `manual` — 이 프로젝트의 수동 단가 `{seq: {material, labor, expense, source, entered_at}}`. """ - ctx = _Context(book, find_variant, {str(t["code"]): t for t in library if t.get("code")}) - code = str(template.get("code") or "") - assembled = _assemble(template, sheet, ctx, 1, (code,) if code else ()) + assembled = unit_price_money(template, sheet, book, find_variant, library, manual) return assembled[0] if assembled else None + + +def unit_price_money( + template: dict[str, Any], + sheet: dict[str, Any], + book: Any, + find_variant: Callable[[str, str], str | None], + library: Iterable[dict[str, Any]] = (), + manual: dict[str, dict[str, Any]] | None = None, +) -> tuple[dict[str, Any], Any] | None: + """`unit_price_table` 과 같되 **안 자른 단위당 3분할**도 — B09 내역 줄이 이 값에 수량을 곱함.""" + ctx = _Context( + book, find_variant, {str(t["code"]): t for t in library if t.get("code")}, manual or {} + ) + code = str(template.get("code") or "") + return _assemble(template, sheet, ctx, 1, (code,) if code else ()) + + +def with_rows(template: dict[str, Any], rows: list[dict[str, Any]] | None) -> dict[str, Any]: + """고친 줄 조합을 얹은 양식 — 고친 적 없으면 양식 줄. 줄 없는 양식도 빈 표로 세움.""" + spec = template.get("unit_price") or {} + picked = (spec.get("rows") or []) if rows is None else rows + return {**template, "unit_price": {**spec, "rows": picked}} + + +def save_rows( + current: dict[str, Any], type_id: str, template: dict[str, Any], rows: list[dict[str, Any]] +) -> tuple[dict[str, Any], bool]: + """줄 조합 저장본 — 양식과 같으면 그 종류를 지움(양식대로). (새 저장본, 바뀜).""" + merged = {key: value for key, value in current.items() if key != type_id} + if rows != ((template.get("unit_price") or {}).get("rows") or []): + merged[type_id] = rows + return merged, merged != current + + +def save_manual( + current: dict[str, Any], + type_id: str, + rows: list[dict[str, Any]], + prices: dict[str, dict[str, Any]], + today: str, +) -> dict[str, Any]: + """수동 단가 저장본 — **남은 줄의 것만** 둠(뺀 줄의 값이 같은 차례 새 줄에 붙지 않게). + + 넣은 날짜는 값·출처가 그대로면 옛 날짜를 둠 — 다시 저장했다고 날짜가 새로 서면 안 됨. + """ + seqs = {str(row.get("seq")) for row in rows} + before = current.get(type_id) or {} + kept = {} + for seq, price in prices.items(): + if str(seq) not in seqs: + continue + old = before.get(str(seq)) or {} + same = all(old.get(key) == price.get(key) for key in (*_MONEY_KEYS, "source")) + kept[str(seq)] = {**price, "entered_at": old.get("entered_at") if same else today} + merged = {key: value for key, value in current.items() if key != type_id} + if kept: + merged[type_id] = kept + return merged + + +def search_titles(book: Any, query: str, kind: str, limit: int = 50) -> list[dict[str, Any]]: + """고르개 — 단가표에서 낱말이 **모두** 든(코드·이름·규격) 항목. 단가가 섰는지도 함께.""" + from B09_Estimation.B09_Estimation_PriceBook import PriceBookError + + kinds = SEARCH_KINDS[kind] + words = query.lower().split() + found: list[dict[str, Any]] = [] + for title in book.titles.values() if words else (): + if len(found) >= limit: + break + if title.kind.value not in kinds: + continue + text = f"{title.code} {title.name} {title.spec}".lower() + if all(word in text for word in words): + try: + money = book.resolve(title.code) + total: float | None = float(money.material + money.labor + money.expense) + except PriceBookError: + total = None + found.append( + { + "code": title.code, + "name": title.name, + "spec": title.spec, + "unit": _unit(title.unit), + "price": total, + } + ) + return found diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index ad881d51f..78d5b6f58 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -33,13 +33,12 @@ from B06_Section.B06_Section_Repository import ( from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table -from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table -from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping +from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table +from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan, summary_input_rows from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table -from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as build_slope_table -from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes +from B08_Quantity.B08_Quantity_Engine_SlopeLength import road_surface_area, station_slopes from B08_Quantity.B08_Quantity_Provenance import quantity_provenance from common_util.common_util_project_settings import ( CONCRETE_PLACING_METHODS, @@ -48,15 +47,18 @@ from common_util.common_util_project_settings import ( concrete_placing_method, earthwork_conversion_choices, earthwork_conversion_factors, + haul_limit_choice, quantity_settings, rock_classes, save_section, + topsoil_target, ) from common_util.common_util_storage import resolve_stored_project_path from config.config_db import run_with_connection from config.config_system_design import ( EARTHWORK_CONVERSION_FACTORS, EARTHWORK_CONVERSION_PUMSEM_C_RANGES, + EARTHWORK_HAUL_EQUIPMENT_LIMITS_M, ) logger = logging.getLogger(__name__) @@ -90,6 +92,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: table = build_table(_stations(designs), factors) # 화면이 「무엇을 골랐나 · 품셈 범위 안인가」를 보이는 데 쓴다. 계산에는 안 들어간다. table["conversion_factor_choices"] = earthwork_conversion_choices(settings) + # 도쟈 한계거리 — 지금 값·기본값·근거를 함께 보인다(유토곡선 장비 경계, 2026-09-13 판정). + table["haul_limit_choice"] = haul_limit_choice(settings) # 품셈 암종별 범위 — **화면 안내용**이다. 정의처가 서버 한 곳이라 내려보내 쓴다 # (프론트에 다시 적으면 두 벌이 되어 갈린다). table["conversion_factor_pumsem_ranges"] = [ @@ -97,7 +101,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: for name, low, high in EARTHWORK_CONVERSION_PUMSEM_C_RANGES ] # 사면 계열은 저장된 설계선에서 유도한다. - slope = build_slope_table(station_slopes(designs)) + slopes = station_slopes(designs) + slope = build_slope_table(slopes) table["slope"] = slope plan = await _stored_haul_plan(project_id, route_id) @@ -144,6 +149,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: key: application_ratio(settings, key) for key in (settings.get("application_ratios_pct") or {}) }, + # 노체다짐 — 기본 꺼짐. 켠 프로젝트에서만 줄이 선다(2026-09-13 판정). + subgrade_compaction_enabled=bool(settings.get("subgrade_compaction_enabled")), ) ) # 준비공·사방공 — 못 서는 줄도 사유와 함께 남긴다(빈 표는 「빠뜨림」과 구별이 안 됨). @@ -163,6 +170,10 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: # 임목파쇄 — 기본 꺼짐. 켠 프로젝트에서만 줄이 선다(확정 5차 5번). settings.get("wood_chipping_enabled"), settings.get("wood_chipping_volume_m3"), + # 표토제거 대상 「노면」 면적 — 사면과 같은 측점 설계에서(2026-09-13 판정 「실무대로」). + road_surface_area(slopes), + # 대상 — 기본 노면 + 절토(별표2 문언) · 노면만은 설계자가 고름(2026-09-14 판정). + topsoil_target(settings) == "road_only", ) method, method_is_default = concrete_placing_method(settings) # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. @@ -434,6 +445,8 @@ class QuantitySettingsBody(BaseModel): structure_trench_water: str | None = None # 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다. topsoil_haul_distance_m: float | None = None + # 표토제거 대상 — "road_only" 는 노면만. `""` 는 기본(노면 + 절토, 별표2 문언)으로 되돌림. + topsoil_target: str | None = None # 임목축적 등급 — "소림"·"중림"·"밀림"(품셈 9-21 [주]①). `""` 는 「안 정함」이다. stand_volume_class: str | None = None # 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. @@ -441,10 +454,14 @@ class QuantitySettingsBody(BaseModel): # 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜면 줄이 서고, 부피를 넣으면 값이 선다. wood_chipping_enabled: bool | None = None wood_chipping_volume_m3: float | None = None + # 노체다짐 — 기본 꺼짐(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄. + subgrade_compaction_enabled: bool | None = None # 토량환산계수(다짐) — `{갈래: {"compacted": C, "reason": 사유}}`. # ⚠ **기본값을 복사해 넣지 않는다** — 안 고른 갈래는 키가 없어야 정본이 선다. # 빈 dict 는 「전부 기본값으로 되돌림」이라 통째로 갈아 끼운다. conversion_factors_override: dict[str, Any] | None = None + # 도쟈 한계거리(m) — `None` 은 기본값(60 m). 종무대 20 m 보다 커야 한다(도쟈 몫이 사라짐). + dozer_haul_limit_m: float | None = None #: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**. @@ -456,6 +473,7 @@ NULLABLE_SETTING_KEYS = ( "rubble_base_thickness_m", "topsoil_haul_distance_m", "wood_chipping_volume_m3", + "dozer_haul_limit_m", ) @@ -482,10 +500,25 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - for key in NULLABLE_SETTING_KEYS: if key in body.model_fields_set: values[key] = getattr(body, key) + dozer_limit = values.get("dozer_haul_limit_m") + free_haul = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["free_haul"] or 0.0 + if dozer_limit is not None and dozer_limit <= free_haul: + # 조용히 기본값으로 돌리지 않는다 — 넣은 값이 안 쓰이는 줄 모른다. + return JSONResponse( + status_code=400, + content={ + "status": "error", + "message": f"도쟈 한계거리는 종무대 {free_haul:g} m 보다 커야 합니다.", + }, + ) if "concrete_placing_method" in values: method = values["concrete_placing_method"] # 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리). values["concrete_placing_method"] = method if method in CONCRETE_PLACING_METHODS else None + if "topsoil_target" in values: + # 기본(노면 + 절토)은 저장하지 않는다 — 「안 정함」과 같게 둬 법 문언이 선다. + target = values["topsoil_target"] + values["topsoil_target"] = target if target == "road_only" else None if "conversion_factors_override" in values: # 아는 갈래·양수만 남긴다. 사유는 값이 있을 때만 따라간다(계산에는 안 쓴다). cleaned: dict[str, Any] = {} @@ -520,6 +553,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - "rock_methods", "material_supply", "concrete_placing_method", + "topsoil_target", "ancillary_counts", # 고른 계수를 **기본값으로 되돌릴 길**이 있어야 한다 — 병합이면 못 지운다. "conversion_factors_override", diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 6508c37a0..acfb37081 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -23,6 +23,7 @@ from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse +from pydantic import BaseModel, ConfigDict, Field from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B06_Section.B06_Section_Repository import get_cross_section_designs @@ -194,6 +195,161 @@ async def get_material_summary(project_id: UUID) -> JSONResponse: return JSONResponse(content=body) +@router.get("/{project_id}/quantity/structure-summary") +async def get_structure_summary(project_id: UUID) -> JSONResponse: + """**구조물 집계표**(PLAN 2장) — 측점별 한 줄 · 종류별 표 · 칸마다 출처(자동·사용자·라이브러리). + + ⚠ 값을 셈하지 않음 — 정본 둘(`structures.json`·`pipe_points.json`)과 B06 관 연장을 읽기만. + """ + from B08_Quantity.B08_Quantity_Engine_StructureSummary import ( + USER_CELLS_KEY, + build_summary, + pipe_lengths_from_designs, + ) + from B08_Quantity.B08_Quantity_Engine_StructureTemplate import template_of + from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file + + try: + stored_path = await run_with_connection(get_project_storage_relative_path, project_id) + project_root = resolve_stored_project_path(stored_path) + revision, items = load_structures(project_root) + points = read_pipe_points_file(pipe_points_path_in(Path(project_root))) + except Exception: + logger.exception("B08 구조물 집계표 — 정본 읽기 실패: project_id=%s", project_id) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "구조물 정본을 읽지 못했습니다."}, + ) + types = structure_type_map() + embedded = project_templates(project_root) + templates = {type_id: template_of(type_id, embedded) or {} for type_id in types} + body = build_summary( + [item.model_dump() for item in items], + [point.as_dict() for point in points], + types, + templates, + pipe_lengths_from_designs(await _designs(project_id)), + quantity_settings(project_root).get(USER_CELLS_KEY) or {}, + ) + return JSONResponse( + content={"status": "success", "revision": revision, "project_id": str(project_id), **body} + ) + + +async def _designs(project_id: UUID) -> list[dict[str, Any]]: + """현재 노선의 저장된 횡단 설계. 못 읽으면 빈 목록 — 관 연장이 빈칸으로 섬(0 아님).""" + try: + context = await run_with_connection(get_workflow_route_context, project_id) + route_id = int((context or {}).get("route_id") or 0) + return await run_with_connection(get_cross_section_designs, route_id) if route_id else [] + except Exception: + logger.exception("B08 구조물 집계표 — 횡단 설계 조회 실패: project_id=%s", project_id) + return [] + + +class StructureSummaryEdit(BaseModel): + """집계표 칸 하나 — 빈 값(null·"")은 그 칸을 지움.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(min_length=1, max_length=200) + key: str = Field(min_length=1, max_length=100) + value: float | str | None = None + + +class StructureSummaryEditRequest(BaseModel): + """[저장] 한 번에 보내는 손 고침 — 읽어 간 구조물 판번호와 함께.""" + + model_config = ConfigDict(extra="forbid") + + base_revision: int = Field(ge=0) + edits: list[StructureSummaryEdit] = Field(max_length=500) + + +def _summary_error(status: int, message: str) -> JSONResponse: + return JSONResponse(status_code=status, content={"status": "error", "message": message}) + + +@router.put("/{project_id}/quantity/structure-summary") +async def put_structure_summary( + project_id: UUID, payload: StructureSummaryEditRequest +) -> JSONResponse: + """집계표 손 고침을 **정본에 바로 씀**(브레인 판정) + 산출 조건에 「손댄 칸」 표. + + ⚠ 덮개층을 두지 않음 — `structures.json`·`pipe_points.json` 이 곧 도면·수량의 한 값(지침 5장). + ⚠ 다 맞춰 본 뒤에 씀 — 한 칸이라도 틀리면 아무것도 안 씀(구조물 판번호가 어긋나도 409). + ⛔ 놓기 칸(자리·길이·높이·관경)은 거절 — 시·종점과 한 벌이라 B05 몫. + """ + import asyncio + + from B05_Profile.B05_Profile_Structures_Repository import ( + StructureRevisionConflict, + save_structures, + ) + from B05_Profile.B05_Profile_Structures_Schema import StructureInstance + from B08_Quantity.B08_Quantity_Engine_StructureSummary import ( + USER_CELLS_KEY, + apply_edits, + pipe_row_id, + prune_marks, + ) + from common_util.common_util_drainage_pipes import pipe_points_path_in + from common_util.common_util_json import atomic_write_json + from common_util.common_util_project_settings import save_section + + try: + stored_path = await run_with_connection(get_project_storage_relative_path, project_id) + project_root = resolve_stored_project_path(stored_path) + revision, items = load_structures(project_root) + path = pipe_points_path_in(Path(project_root)) + document = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} + except Exception: + logger.exception("B08 구조물 집계표 저장 — 정본 읽기 실패: project_id=%s", project_id) + return _summary_error(500, "구조물 정본을 읽지 못했습니다.") + if revision != payload.base_revision: + return _summary_error(409, "그 사이 구조물이 바뀌었습니다 — 표를 다시 불러와 고칠 것.") + + structures = [item.model_dump() for item in items] + points = [point for point in document.get("points") or [] if isinstance(point, dict)] + settings = quantity_settings(project_root) + try: + changed, marks = apply_edits( + structures, + points, + structure_type_map(), + [edit.model_dump() for edit in payload.edits], + settings.get(USER_CELLS_KEY) or {}, + ) + instances = [StructureInstance.model_validate(item) for item in structures] + except (LookupError, ValueError) as exc: + return _summary_error(422, str(exc)) + + new_revision = revision + pipe_ids = {pipe_row_id(point) for point in points} + try: + if any(row_id not in pipe_ids for row_id in changed): + new_revision = await asyncio.to_thread( + save_structures, project_root, instances, base_revision=revision + ) + if any(row_id in pipe_ids for row_id in changed): + # 관 지점 파일은 옵션만 바꿔 **그대로** 씀 — 노선 지문·좌표·다른 칸은 손대지 않음. + await asyncio.to_thread(atomic_write_json, path, {**document, "points": points}) + except StructureRevisionConflict: + return _summary_error(409, "그 사이 구조물이 바뀌었습니다 — 표를 다시 불러와 고칠 것.") + except ValueError as exc: + return _summary_error(422, str(exc)) + await asyncio.to_thread( + save_section, + project_root, + "quantity", + {USER_CELLS_KEY: prune_marks(marks, structures, points)}, + replace_keys=[USER_CELLS_KEY], + ) + return JSONResponse( + content={"status": "success", "revision": new_revision, "changed_rows": len(changed)} + ) + + async def project_haul_inputs(project_id: UUID) -> dict[str, Any]: """유토곡선(B06)이 받아야 할 **구조물 몫** — 채집석 공제 · 구조물 잔토. @@ -249,10 +405,11 @@ async def get_handoff(project_id: UUID) -> JSONResponse: structures, names, skipped = _collect_structures(project_root) settings = quantity_settings(project_root) + modes = await _section_modes(project_id) unit_table = build_unit_table( structures, names, - await _section_modes(project_id), + modes, await _ground_types(project_id), settings.get("rubble_base_thickness_m"), structure_formulas=settings.get("structure_formula_overrides"), @@ -307,6 +464,8 @@ async def get_handoff(project_id: UUID) -> JSONResponse: structure_trench_water=settings.get("structure_trench_water"), # 임목축적 등급 — 지장목제거 뿌리뽑기(제근 9-21)의 품 갈래. 안 넣으면 B09 가 후보를 보임. stand_volume_class=settings.get("stand_volume_class"), + # 구조물도 양식 일위대가로 셀 장 — 그 구조물은 호표 `AX-ST` 줄 하나로(PLAN 6장 ②). + priced_sheets=_priced_sheets(project_root, unit_table, modes, settings), ) handoff["summary"] = summarize(handoff) handoff["skipped_structures"] = skipped @@ -314,6 +473,74 @@ async def get_handoff(project_id: UUID) -> JSONResponse: return JSONResponse(content=handoff) +def _priced_sheets( + project_root: str, + unit_table: dict[str, Any], + section_modes: dict[float, str] | None, + settings: dict[str, Any], +) -> list[dict[str, Any]]: + """양식 일위대가로 셀 구조물도 장 — 구조물도 탭과 **같은 접기**(`build_standard_sheets` → + `apply_templates`)로 세움. 인계와 내역 금액이 이 한 목록을 봄.""" + from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import priced_sheets + from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets + from B08_Quantity.B08_Quantity_Engine_StructureTemplate import apply_templates, template_of + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import ROWS_KEY + + embedded = project_templates(project_root) + payload = build_standard_sheets(unit_table, section_modes) + apply_templates(payload, settings, embedded) + sheets = payload.get("sheets") or [] + templates = { + type_id: template + for type_id in {str(sheet.get("type_id") or "") for sheet in sheets} + if (template := template_of(type_id, embedded)) + } + return priced_sheets(sheets, templates, settings.get(ROWS_KEY)) + + +async def structure_bill_prices(project_id: UUID, build: Any) -> dict[str, dict[str, Any]]: + """B09 내역이 부름 — 구조물도 호표 금액. **B09 가 넘긴 단가표**로 B08 일위대가 엔진을 돌림. + + ⚠ 금액 정본은 B09 — 여기서 단가표를 따로 짓지 않음. 수동 단가(프로젝트 값)도 입력으로 씀. + ⚠ 인계(`get_handoff`)와 **같은 전개**를 한 번 더 돎 — 두 요청이라 한 벌을 못 나눔. + """ + # ponytail: 인계와 전개를 두 번 돎 — 느려지면 인계 응답에 장 목록을 실어 한 번으로. + from B08_Quantity.B08_Quantity_Engine_StructureLibrary import available_templates + from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import price_sheets + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import MANUAL_KEY, ROWS_KEY, with_rows + from B09_Estimation.B09_Estimation_UnitPrice import find_variant_code + + stored_path = await run_with_connection(get_project_storage_relative_path, project_id) + project_root = resolve_stored_project_path(stored_path) + structures, names, _skipped = _collect_structures(project_root) + settings = quantity_settings(project_root) + modes = await _section_modes(project_id) + unit_table = build_unit_table( + structures, + names, + modes, + await _ground_types(project_id), + settings.get("rubble_base_thickness_m"), + structure_formulas=settings.get("structure_formula_overrides"), + structure_templates=project_templates(project_root), + ) + entries = _priced_sheets(project_root, unit_table, modes, settings) + if not entries: + return {} + row_edits = settings.get(ROWS_KEY) or {} + library = [ + with_rows(template, row_edits.get(template.get("type_id"))) + for template in available_templates(project_root) + ] + return price_sheets( + entries, + build.book, + lambda code, value: find_variant_code(code, value, build), + library, + settings.get(MANUAL_KEY) or {}, + ) + + def _pipe_table( project_root: str, pipe_lengths: list[dict[str, Any]], diff --git a/B08_Quantity/B08_Quantity_Router_StructureSheet.py b/B08_Quantity/B08_Quantity_Router_StructureSheet.py index 28178e7af..a80d9d496 100644 --- a/B08_Quantity/B08_Quantity_Router_StructureSheet.py +++ b/B08_Quantity/B08_Quantity_Router_StructureSheet.py @@ -323,9 +323,9 @@ async def put_structure_library_import( payload: LibraryImportRequest, session: dict[str, Any] = Depends(verify_session), ) -> JSONResponse: - """고른 항목을 **프로젝트 작업본에 박고** 그 종류의 고친 식을 비움. + """고른 항목을 **프로젝트 작업본에 박고** 그 종류의 고친 식·줄 조합·수동 단가를 비움. - ⚠ 고친 식을 비우는 까닭 — 새 양식과 줄 차례가 안 맞을 수 있음. 묻는 것은 화면 몫. + ⚠ 비우는 까닭 — 셋 다 줄 차례에 묶였는데 새 양식과 차례가 안 맞을 수 있음. 묻는 것은 화면 몫. """ from B08_Quantity.B08_Quantity_Engine_StructureLibrary import ( find_item, @@ -333,6 +333,7 @@ async def put_structure_library_import( tier_dirs, ) from B08_Quantity.B08_Quantity_Engine_StructureTemplate import OVERRIDES_KEY + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import MANUAL_KEY, ROWS_KEY from common_util.common_util_project_settings import quantity_settings, save_section project_root = await _project_root(project_id) @@ -346,23 +347,22 @@ async def put_structure_library_import( content={"status": "error", "message": "가져올 양식 항목을 찾지 못했습니다."}, ) await asyncio.to_thread(import_item, project_root, item, payload.tier) - current = quantity_settings(project_root).get(OVERRIDES_KEY) or {} - cleared = len(current.get(payload.type_id) or {}) - if cleared: - rest = {key: rows for key, rows in current.items() if key != payload.type_id} - await asyncio.to_thread( - save_section, - project_root, - "quantity", - {OVERRIDES_KEY: rest}, - replace_keys=[OVERRIDES_KEY], - ) + settings = quantity_settings(project_root) + current = {key: settings.get(key) or {} for key in (OVERRIDES_KEY, ROWS_KEY, MANUAL_KEY)} + touched = [key for key, value in current.items() if payload.type_id in value] + if touched: + rest = { + key: {k: v for k, v in current[key].items() if k != payload.type_id} for key in touched + } + await asyncio.to_thread(save_section, project_root, "quantity", rest, replace_keys=touched) return JSONResponse( content={ "status": "success", "code": payload.code, "imported_from": payload.tier, - "cleared_formulas": cleared, + "cleared_formulas": len(current[OVERRIDES_KEY].get(payload.type_id) or {}), + "cleared_unit_price_rows": payload.type_id in current[ROWS_KEY], + "cleared_manual_prices": len(current[MANUAL_KEY].get(payload.type_id) or {}), } ) @@ -404,6 +404,7 @@ async def put_structure_library_personal( save_personal, ) from B08_Quantity.B08_Quantity_Engine_StructureTemplate import OVERRIDES_KEY, template_of + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import ROWS_KEY from common_util.common_util_project_settings import quantity_settings folder = _personal_dir(session) @@ -421,8 +422,11 @@ async def put_structure_library_personal( status_code=404, content={"status": "error", "message": "양식이 있는 구조물도 장을 찾지 못했습니다."}, ) - overrides = (quantity_settings(project_root).get(OVERRIDES_KEY) or {}).get(type_id) - code = await asyncio.to_thread(save_personal, folder, template, overrides) + settings = quantity_settings(project_root) + overrides = (settings.get(OVERRIDES_KEY) or {}).get(type_id) + # ⛔ 수동 단가(`MANUAL_KEY`)는 안 넘김 — 프로젝트의 값(브레인 판정). + rows = (settings.get(ROWS_KEY) or {}).get(type_id) + code = await asyncio.to_thread(save_personal, folder, template, overrides, rows) return JSONResponse(content={"status": "success", "code": code, "edited": len(overrides or {})}) @@ -447,51 +451,193 @@ async def _price_build(project_id: UUID) -> Any: return await _build_for(project_id) +async def _sheet_template( + project_id: UUID, project_root: str, sheet_key: str +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """장 하나와 그 양식(프로젝트에 박힌 것 → 프로그램 기본).""" + from B08_Quantity.B08_Quantity_Engine_StructureLibrary import project_templates + from B08_Quantity.B08_Quantity_Engine_StructureTemplate import template_of + + sheets = (await _sheets_of(project_id, project_root)).get("sheets") or [] + picked = next((s for s in sheets if s.get("key") == sheet_key), None) + if picked is None: + return None, None + return picked, template_of(str(picked.get("type_id") or ""), project_templates(project_root)) + + +def _no_sheet() -> JSONResponse: + return JSONResponse( + status_code=404, + content={"status": "error", "message": "그 구조물도 장을 찾지 못했습니다."}, + ) + + +async def _price_build_or_error(project_id: UUID) -> tuple[Any, JSONResponse | None]: + try: + return await _price_build(project_id), None + except Exception: + logger.exception("B08 구조물도 일위대가 — 단가표 조립 실패: project_id=%s", project_id) + return None, JSONResponse( + status_code=500, + content={"status": "error", "message": "단가표를 조립하지 못했습니다."}, + ) + + @router.get("/{project_id}/quantity/structure-sheets/unit-price") async def get_structure_unit_price(project_id: UUID, sheet_key: str) -> JSONResponse: - """장 하나의 하단 **일위대가 표**(미리보기) — 줄 조합은 양식, 단가는 B09 단가표. + """장 하나의 하단 **일위대가 표**(미리보기) — 줄 조합은 양식(+고친 것), 단가는 B09 단가표. ⚠ 장 조회와 창구를 나눔 — 단가표 첫 조립이 십여 초라 장 조회를 늦추지 않게. ⚠ 값을 여기서 정본으로 적지 않음 — 보이기만(내역 금액은 B09 가 셈). + ⚠ `editor` — 화면이 고칠 줄 조합·양식 줄·수동 단가. 값(금액)은 늘 이 서버 계산만 씀. """ - from B08_Quantity.B08_Quantity_Engine_StructureLibrary import ( - available_templates, - project_templates, + from B08_Quantity.B08_Quantity_Engine_StructureLibrary import available_templates + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import ( + MANUAL_KEY, + ROWS_KEY, + unit_price_table, + with_rows, ) - from B08_Quantity.B08_Quantity_Engine_StructureTemplate import template_of - from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import unit_price_table from B09_Estimation.B09_Estimation_UnitPrice import find_variant_code + from common_util.common_util_project_settings import quantity_settings project_root = await _project_root(project_id) if project_root is None: return _not_found() - sheets = (await _sheets_of(project_id, project_root)).get("sheets") or [] - picked = next((s for s in sheets if s.get("key") == sheet_key), None) + picked, template = await _sheet_template(project_id, project_root, sheet_key) if picked is None: - return JSONResponse( - status_code=404, - content={"status": "error", "message": "그 구조물도 장을 찾지 못했습니다."}, - ) - template = template_of(str(picked.get("type_id") or ""), project_templates(project_root)) - if not template or not template.get("unit_price"): + return _no_sheet() + if not template: return JSONResponse(content={"status": "success", "unit_price": None}) - try: - build = await _price_build(project_id) - except Exception: - logger.exception("B08 구조물도 일위대가 — 단가표 조립 실패: project_id=%s", project_id) - return JSONResponse( - status_code=500, - content={"status": "error", "message": "단가표를 조립하지 못했습니다."}, - ) + build, error = await _price_build_or_error(project_id) + if error: + return error + settings = quantity_settings(project_root) + row_edits = settings.get(ROWS_KEY) or {} + type_id = str(template.get("type_id") or "") + manual = (settings.get(MANUAL_KEY) or {}).get(type_id) or {} + effective = with_rows(template, row_edits.get(type_id)) + # 하위 양식도 그 종류에서 고친 줄 조합으로 풂 — 수동 단가는 맨 윗 표만. + library = [ + with_rows(t, row_edits.get(t.get("type_id"))) for t in available_templates(project_root) + ] table = await asyncio.to_thread( unit_price_table, - template, + effective, picked, build.book, lambda code, value: find_variant_code(code, value, build), - available_templates(project_root), + library, + manual, ) - return JSONResponse(content={"status": "success", "unit_price": table}) + editor = { + "rows": effective["unit_price"]["rows"], + "default_rows": (template.get("unit_price") or {}).get("rows") or [], + "edited": type_id in row_edits, + "manual_prices": manual, + } + return JSONResponse(content={"status": "success", "unit_price": table, "editor": editor}) + + +class UnitPriceRowEdit(BaseModel): + """일위대가 줄 하나 — 수량은 원단위 줄(`from_row`) 또는 박힌 값, 코드는 고르개가 채움.""" + + model_config = ConfigDict(extra="forbid") + + seq: int = Field(ge=1) + name: str | None = Field(default=None, max_length=200) + spec: str | None = Field(default=None, max_length=200) + unit: str | None = Field(default=None, max_length=20) + from_row: int | None = Field(default=None, ge=1) + quantity: float | None = None + ref_code: str | None = Field(default=None, max_length=100) + work_item_code: str | None = Field(default=None, max_length=100) + variant_from: str | None = Field(default=None, max_length=50) + sub_vars: dict[str, float | str] | None = None + + +class ManualPrice(BaseModel): + """수동 단가 — 단위당 3분할과 출처. 넣은 날짜는 서버가 붙임.""" + + model_config = ConfigDict(extra="forbid") + + material: float = Field(default=0, ge=0) + labor: float = Field(default=0, ge=0) + expense: float = Field(default=0, ge=0) + source: str = Field(default="", max_length=200) + + +class UnitPriceEditRequest(BaseModel): + """한 화면에서 고친 일위대가 — **저장 자리는 둘**(줄 조합 = 양식+프로젝트 · 단가 = 프로젝트).""" + + model_config = ConfigDict(extra="forbid") + + sheet_key: str + rows: list[UnitPriceRowEdit] = Field(max_length=100) + manual_prices: dict[str, ManualPrice] = Field(default_factory=dict) + + +@router.put("/{project_id}/quantity/structure-sheets/unit-price") +async def put_structure_unit_price(project_id: UUID, payload: UnitPriceEditRequest) -> JSONResponse: + """고친 줄 조합·수동 단가를 산출 조건에 저장. 금액은 화면이 다시 조회해 서버 계산으로 받음.""" + from datetime import date + + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import ( + MANUAL_KEY, + ROWS_KEY, + save_manual, + save_rows, + ) + from common_util.common_util_project_settings import quantity_settings, save_section + + rows = [row.model_dump(exclude_none=True) for row in payload.rows] + if len({row["seq"] for row in rows}) != len(rows): + return JSONResponse( + status_code=422, + content={"status": "error", "message": "일위대가 줄 차례가 겹칩니다."}, + ) + project_root = await _project_root(project_id) + if project_root is None: + return _not_found() + picked, template = await _sheet_template(project_id, project_root, payload.sheet_key) + if picked is None or template is None: + return _no_sheet() + type_id = str(template.get("type_id") or "") + settings = quantity_settings(project_root) + merged_rows, changed = save_rows(settings.get(ROWS_KEY) or {}, type_id, template, rows) + prices = {seq: price.model_dump() for seq, price in payload.manual_prices.items()} + merged_manual = save_manual( + settings.get(MANUAL_KEY) or {}, type_id, rows, prices, date.today().isoformat() + ) + await asyncio.to_thread( + save_section, + project_root, + "quantity", + {ROWS_KEY: merged_rows, MANUAL_KEY: merged_manual}, + replace_keys=[ROWS_KEY, MANUAL_KEY], + ) + return JSONResponse( + content={ + "status": "success", + "changed_rows": changed, + "edited": type_id in merged_rows, + "manual_prices": len(merged_manual.get(type_id) or {}), + } + ) + + +@router.get("/{project_id}/quantity/structure-sheets/price-search") +async def get_structure_price_search( + project_id: UUID, q: str, kind: Literal["work", "resource"] = "work" +) -> JSONResponse: + """고르개 — 이 프로젝트 단가표에서 품셈(일위대가)·자원(자재·노임·중기)을 낱말로 찾음.""" + from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import search_titles + + build, error = await _price_build_or_error(project_id) + if error: + return error + items = await asyncio.to_thread(search_titles, build.book, q[:100], kind) + return JSONResponse(content={"status": "success", "items": items}) @router.get("/{project_id}/quantity/structure-sheets") diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index ac19b7b30..36c58ed7d 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -92,16 +92,22 @@ export interface QuantitySettings { structure_trench_water?: string | null; /** 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다. */ topsoil_haul_distance_m?: number | null; + /** 도쟈 한계거리(m) — `null`·없음이면 기본값(서버 정본 60 m)이 선다. */ + dozer_haul_limit_m?: number | null; /** 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13). */ ancillary_counts?: Record; /** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */ stand_volume_class?: string | null; + /** 표토제거 대상 — 없음이면 기본(노면 + 절토) · `"road_only"` 는 노면만. */ + topsoil_target?: string | null; /** 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. */ frame_material?: Record; /** 임목파쇄 — 기본 꺼짐(확정 5차 5번). 켜야 줄이 선다. */ wood_chipping_enabled?: boolean | null; /** 파쇄 부피(㎥) — 켜도 이 값이 없으면 줄만 서고 사유가 남는다. */ wood_chipping_volume_m3?: number | null; + /** 노체다짐 — 기본 꺼짐(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄. */ + subgrade_compaction_enabled?: boolean | null; } /** 갈래 하나의 「무엇을 골랐나」. 서버 `earthwork_conversion_choices` 와 짝이다. */ @@ -141,6 +147,15 @@ export interface EarthworkTable { conversion_factor_choices?: Record; /** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */ conversion_factor_pumsem_ranges?: PumsemRange[]; + /** 도쟈 한계거리 — 지금 값·기본값·근거(서버가 정본). 종무대 20 m 는 규정이라 값만 보인다. */ + haul_limit_choice?: { + value: number; + default: number; + chosen: boolean; + basis: string; + free_haul_m: number; + free_haul_basis: string; + }; /** 근거 사전 — ⚠ **개발환경에서만** 실려 온다. 운영에서는 칸 자체가 없다. */ provenance?: ProvenancePayload; } diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index f34ad20f7..9125dedf3 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -39,6 +39,7 @@ import { type MaterialResponse, } from "./B08_Quantity_UI_MaterialGrid"; import { renderStructureSheets } from "./B08_Quantity_UI_StructureSheet"; +import { renderStructureSummary } from "./B08_Quantity_UI_StructureSummary"; /** locale 헬퍼 */ function L(key: keyof typeof ui_locales): string { @@ -101,14 +102,19 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr spoil_site_distance_m: draft.spoil_site_distance_m, structure_trench_water: draft.structure_trench_water, topsoil_haul_distance_m: draft.topsoil_haul_distance_m, + // `""` 는 기본(노면 + 절토)으로 되돌림 — 서버가 None 으로 둔다. + topsoil_target: draft.topsoil_target, stand_volume_class: draft.stand_volume_class, frame_material: draft.frame_material, wood_chipping_enabled: draft.wood_chipping_enabled, wood_chipping_volume_m3: draft.wood_chipping_volume_m3, + subgrade_compaction_enabled: draft.subgrade_compaction_enabled, // 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다. ancillary_counts: draft.ancillary_counts, // 토량환산계수 — 고른 갈래만 담긴다. 빈 dict 는 「전부 기본값으로 되돌림」이다. conversion_factors_override: conversionOverridePayload(draft.conversion_factors), + // 도쟈 한계거리 — `null` 도 보낸다(기본값으로 되돌리는 길). + dozer_haul_limit_m: draft.dozer_haul_limit_m, }), }, ); @@ -281,6 +287,8 @@ interface DraftSettings { structure_trench_water: string; // 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. `null` 은 「안 정함」. topsoil_haul_distance_m: number | null; + // 표토제거 대상 — `""` 기본(노면 + 절토, 별표2 문언) · "road_only" 노면만(설계자 선택). + topsoil_target: string; // 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13). ancillary_counts: Record; // 임목축적 등급 — "소림"·"중림"·"밀림". ⚠ 본수가 아니라 축적이다(품셈 9-21 [주]①). @@ -290,10 +298,14 @@ interface DraftSettings { // 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 켜야 줄이 선다. 근주이식은 칸 자체가 없다. wood_chipping_enabled: boolean; wood_chipping_volume_m3: number | null; + // 노체다짐 — **기본 꺼짐**(9-16-2 [주]⑤ 조건부). 켜면 토공집계에 별도 줄(수량 = 성토량). + subgrade_compaction_enabled: boolean; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; // 갈래별 토량환산계수(다짐) — `compacted` 가 `null` 이면 「안 고름」이라 기본값이 선다. conversion_factors: Record; + // 도쟈 한계거리(m) — `null` 은 기본값(서버 정본 60 m)이 선다. + dozer_haul_limit_m: number | null; dirty: boolean; } @@ -402,6 +414,30 @@ function buildQuantitySidePanel( ); if (factorFields) panel.append(factorFields); + // ── 운반장비 거리 경계 — 도쟈 한계거리는 설계 조건이라 칸으로(2026-09-13 판정) ── + // ⚠ 기본값과 근거를 **서버가 준 그대로** 옆에 보인다 — 「이 값이 어디서 왔나」가 화면에 있어야 함. + const haul = table?.haul_limit_choice; + if (haul) { + panel.append(field(L("B08_Quantity_Side_HaulLimits"), "")); + const dozer = optionalNumberField( + L("B08_Quantity_Side_DozerLimit_Label"), + draft.dozer_haul_limit_m, + "5", + (value) => { + draft.dozer_haul_limit_m = value; + draft.dirty = true; + }, + ); + const input = dozer.querySelector("input"); + if (input) input.placeholder = String(haul.default); + panel.append(dozer); + panel.append(hintRow(`${L("B08_Quantity_Factor_Default")} ${haul.default} m — ${haul.basis}`)); + panel.append( + hintRow(`${L("B08_Quantity_Side_FreeHaul")} ${haul.free_haul_m} m — ${haul.free_haul_basis}`), + ); + panel.append(hintRow(L("B08_Quantity_Side_HaulLimits_Reach"))); + } + // ── 지반 구성비 — 갈래 수는 프로젝트 세트가 정한다(코드에 안 박음, PLAN 8-13) ── const classes = [...(table?.summary?.rock_classes ?? [])]; // ⚠ 비율을 아직 안 넣었으면 집계가 **「암」 한 줄**로 나온다(갈래로 안 갈림). 그 줄에도 @@ -459,10 +495,26 @@ function buildQuantitySidePanel( } } - // ── 표토 두께 — 정해야 준비공 표토제거 줄이 선다(품셈 9-15 [주]② 의 「T : 표토두께(m)」) ── + // ── 표토 두께 — 표토 운반 부피(제거 ㎡ × T)에 씀 · 제거 줄은 ㎡ 라 안 곱함(2026-09-13 「실무대로」) ── // ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.** // 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리. panel.append(field(L("B08_Quantity_Side_Topsoil"), "")); + // 대상 — 기본은 법 문언(노면 + 절토). 영월 한 건이 노면만이라 3배 차이 → 설계자가 고름(2026-09-14). + panel.append( + selectField( + L("B08_Quantity_Side_TopsoilTarget_Label"), + draft.topsoil_target, + [ + { value: "", label: L("B08_Quantity_TopsoilTarget_RoadAndCut") }, + { value: "road_only", label: L("B08_Quantity_TopsoilTarget_RoadOnly") }, + ], + (value) => { + draft.topsoil_target = value; + draft.dirty = true; + }, + ), + ); + panel.append(hintRow(L("B08_Quantity_Side_TopsoilTarget_Hint"))); panel.append( optionalNumberField( L("B08_Quantity_Side_Topsoil_Label"), @@ -492,6 +544,10 @@ function buildQuantitySidePanel( ), ); panel.append(hintRow(L("B08_Quantity_Side_BenchCut_Hint"))); + // 층따기 관행값 — **제안값**이지 확정이 아니다(원본 그림 확인 전 · 10장 판정). 칸에 넣지 않고 보이기만. + const benchSuggest = hintRow(L("B08_Quantity_Side_BenchCut_Suggest")); + benchSuggest.classList.add("b08-quantity__hint--warn"); + panel.append(benchSuggest); panel.append( optionalNumberField( L("B08_Quantity_Side_Rubble_Label"), @@ -629,6 +685,24 @@ function buildQuantitySidePanel( ); panel.append(hintRow(L("B08_Quantity_Side_Chipping_Hint"))); + // ── 노체다짐 — ⚠ **기본 꺼짐**(2026-09-13 판정). 9-16-2 [주]⑤ 조건부라 켜야 토공집계에 줄이 선다. + panel.append(field(L("B08_Quantity_Side_SubgradeCompaction"), "")); + panel.append( + selectField( + L("B08_Quantity_SubgradeCompaction_Label"), + draft.subgrade_compaction_enabled ? "on" : "", + [ + { value: "", label: L("B08_Quantity_Chipping_Off") }, + { value: "on", label: L("B08_Quantity_Chipping_On") }, + ], + (value) => { + draft.subgrade_compaction_enabled = value === "on"; + draft.dirty = true; + }, + ), + ); + panel.append(hintRow(L("B08_Quantity_Side_SubgradeCompaction_Hint"))); + // ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ── panel.append(field(L("B08_Quantity_Side_Placing"), "")); panel.append( @@ -831,6 +905,11 @@ function buildQuantityBody( }, // 구조물도 — 제원 조합 하나 = 한 장(PLAN 3장). 스스로 받아 오므로 표를 넘기지 않음. // ⚠ 탭 차례 바꾸기·원단위 탭 흡수는 PLAN 5장 몫 — 여기서는 원단위 앞에 붙이기만 함. + // 구조물 집계표 — 측점별 한 줄(PLAN 2장). 구조물도의 앞 사슬이라 그 바로 앞에 둠. + { + label: L("B08_Quantity_Tab_StructureSummary"), + build: () => renderStructureSummary(projectId), + }, { label: L("B08_Quantity_Tab_StructureSheet"), build: () => renderStructureSheets(projectId) }, { label: L("B08_Quantity_Tab_UnitQuantity"), @@ -916,10 +995,12 @@ export async function renderB08Quantity(root: HTMLElement): Promise { spoil_site_distance_m: (stored.spoil_site_distance_m as number | null) ?? null, structure_trench_water: (stored.structure_trench_water as string) ?? "", topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null, + topsoil_target: (stored.topsoil_target as string) ?? "", stand_volume_class: (stored.stand_volume_class as string) ?? "", frame_material: { ...((stored.frame_material ?? {}) as Record) }, wood_chipping_enabled: Boolean(stored.wood_chipping_enabled), wood_chipping_volume_m3: (stored.wood_chipping_volume_m3 as number | null) ?? null, + subgrade_compaction_enabled: Boolean(stored.subgrade_compaction_enabled), ancillary_counts: { ...((stored.ancillary_counts ?? {}) as Record), }, @@ -934,6 +1015,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { }, ]), ), + dozer_haul_limit_m: (stored.dozer_haul_limit_m as number | null) ?? null, dirty: false, }; const reload = (): void => { diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts index 596933b02..ebea1de3c 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts @@ -124,6 +124,15 @@ const CSS = ` .b08-sheet__formula select.is-changed { outline: 1px solid var(--color-accent, #6c8ebf); } .b08-sheet__formula .b08-sheet__digits { flex: 0 0 3.2rem; min-width: 3.2rem; } .b08-sheet__actions { display: flex; gap: 8px; align-items: center; } +/* 수동 단가 — 빨간 테두리 + 「미확정 N건」 배지(PLAN 확정 ⑦). */ +.b08-unit__manual { outline: 2px solid var(--color-danger, #d9534f); outline-offset: -2px; } +.b08-unit__badge { margin-left: 8px; padding: 0 6px; border-radius: 8px; color: #fff; + background: var(--color-danger, #d9534f); font-size: 12px; white-space: nowrap; } +.b08-unit-edit { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } +.b08-unit-edit td { white-space: nowrap; } +.b08-unit-edit__picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; } +.b08-unit-edit__results { display: flex; flex-direction: column; align-items: flex-start; + gap: 2px; width: 100%; max-height: 14rem; overflow: auto; } @media (max-width: 900px) { .b08-sheet { flex-direction: column; } .b08-sheet__aside { flex-basis: auto; width: 100%; } @@ -430,7 +439,9 @@ export function renderStructureSheets(projectId: string | null): HTMLElement { }, ) : null; - const unitPrice = sheet.library_item ? unitPriceSection(projectId, sheet.key) : null; + const unitPrice = sheet.library_item + ? unitPriceSection(projectId, sheet.key, sheet.rows) + : null; pane.replaceChildren(sheetBody(sheet, editor, unitPrice), aside); }; sheets.forEach((sheet, index) => { diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts index 9cf11d0ef..084e14f03 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts @@ -1,14 +1,19 @@ /* ============================================================================= * B08_Quantity_UI_StructureSheet_UnitPrice.ts - * 구조물도 장 아래 **일위대가 표**(미리보기) — PLAN 3장 하단 ①②. + * 구조물도 장 아래 **일위대가 표**(미리보기) — PLAN 3장 하단 ①②③. * * ⚠ 값을 셈하지 않음 — 서버(`…/structure-sheets/unit-price`)가 B09 단가표로 낸 금액을 적기만. * ⚠ 장 조회와 따로 받음 — 단가표 첫 조립이 십여 초라 표가 늦게 차도 위 수량표는 먼저 보임. * ⚠ 막힌 줄은 0 이 아니라 까닭을 적고, 하나라도 있으면 합계 앞에 「미완」. + * ⚠ 수동 단가 줄은 금액 칸 빨간 테두리 + 머리 「미확정 N건」 배지(PLAN 확정 ⑦). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { el, num } from "./B08_Quantity_UI_StructureSheet_Formula"; +import { + unitPriceEditor, + type UnitPriceEditorData, +} from "./B08_Quantity_UI_StructureSheet_UnitPriceEdit"; interface UnitPriceRow { seq: number; @@ -23,6 +28,9 @@ interface UnitPriceRow { labor?: number; expense?: number; total?: number; + manual?: boolean; + manual_source?: string; + manual_entered_at?: string; } interface UnitPriceTable { @@ -36,6 +44,7 @@ interface UnitPriceTable { total: number; blocked: number; complete: boolean; + unconfirmed: number; } /** 금액 칸 — 0.1원 자리까지(금액란 규칙). 못 푼 줄은 빈칸. */ @@ -43,13 +52,17 @@ function won(value: number | undefined): string { return value === undefined ? "" : num(value, 1); } -/** 장 아래 일위대가 칸 — 받는 동안 안내를 두고, 오면 표로 갈음. */ -export function unitPriceSection(projectId: string, sheetKey: string): HTMLElement { +/** 장 아래 일위대가 칸 — 받는 동안 안내를 두고, 오면 표로 갈음. 저장 뒤엔 다시 받음. */ +export function unitPriceSection( + projectId: string, + sheetKey: string, + sheetRows: { no: number; name: string; unit: string }[], +): HTMLElement { const wrap = el("div", "b08-grid"); - wrap.append( - el("p", "b08-grid__caption", "일위대가(미리보기) 불러오는 중… 단가표 첫 조립은 십여 초"), - ); - void (async () => { + const load = async (): Promise => { + wrap.replaceChildren( + el("p", "b08-grid__caption", "일위대가(미리보기) 불러오는 중… 단가표 첫 조립은 십여 초"), + ); try { const response = await fetch( `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/unit-price?sheet_key=${encodeURIComponent(sheetKey)}`, @@ -57,15 +70,38 @@ export function unitPriceSection(projectId: string, sheetKey: string): HTMLEleme ); const payload = (await response.json().catch(() => ({}))) as { unit_price?: UnitPriceTable | null; + editor?: UnitPriceEditorData; message?: string; }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); const table = payload.unit_price; - if (!table) { + const editor = payload.editor; + if (!table || !editor) { wrap.replaceChildren(el("p", "b08-grid__caption", "이 양식에는 일위대가 줄이 아직 없음")); return; } - wrap.replaceChildren(...render(table)); + const open = el("button", "", editor.edited ? "줄 고치기 (고친 조합)" : "줄 고치기"); + open.type = "button"; + open.style.alignSelf = "flex-start"; + const view = table.rows.length + ? render(table) + : [el("p", "b08-grid__caption", "일위대가 줄이 아직 없음 — [줄 고치기]로 더함")]; + open.addEventListener("click", () => { + open.disabled = true; + const panel = unitPriceEditor({ + projectId, + sheetKey, + sheetRows, + data: editor, + onSaved: () => void load(), + onClose: () => { + panel.remove(); + open.disabled = false; + }, + }); + wrap.append(panel); + }); + wrap.replaceChildren(...view, open); } catch (error) { wrap.replaceChildren( el( @@ -75,7 +111,8 @@ export function unitPriceSection(projectId: string, sheetKey: string): HTMLEleme ), ); } - })(); + }; + void load(); return wrap; } @@ -88,6 +125,9 @@ function render(table: UnitPriceTable): HTMLElement[] { "b08-sheet__head", `일위대가 ${table.code} · ${table.unit}당 ${total} (미리보기 — 내역 금액은 원가계산이 셈)`, ); + if (table.unconfirmed) { + head.append(el("span", "b08-unit__badge", `미확정 ${table.unconfirmed}건`)); + } const scroller = el("div", "b08-grid__scroll"); const grid = el("table", "b08-grid__table b08-grid__table--summary"); const headRow = document.createElement("tr"); @@ -109,19 +149,24 @@ function render(table: UnitPriceTable): HTMLElement[] { const tbody = document.createElement("tbody"); for (const row of table.rows) { const tr = document.createElement("tr"); - const note = row.skipped ? `안 섬 — ${row.reason}` : row.reason ? `⚠ ${row.reason}` : ""; + const manual = row.manual + ? `수동 단가 — ${row.manual_source || "출처 없음"} · ${row.manual_entered_at ?? ""}` + : ""; + const note = row.skipped ? `안 섬 — ${row.reason}` : row.reason ? `⚠ ${row.reason}` : manual; // 단가표 이름에 갈래가 이미 들었거나 규격 칸이 코드 자체면 겹쳐 적지 않음. const spec = row.spec && !row.name.includes(row.spec) && !row.ref_code.includes(row.spec) ? row.spec : ""; + const money = [row.material, row.labor, row.expense, row.total].map((value) => { + const cell = el("td", row.manual ? "b08-unit__manual" : "", won(value)); + if (row.manual) cell.title = manual; + return cell; + }); tr.append( el("td", "", spec ? `${row.name} (${spec})` : row.name), el("td", "", row.ref_code), el("td", "", row.quantity === null ? "" : num(row.quantity, 3)), el("td", "", row.unit), - el("td", "", won(row.material)), - el("td", "", won(row.labor)), - el("td", "", won(row.expense)), - el("td", "", won(row.total)), + ...money, el("td", "", note), ); tbody.append(tr); diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPriceEdit.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPriceEdit.ts new file mode 100644 index 000000000..66ff82951 --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPriceEdit.ts @@ -0,0 +1,343 @@ +/* ============================================================================= + * B08_Quantity_UI_StructureSheet_UnitPriceEdit.ts + * 구조물도 일위대가 **줄 고치기** — 줄 더하기·빼기 · 고르개(품셈·자원 찾기) · 수동 단가. PLAN 3장 ③. + * + * ⚠ 저장 자리는 둘(브레인 판정) — 줄 조합은 양식+프로젝트, 수동 단가는 프로젝트만. 한 번의 [저장]이 + * 둘을 함께 보내고 서버가 갈라 적음. [내 라이브러리에 저장]은 줄 조합만 실음. + * ⚠ 금액은 여기서 셈하지 않음 — 저장 뒤 표를 다시 받아 서버 계산(B09 단가표)으로 그림. + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { el } from "./B08_Quantity_UI_StructureSheet_Formula"; + +export interface UnitPriceSpecRow { + seq: number; + name?: string; + spec?: string; + unit?: string; + from_row?: number; + quantity?: number; + ref_code?: string; + work_item_code?: string; + variant_from?: string; + sub_vars?: Record; +} + +export interface ManualPrice { + material: number; + labor: number; + expense: number; + source: string; + entered_at?: string; +} + +export interface UnitPriceEditorData { + rows: UnitPriceSpecRow[]; + default_rows: UnitPriceSpecRow[]; + edited: boolean; + manual_prices: Record; +} + +interface SearchItem { + code: string; + name: string; + spec: string; + unit: string; + price: number | null; +} + +interface EditorOptions { + projectId: string; + sheetKey: string; + sheetRows: { no: number; name: string; unit: string }[]; + data: UnitPriceEditorData; + onSaved: () => void; + onClose: () => void; +} + +const MONEY: [keyof Omit, string][] = [ + ["material", "재료비"], + ["labor", "노무비"], + ["expense", "경비"], +]; + +function sheetUrl(projectId: string, tail: string): string { + return `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets${tail}`; +} + +function input(value: string | number | undefined, width: string, type = "text"): HTMLInputElement { + const node = document.createElement("input"); + node.type = type; + node.value = value === undefined ? "" : String(value); + node.style.width = width; + return node; +} + +function codeText(row: UnitPriceSpecRow): string { + if (row.ref_code) return row.ref_code; + if (row.work_item_code) { + return row.variant_from + ? `${row.work_item_code} (갈래: ${row.variant_from})` + : row.work_item_code; + } + return "코드 없음"; +} + +/** 줄 고치기 칸 — 로컬에서만 고치다 [저장]에 서버로. */ +export function unitPriceEditor(options: EditorOptions): HTMLElement { + const { projectId, sheetKey, sheetRows, data, onSaved, onClose } = options; + let rows: UnitPriceSpecRow[] = structuredClone(data.rows); + const manual: Record = structuredClone(data.manual_prices); + let picking: number | null = null; + + const wrap = el("div", "b08-unit-edit"); + const tbody = document.createElement("tbody"); + const picker = el("div", "b08-unit-edit__picker"); + const status = el("span", "b08-grid__caption"); + + const render = (): void => { + tbody.replaceChildren(...rows.map((row, index) => rowOf(row, index))); + picker.hidden = picking === null; + }; + + const rowOf = (row: UnitPriceSpecRow, index: number): HTMLTableRowElement => { + const tr = document.createElement("tr"); + const name = input(row.name, "9rem"); + name.addEventListener("input", () => (row.name = name.value || undefined)); + + // 수량 — 원단위 줄을 따르거나 박힌 값. + const source = document.createElement("select"); + source.append(new Option("박힌 값", "")); + for (const line of sheetRows) { + source.append(new Option(`원단위 ${line.no}. ${line.name} (${line.unit})`, String(line.no))); + } + source.value = row.from_row ? String(row.from_row) : ""; + const fixed = input(row.quantity, "5rem", "number"); + fixed.step = "any"; + fixed.disabled = Boolean(row.from_row); + source.addEventListener("change", () => { + row.from_row = source.value ? Number(source.value) : undefined; + if (row.from_row) delete row.quantity; + fixed.disabled = Boolean(row.from_row); + }); + fixed.addEventListener("input", () => { + row.quantity = fixed.value === "" ? undefined : Number(fixed.value); + }); + const quantity = el("td", ""); + quantity.append(source, fixed); + + const unit = input(row.unit, "3rem"); + unit.addEventListener("input", () => (row.unit = unit.value || undefined)); + + const code = el("td", "", codeText(row)); + const find = el("button", "", "찾기"); + find.type = "button"; + find.title = "품셈·자원을 찾아 이 줄의 코드로 넣음"; + find.addEventListener("click", () => { + picking = index; + render(); + picker.querySelector("input")?.focus(); + }); + code.append(" ", find); + + // 수동 단가 — 넣으면 빨간 테두리 · 표 머리에 「미확정」으로 셈. 프로젝트에만 저장됨. + const price = el("td", ""); + const key = String(row.seq); + const on = document.createElement("input"); + on.type = "checkbox"; + on.checked = key in manual; + on.title = "수동 단가 — 이 프로젝트에만 저장(라이브러리로 안 감)"; + price.append(on, " 수동"); + const fields = MONEY.map(([field, label]) => { + const box = input(manual[key]?.[field], "5.5rem", "number"); + box.min = "0"; + box.step = "any"; + box.placeholder = label; + box.title = `${label} 단가(단위당)`; + box.addEventListener("input", () => { + if (manual[key]) manual[key][field] = Number(box.value || 0); + }); + return box; + }); + const origin = input(manual[key]?.source, "7rem"); + origin.placeholder = "출처"; + origin.addEventListener("input", () => { + if (manual[key]) manual[key].source = origin.value; + }); + const sync = (): void => { + for (const box of [...fields, origin]) { + box.disabled = !on.checked; + box.classList.toggle("b08-unit__manual", on.checked); + } + }; + on.addEventListener("change", () => { + if (on.checked) { + manual[key] = { material: 0, labor: 0, expense: 0, source: "" }; + fields.forEach((box, i) => (manual[key][MONEY[i][0]] = Number(box.value || 0))); + manual[key].source = origin.value; + } else { + delete manual[key]; + } + sync(); + }); + sync(); + price.append(...fields, origin); + if (manual[key]?.entered_at) price.append(` ${manual[key].entered_at}`); + + const remove = el("button", "", "빼기"); + remove.type = "button"; + remove.addEventListener("click", () => { + rows = rows.filter((_, i) => i !== index); + delete manual[key]; + picking = null; + render(); + }); + const actions = el("td", ""); + actions.append(remove); + + const nameCell = el("td", ""); + nameCell.append(name); + const unitCell = el("td", ""); + unitCell.append(unit); + tr.append(el("td", "", String(row.seq)), nameCell, quantity, unitCell, code, price, actions); + return tr; + }; + + // 고르개 — 이 프로젝트 단가표에서 낱말로 찾음. + const kind = document.createElement("select"); + kind.append(new Option("품셈(일위대가)", "work"), new Option("자원(자재·노임·중기)", "resource")); + const query = input("", "12rem"); + query.placeholder = "이름·코드·규격 낱말"; + const go = el("button", "", "찾기"); + go.type = "button"; + const shut = el("button", "", "닫기"); + shut.type = "button"; + const results = el("div", "b08-unit-edit__results"); + const search = async (): Promise => { + if (!query.value.trim()) return; + results.replaceChildren(el("span", "b08-grid__caption", "찾는 중…")); + try { + const response = await fetch( + sheetUrl( + projectId, + `/price-search?q=${encodeURIComponent(query.value)}&kind=${kind.value}`, + ), + { credentials: "include" }, + ); + const payload = (await response.json()) as { items?: SearchItem[]; message?: string }; + if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); + const items = payload.items ?? []; + results.replaceChildren( + ...(items.length ? items.map(choice) : [el("span", "b08-grid__caption", "없음")]), + ); + } catch (error) { + results.replaceChildren( + el("span", "b08-grid__caption", `못 찾음 — ${error instanceof Error ? error.message : ""}`), + ); + } + }; + const choice = (item: SearchItem): HTMLElement => { + const price = item.price === null ? "단가 안 섬" : `${item.price.toLocaleString("ko-KR")}원`; + const button = el( + "button", + "", + `${item.code} · ${item.name}${item.spec ? ` ${item.spec}` : ""} · ${item.unit} · ${price}`, + ); + button.type = "button"; + button.addEventListener("click", () => { + const row = picking === null ? undefined : rows[picking]; + if (!row) return; + row.ref_code = item.code; + row.name = item.name; + row.spec = item.spec || undefined; + row.unit = item.unit || undefined; + delete row.work_item_code; + delete row.variant_from; + delete row.sub_vars; + picking = null; + render(); + }); + return button; + }; + go.addEventListener("click", () => void search()); + query.addEventListener("keydown", (event) => { + if (event.key === "Enter") void search(); + }); + shut.addEventListener("click", () => { + picking = null; + render(); + }); + picker.append(kind, query, go, shut, results); + + const grid = el("table", "b08-grid__table"); + const head = document.createElement("tr"); + for (const label of ["차례", "이름", "수량", "단위", "코드", "단가 수동(단위당)", ""]) { + head.append(el("th", "", label)); + } + const thead = document.createElement("thead"); + thead.append(head); + grid.append(thead, tbody); + const scroller = el("div", "b08-grid__scroll"); + scroller.append(grid); + + const add = el("button", "", "줄 더하기"); + add.type = "button"; + add.addEventListener("click", () => { + // 양식 줄 차례와도 안 겹치게 — 뺀 양식 줄의 차례를 새 줄이 물려받지 않음. + const seq = Math.max(0, ...rows.map((r) => r.seq), ...data.default_rows.map((r) => r.seq)) + 1; + rows.push({ seq, name: "", quantity: 1 }); + render(); + }); + const reset = el("button", "", "양식대로"); + reset.type = "button"; + reset.title = "줄 조합을 양식 원래대로 — [저장]해야 반영"; + reset.addEventListener("click", () => { + rows = structuredClone(data.default_rows); + picking = null; + render(); + }); + const save = el("button", "b08-spec__save", "일위대가 저장"); + save.type = "button"; + save.addEventListener("click", () => { + void (async () => { + save.disabled = true; + status.textContent = "저장 중…"; + try { + // 넣은 날짜는 서버가 붙임 — 값·출처만 보냄. + const prices = Object.fromEntries( + Object.entries(manual).map(([seq, { entered_at: _date, ...price }]) => [seq, price]), + ); + const response = await fetch(sheetUrl(projectId, "/unit-price"), { + method: "PUT", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sheet_key: sheetKey, rows, manual_prices: prices }), + }); + const payload = (await response.json().catch(() => ({}))) as { message?: string }; + if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); + onSaved(); + } catch (error) { + status.textContent = `저장 못 함 — ${error instanceof Error ? error.message : ""}`; + save.disabled = false; + } + })(); + }); + const close = el("button", "b08-quantity__tab", "고친 것 버리고 닫기"); + close.type = "button"; + close.addEventListener("click", onClose); + + const actions = el("div", "b08-sheet__actions"); + actions.append(add, save, reset, close, status); + wrap.append( + el( + "p", + "b08-grid__caption", + "줄 조합은 이 양식+프로젝트에 저장(내 라이브러리로 갈 수 있음) · 수동 단가는 이 프로젝트에만 저장", + ), + scroller, + picker, + actions, + ); + render(); + return wrap; +} diff --git a/B08_Quantity/B08_Quantity_UI_StructureSummary.ts b/B08_Quantity/B08_Quantity_UI_StructureSummary.ts new file mode 100644 index 000000000..cf239bbcc --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_StructureSummary.ts @@ -0,0 +1,398 @@ +/* ============================================================================= + * B08_Quantity_UI_StructureSummary.ts + * **구조물 집계표** 탭 — 측점별 구조물 한 줄 · 종류별 표 · 칸 고치기 (PLAN 2장). + * + * ⚠ 값을 셈하지 않음 — 서버(`…/quantity/structure-summary`)가 정본을 읽어 세운 줄을 적기만. + * ⚠ 칸마다 출처 표시 — 자동(정본) · 사용자(이 표에서 고침) · 라이브러리(양식 기본값) · 빈칸. + * ⚠ 이 표에서 고친 값을 B05 가 바꿔 자동으로 돌아간 칸은 **조용히 넘기지 않고** 줄에 알림. + * ⚠ 고치기는 데이터 3층 — 칸 조작은 캐시(sessionStorage)에만, [저장] 때 서버가 **정본에** 씀. + * 상세 칸만 고침 — 자리·길이·높이·관경 같은 놓기 칸은 시·종점과 한 벌이라 B05 몫(회색). + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { stationLabel } from "./B08_Quantity_UI_EarthworkGrid"; +import { el, num } from "./B08_Quantity_UI_StructureSheet_Formula"; + +type Source = "auto" | "user" | "library" | "empty"; +type CellValue = number | string | null; + +interface SummaryCell { + value: CellValue; + source: Source; + was?: CellValue; + replaced_user_value?: CellValue; +} + +interface SummaryRow { + id: string; + origin: "structures" | "pipe_points"; + chainage_m: number | null; + start_m: number | null; + end_m: number | null; + count: number; + length_m: number | null; + length_basis: string; + memo: string; + note?: string; + replaced?: string[]; + cells: Record; +} + +interface SummaryColumn { + key: string; + label: string; + unit: string; + input: string; + choices: string[]; + editable: boolean; +} + +interface SummaryTable { + type_id: string; + name: string; + columns: SummaryColumn[]; + rows: SummaryRow[]; + count: number; + length_total_m: number | null; + length_missing: number; + averages: Record; +} + +interface SummaryResponse { + revision: number; + tables: SummaryTable[]; + notes: string[]; + message?: string; +} + +interface Edit { + id: string; + key: string; + value: CellValue; +} + +const SOURCE_LABELS: Record = { + auto: "자동 — 구조물 놓기(B05)·계곡 시설 정본 값", + user: "사용자 — 이 표에서 고친 값(정본에 적힘)", + library: "라이브러리 — 정본이 비어 양식 기본값으로 섬", + empty: "빈칸 — 정본도 양식도 값 없음", +}; + +const STYLE_ID = "b08-structure-summary-style"; +const CSS = ` +.b08-sum { display: flex; flex-direction: column; gap: 10px; min-height: 0; } +.b08-sum__legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; } +.b08-sum__cell--auto { box-shadow: inset 3px 0 0 var(--color-border, #888); } +.b08-sum__cell--user { box-shadow: inset 3px 0 0 var(--color-accent, #6c8ebf); } +.b08-sum__cell--library { box-shadow: inset 3px 0 0 var(--color-success, #5cb85c); } +.b08-sum__cell--empty { color: var(--color-text-muted, #999); } +.b08-sum__cell--replaced { outline: 2px solid var(--color-danger, #d9534f); outline-offset: -2px; } +.b08-sum td.is-changed { background: color-mix(in srgb, var(--color-accent, #6c8ebf) 18%, transparent); } +.b08-sum .b08-grid__table td { white-space: nowrap; } +.b08-sum td input, .b08-sum td select { font-size: 12px; max-width: 8rem; } +.b08-sum td input[type=number] { width: 5rem; } +.b08-sum td button { font-size: 11px; padding: 0 4px; margin-left: 2px; cursor: pointer; } +.b08-sum__actions { display: flex; gap: 8px; align-items: center; } +`; + +function injectStyles(): void { + if (document.getElementById(STYLE_ID)) return; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = CSS; + document.head.append(style); +} + +/** 칸 글 — 정수는 그대로(뒷길이 45), 소수는 둘째 자리까지. */ +function cellText(value: CellValue | undefined): string { + if (value === null || value === undefined || value === "") return ""; + return typeof value === "number" && !Number.isInteger(value) ? num(value, 2) : String(value); +} + +function station(row: SummaryRow): string { + if (row.start_m !== null && row.end_m !== null) { + return `${stationLabel(row.start_m)} ~ ${stationLabel(row.end_m)}`; + } + return row.chainage_m === null ? "" : stationLabel(row.chainage_m); +} + +/** 정본에 지금 적힌 값 — 라이브러리·빈칸은 정본이 빈 것. */ +function storedValue(cell: SummaryCell | undefined): CellValue { + return cell && (cell.source === "auto" || cell.source === "user") ? cell.value : null; +} + +function same(a: CellValue, b: CellValue): boolean { + return String(a ?? "") === String(b ?? ""); +} + +/** 고친 칸 캐시 — [저장] 전까지 이 탭(sessionStorage)에만 둠. 판번호가 다르면 버림. */ +class Draft { + private readonly storageKey: string; + readonly edits = new Map(); + onChange: () => void = () => undefined; + + constructor( + projectId: string, + readonly revision: number, + ) { + this.storageKey = `b08-structure-summary-draft:${projectId}`; + try { + const saved = JSON.parse(sessionStorage.getItem(this.storageKey) ?? "null") as { + revision: number; + edits: Edit[]; + } | null; + if (saved?.revision === revision) { + for (const edit of saved.edits) this.edits.set(`${edit.id}|${edit.key}`, edit); + } + } catch { + // 캐시를 못 읽으면 빈 초안 — 정본은 그대로. + } + } + + get(id: string, key: string): Edit | undefined { + return this.edits.get(`${id}|${key}`); + } + + set(edit: Edit, stored: CellValue): void { + const slot = `${edit.id}|${edit.key}`; + if (same(edit.value, stored)) this.edits.delete(slot); + else this.edits.set(slot, edit); + this.persist(); + } + + clear(): void { + this.edits.clear(); + this.persist(); + } + + private persist(): void { + try { + if (this.edits.size) { + const body = { revision: this.revision, edits: [...this.edits.values()] }; + sessionStorage.setItem(this.storageKey, JSON.stringify(body)); + } else { + sessionStorage.removeItem(this.storageKey); + } + } catch { + // 저장소가 막혀도 화면 초안은 살아 있음. + } + this.onChange(); + } +} + +function editor( + column: SummaryColumn, + row: SummaryRow, + cell: SummaryCell | undefined, + draft: Draft, + td: HTMLTableCellElement, +): HTMLElement[] { + const stored = storedValue(cell); + const pending = draft.get(row.id, column.key); + const current = pending ? pending.value : stored; + let control: HTMLInputElement | HTMLSelectElement; + if (column.input === "select" && column.choices.length) { + const select = document.createElement("select"); + select.append(new Option("—", "")); + for (const choice of column.choices) select.append(new Option(choice, choice)); + select.value = current === null ? "" : String(current); + control = select; + } else { + const input = document.createElement("input"); + input.type = column.input === "number" ? "number" : "text"; + if (column.input === "number") { + input.min = "0"; + input.step = "any"; + } + input.value = current === null ? "" : String(current); + control = input; + } + if (cell?.source === "library") control.title = `비우면 양식 기본값 ${cellText(cell.value)}`; + if (control instanceof HTMLInputElement && cell?.source === "library") { + control.placeholder = cellText(cell.value); + } + const mark = (): void => { + td.classList.toggle("is-changed", Boolean(draft.get(row.id, column.key))); + }; + control.addEventListener("change", () => { + const raw = control.value.trim(); + const value = raw === "" ? null : column.input === "number" ? Number(raw) : raw; + draft.set({ id: row.id, key: column.key, value }, stored); + mark(); + }); + mark(); + const parts: HTMLElement[] = [control]; + if (cell?.source === "user" && cell.was !== undefined) { + const undo = el("button", "", "↺"); + undo.type = "button"; + undo.title = `고치기 전 값(${cellText(cell.was) || "빈칸"})으로 — [저장]하면 「자동」으로 돌아감`; + undo.addEventListener("click", () => { + control.value = cell.was === null || cell.was === undefined ? "" : String(cell.was); + control.dispatchEvent(new Event("change")); + }); + parts.push(undo); + } + return parts; +} + +function renderTable(table: SummaryTable, draft: Draft): HTMLElement { + const wrap = el("div", "b08-grid"); + const length = + table.length_total_m === null ? "" : ` · 연장 합 ${num(table.length_total_m, 2)} m`; + const missing = table.length_missing ? ` · 연장 빈 줄 ${table.length_missing}` : ""; + wrap.append(el("p", "b08-sheet__head", `${table.name} — ${table.count}개소${length}${missing}`)); + const scroller = el("div", "b08-grid__scroll"); + const grid = el("table", "b08-grid__table"); + const head = document.createElement("tr"); + head.append(el("th", "", "측점"), el("th", "", "연장(m)")); + for (const column of table.columns) { + const th = el("th", "", column.label); + if (!column.editable) th.title = "놓기 칸 — 시·종점과 한 벌이라 구조물 놓기(B05)에서 고침"; + head.append(th); + } + head.append(el("th", "", "비고")); + const thead = document.createElement("thead"); + thead.append(head); + const tbody = document.createElement("tbody"); + for (const row of table.rows) { + const tr = document.createElement("tr"); + const lengthCell = el("td", "", row.length_m === null ? "" : num(row.length_m, 2)); + lengthCell.title = row.length_basis || "연장 없음"; + tr.append(el("td", "", station(row)), lengthCell); + for (const column of table.columns) { + const cell = row.cells[column.key]; + const td = el("td", `b08-sum__cell--${cell?.source ?? "empty"}`); + if (column.editable) td.append(...editor(column, row, cell, draft, td)); + else td.textContent = cellText(cell?.value); + td.title = cell ? SOURCE_LABELS[cell.source] : ""; + if (cell?.source === "user" && cell.was !== undefined) { + td.title += ` · 고치기 전 ${cellText(cell.was) || "빈칸"}`; + } + if (cell?.replaced_user_value !== undefined) { + td.classList.add("b08-sum__cell--replaced"); + td.title = `이 표에서 ${cellText(cell.replaced_user_value) || "빈칸"}(으)로 고쳤으나 B05 가 바꿔 자동값으로 돌아감`; + } + tr.append(td); + } + const notes = [ + row.replaced?.length ? `⚠ B05 가 바꿈: ${row.replaced.join("·")}` : "", + row.note ?? "", + row.memo, + ].filter(Boolean); + tr.append(el("td", "", notes.join(" · "))); + tbody.append(tr); + } + const average = document.createElement("tr"); + average.append(el("td", "", "평균치수"), el("td", "", "")); + for (const column of table.columns) { + const value = table.averages[column.key]; + average.append(el("td", "", value === undefined ? "" : num(value, 2))); + } + average.append(el("td", "", "")); + tbody.append(average); + grid.append(thead, tbody); + scroller.append(grid); + wrap.append(scroller); + return wrap; +} + +/** 탭 본문 — 받는 동안 안내, 오면 종류별 표. [저장] 뒤엔 다시 받음. */ +export function renderStructureSummary(projectId: string | null): HTMLElement { + injectStyles(); + const root = el("div", "b08-sum"); + if (!projectId) { + root.append(el("p", "b08-quantity__message", "프로젝트를 먼저 고를 것")); + return root; + } + const url = `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-summary`; + let draft: Draft | null = null; + // 저장 안 한 칸이 조용히 사라지지 않게 — 나갈 때 물음(자동저장은 안 만듦, CLAUDE.md 5장). + window.addEventListener("beforeunload", (event) => { + if (!draft?.edits.size || !root.isConnected) return; + event.preventDefault(); + event.returnValue = "저장 안 한 칸이 있음"; + }); + + const load = async (): Promise => { + root.replaceChildren(el("p", "b08-grid__caption", "구조물 집계표 불러오는 중…")); + try { + const response = await fetch(url, { credentials: "include" }); + const payload = (await response.json().catch(() => ({}))) as SummaryResponse; + if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); + const active = new Draft(projectId, payload.revision); + draft = active; + const legend = el("div", "b08-sum__legend"); + for (const source of ["auto", "user", "library", "empty"] as Source[]) { + legend.append(el("span", `b08-sum__cell--${source}`, ` ${SOURCE_LABELS[source]} `)); + } + const save = el("button", "b08-spec__save", "저장"); + save.type = "button"; + const discard = el("button", "b08-quantity__tab", "고친 것 버리기"); + discard.type = "button"; + const status = el("span", "b08-grid__caption"); + active.onChange = () => { + const count = active.edits.size; + save.disabled = count === 0; + discard.disabled = count === 0; + status.textContent = count + ? `고친 칸 ${count} — [저장]해야 정본에 적힘(구조물도·원단위가 그 값을 씀)` + : "상세 칸만 고침 · 자리·길이·높이·관경은 구조물 놓기(B05)에서"; + }; + active.onChange(); + save.addEventListener("click", () => { + void (async () => { + save.disabled = true; + status.textContent = "저장 중…"; + try { + const result = await fetch(url, { + method: "PUT", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + base_revision: active.revision, + edits: [...active.edits.values()], + }), + }); + const body = (await result.json().catch(() => ({}))) as { message?: string }; + if (!result.ok) throw new Error(body.message ?? `HTTP ${result.status}`); + active.clear(); + await load(); + } catch (error) { + status.textContent = `저장 못 함 — ${error instanceof Error ? error.message : ""}`; + save.disabled = false; + } + })(); + }); + discard.addEventListener("click", () => { + active.clear(); + void load(); + }); + const actions = el("div", "b08-sum__actions"); + actions.append(save, discard, status); + const total = payload.tables.reduce((sum, table) => sum + table.count, 0); + root.replaceChildren( + el( + "p", + "b08-sheet__head", + `구조물 집계표 · ${payload.tables.length}종 · ${total}개소 (측점별 실치수 — 구조물도가 이 값을 씀)`, + ), + legend, + actions, + ...payload.notes.map((note) => el("p", "b08-grid__caption b08-grid__caption--warn", note)), + ...(payload.tables.length + ? payload.tables.map((table) => renderTable(table, active)) + : [el("p", "b08-grid__caption", "놓인 구조물이 없음")]), + ); + } catch (error) { + root.replaceChildren( + el( + "p", + "b08-grid__caption b08-grid__caption--warn", + `구조물 집계표를 불러오지 못함 — ${error instanceof Error ? error.message : ""}`, + ), + ); + } + }; + void load(); + return root; +} diff --git a/B09_Estimation/B09_Estimation_BasisSheet.py b/B09_Estimation/B09_Estimation_BasisSheet.py index fe047296e..fcb0419be 100644 --- a/B09_Estimation/B09_Estimation_BasisSheet.py +++ b/B09_Estimation/B09_Estimation_BasisSheet.py @@ -110,11 +110,19 @@ def work_item_basis(build: Any) -> list[dict[str, Any]]: for code, title in sorted(build.book.titles.items()): if title.kind is not PriceKind.UNIT_PRICE: continue - notes = [ - detail.note.strip() - for detail in build.book.details.get(code, []) - if detail.note and detail.note.strip() + own = build.book.details.get(code, []) + # Q 식 문구는 층 차례 뒤 D(단가산출) 줄에 삶 — 그 일위대가가 부르는 D 의 줄 문구도 함께(명세 3장). + basis = [ + detail + for ref in own + if ref.ref_code.startswith("D-") + for detail in build.book.details.get(ref.ref_code, []) ] + notes = [] + for detail in [*own, *basis]: + text = detail.note.strip() if detail.note else "" + if text and text not in notes: + notes.append(text) work_item_code = code[2:].split("#")[0] source = build.factor_sources.get(work_item_code) if source and source not in notes: diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities.py b/B09_Estimation/B09_Estimation_BillOfQuantities.py index 0c9e6578f..6a6a70d9b 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities.py @@ -29,9 +29,12 @@ from B09_Estimation.B09_Estimation_Guards import ( check_drain_pipe_not_double_counted, check_included_materials_not_listed, check_free_haul_not_priced, + check_handoff_boundaries, check_haul_volume_within_cut, + check_material_surcharge_once, ) from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master +from common_util.common_util_pum_edition import edition_waiting_reason from B09_Estimation.B09_Estimation_QuantityDigits import round_quantity from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import ( @@ -42,6 +45,12 @@ from B09_Estimation.B09_Estimation_UnitPrice import ( _ZERO = Decimal(0) +#: 총 절취량으로 셀 공종 — 9-3 토사깎기 · 9-4 암절취 · 9-5 발파암(인계 대응표 「흙깎기」 셋). +#: ⚠ 판에 묶인 절 번호임(명세 17장) — 판이 바뀌면 대응표와 함께 고칠 것. +CUT_CODE_PREFIXES = ("FP-09-03", "FP-09-04", "FP-09-05") +#: 구조물도 양식 호표 코드 머리(명세 2장 자체 확장) — 금액은 B08 일위대가 엔진을 B09 단가표로 셈. +STRUCTURE_PRICE_PREFIX = "AX-ST-" + #: 자재 공급 구분이 안 갈린 값. B08 이 실제로 이 값을 보낸다(2026-09-08 실물 확인). #: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다. SUPPLY_UNKNOWN = "unknown" @@ -107,6 +116,8 @@ class HandoffWorkItem: #: **근거는 산출근거 칸에 그대로 적는다** — 우리가 다시 지어내지 않는다. spec_class: str = "" spec_class_basis: str = "" + #: 이 줄의 공종 코드가 본 품셈 판 — 마스터 판과 다르면 값을 안 씀(명세 17장). + pum_edition: str = "" @property def display_name(self) -> str: @@ -152,6 +163,8 @@ class BillRow: expense_krw: Decimal = _ZERO is_group: bool = False in_bill: bool = True + #: 금액을 낸 단가 코드(`B-…#갈래`) — 단산 번호를 그 코드로 찾음. 안 선 줄은 빈 글. + price_code: str = "" #: 줄 사유 **조각** — `(닿는 열 키, 글)`. 화면 「비고」는 이것을 이어 붙인 것이고, #: 근거 호버는 열 키로 걸러 **그 사유가 닿는 칸에만** 띄운다(PLAN 8-36 ㉮). #: ⚠ 종전엔 `note` 한 칸에 덮어썼다 — 한 줄에 사유가 둘이면 **하나가 조용히 사라졌다** @@ -224,6 +237,8 @@ class BillResult: price_basis: Any = None #: 자재대 표 — 사급·관급·미정 셋으로 갈린다(PLAN 8-7 「금액은 B09」). material_sheet: Any = None + #: 수동 단가로 선 자리 — 내역서 끝 「미확정 N건」(PLAN 확정 ⑦). 줄마다 `{name, count}`. + unconfirmed: list[dict[str, Any]] = field(default_factory=list) @property def direct_material_krw(self) -> Decimal: @@ -285,6 +300,7 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[ variant_value=str(row.get("variant_value") or ""), spec_class=row.get("spec_class") or "", spec_class_basis=row.get("spec_class_basis") or "", + pum_edition=str(row.get("pum_edition") or ""), ) for row in payload["work_items"] ] @@ -351,9 +367,9 @@ def _number_of(path: tuple[int, ...]) -> str: from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import ( # noqa: E402 _composite_row, _excluded_row, - _haul_price_of, _leaf_row, _material_row, + _structure_price_row, ) @@ -362,11 +378,17 @@ def build_bill( *, build: UnitPriceBuild | None = None, master: dict[str, Any] | None = None, + structure_prices: dict[str, dict[str, Any]] | None = None, ) -> BillResult: - """인계 응답 한 벌을 ④ 예산내역서 한 장으로 접는다.""" + """인계 응답 한 벌을 ④ 예산내역서 한 장으로 접는다. + + `structure_prices` — 구조물도 호표 금액 `{B-AX-ST-…#키: …}`(B08 `structure_bill_prices`, + **이 `build` 단가표로** 셈). 안 주면 호표 줄은 금액 없이 사유와 함께 섬. + """ work_items, materials = parse_handoff(payload) unit_prices = build or cached_build() - index = _master_index(master or load_work_item_master()) + master = master or load_work_item_master() + index = _master_index(master) result = BillResult() # ── 1) 쓰인 공종의 조상만 남긴 가지치기 나무 ──────────────────────────────── @@ -374,6 +396,7 @@ def build_bill( used: list[tuple[tuple[int, ...], HandoffWorkItem, list[_MasterNode]]] = [] orphans: list[HandoffWorkItem] = [] composites: list[HandoffWorkItem] = [] + templated: list[HandoffWorkItem] = [] for item in work_items: if not item.in_bill: # ⚠ 코드 유무보다 **먼저** 가른다. 보정량계는 공종코드가 없어서가 아니라 @@ -394,6 +417,23 @@ def build_bill( } ) continue + if str(item.work_item_code or "").startswith(STRUCTURE_PRICE_PREFIX): + # 구조물도 호표(PLAN 6장 ②) — 공종 마스터에 없는 자체 확장 코드라 나무에 안 끼움. + templated.append(item) + continue + # 품셈 판이 다른 줄 — 절 번호가 같아도 같은 공종이라는 보장이 없어 값을 안 씀(명세 17장 규칙 4). + waiting = edition_waiting_reason(item.pum_edition, str(master.get("effective_date", ""))) + if waiting: + result.missing.append( + { + "name": item.display_name, + "unit": item.unit, + "quantity": str(item.quantity), + "reason": waiting, + "blocked_kind": "edition_waiting", + } + ) + continue if (item.composite_parts or item.composite_not_ready) and not item.work_item_code: # 묶음 줄 — 품셈에 그 공종이 없어 **조각을 합쳐** 한 줄로 세운다 # (옹벽 = 타설 + 거푸집 + 철근 + 잡석). 「코드 없음」으로 세면 안 된다. @@ -411,6 +451,9 @@ def build_bill( emitted: dict[str, str] = {} # 코드 → ITEM NO. counters: dict[str, int] = {} # 부모 ITEM NO. → 마지막 번호 + # 인계 줄 → 그 줄로 선 내역 줄. ⚠ 이름으로 되찾지 않음 — 도자운반 토사·리핑암처럼 + # 이름이 같은 줄이 둘이면 **첫 줄 단가**가 둘째에도 붙음(2026-09-13 축 C 조사 실결함). + row_of: dict[int, BillRow] = {} def next_number(parent_no: str) -> str: counters[parent_no] = counters.get(parent_no, 0) + 1 @@ -440,12 +483,22 @@ def build_bill( # 물려주면 ITEM NO. 가 겹쳐 어느 줄인지 못 가린다. 머리글만 물려준다. item_no = next_number(parent_no) emitted.setdefault(leaf.code, item_no) - result.rows.append(_leaf_row(item_no, leaf, item, unit_prices, result)) + row_of[id(item)] = _leaf_row(item_no, leaf, item, unit_prices, result) + result.rows.append(row_of[id(item)]) # ── 1-2) 묶음 줄 ────────────────────────────────────────────────────────── for item in composites: counters[""] = counters.get("", 0) + 1 - result.rows.append(_composite_row(str(counters[""]), item, unit_prices, result)) + row_of[id(item)] = _composite_row(str(counters[""]), item, unit_prices, result) + result.rows.append(row_of[id(item)]) + + # ── 1-3) 구조물도 호표 줄 ───────────────────────────────────────────────── + for item in templated: + counters[""] = counters.get("", 0) + 1 + row_of[id(item)] = _structure_price_row( + str(counters[""]), item, structure_prices or {}, result + ) + result.rows.append(row_of[id(item)]) # ── 2) 공종을 못 고른 줄 — 이름째 남긴다 ──────────────────────────────────── for item in orphans: @@ -472,6 +525,10 @@ def build_bill( # ── 4) 검사 — `in_bill=false` 줄에 금액이 붙지 않았는가 ────────────────────── check_excluded_rows_not_priced(rows=[r.as_dict() for r in result.excluded]) + # 이중계상 경계 ①②③(명세 6장) — B08 이 갈 곳 칸으로 판정한 어긴 자리 · 자재 할증 한 번. + check_handoff_boundaries(payload.get("double_count_violations")) + check_material_surcharge_once(materials) + # ㉦ 큰돌쌓기 품에 포함된 자재(고임돌·채움콘크리트)를 따로 세지 않았는가. check_included_materials_not_listed( work_item_codes=[row.code or "" for row in result.rows], @@ -496,7 +553,8 @@ def build_bill( haul_rows = [ { "equipment": item.haul_equipment, - "unit_price_krw": _haul_price_of(item, result), + # 그 줄에 실제로 붙은 단가. 안 선 줄은 0 — ㉡ 검사에 넘길 값이다. + "unit_price_krw": getattr(row_of.get(id(item)), "unit_price_krw", None) or _ZERO, } for item in work_items if item.haul_equipment @@ -504,8 +562,14 @@ def build_bill( check_free_haul_not_priced(haul_rows=haul_rows) # ㉡ 보조 — 운반토량 합이 총 절취량을 넘지 않는가(같은 흙을 두 번 세지 않았는가). + # 절취 = **코드로** 가름(9-3 토사깎기 · 9-4 암절취 · 9-5 발파암 — 인계 대응표의 「흙깎기」 셋). + # ⚠ 종전엔 줄 이름에 「깎기」·「절취」가 드는지로 셌음 — 이름이 바뀌면 검산이 조용히 멈춤. cut_total = sum( - (item.quantity for item in work_items if "깎기" in item.name or "절취" in item.name), + ( + item.quantity + for item in work_items + if str(item.work_item_code or "").startswith(CUT_CODE_PREFIXES) + ), _ZERO, ) haul_total = sum((item.quantity for item in work_items if item.haul_equipment), _ZERO) @@ -530,10 +594,11 @@ def build_bill( for row in result.rows: if row.is_group or row.code is None or row.amount_krw is None: continue - entry = sheet.by_unit_price(f"B-{row.code}") - if entry is not None: + # 그 줄이 **실제로 쓴** 단가 코드(갈래 `#…` 포함)로 찾음 — 갈래로 선 줄도 번호가 붙게. + label = sheet.label_for(row.price_code or f"B-{row.code}") + if label: # 종전처럼 **맨 앞**에 놓는다 — 실무 참조번호(「단산 46」)가 먼저 읽혀야 한다. - row.notes.insert(0, ("unit_price_krw", entry.label)) + row.notes.insert(0, ("unit_price_krw", label)) result.price_basis = sheet if any(m.surcharge_pct is None for m in materials): @@ -554,6 +619,8 @@ def bill_summary(result: BillResult) -> dict[str, Any]: "material_rows": len(result.material_rows), "material_sheet": result.material_sheet.as_dict() if result.material_sheet else None, "missing": result.missing, + "unconfirmed": result.unconfirmed, + "unconfirmed_count": sum(int(entry["count"]) for entry in result.unconfirmed), "body_total_krw": str(result.body_total_krw), "direct_material_krw": str(result.direct_material_krw), "direct_labor_krw": str(result.direct_labor_krw), diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index a5c37a822..11d00e31b 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -9,8 +9,6 @@ from __future__ import annotations -from decimal import Decimal - from B09_Estimation.B09_Estimation_BillOfQuantities import ( SUPPLY_OWNER, SUPPLY_UNKNOWN, @@ -25,16 +23,6 @@ from B09_Estimation.B09_Estimation_BillOfQuantities import ( from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, find_variant_code -_ZERO = Decimal(0) - - -def _haul_price_of(item: HandoffWorkItem, result: BillResult) -> Decimal: - """그 운반 줄에 실제로 붙은 단가. 안 붙었으면 0 — ㉡ 검사에 넘길 값이다.""" - for row in result.rows: - if row.name == item.name and row.unit_price_krw is not None: - return row.unit_price_krw - return _ZERO - def _composite_row( item_no: str, @@ -107,6 +95,65 @@ def _composite_row( return row +def _structure_price_row( + item_no: str, + item: HandoffWorkItem, + structure_prices: dict[str, dict], + result: BillResult, +) -> BillRow: + """구조물도 호표 줄(PLAN 6장 ②) — m당 금액은 B08 일위대가 엔진을 **이 내역의 단가표**로 돌린 값. + + ⚠ 일위대가에 막힌 줄이 있으면 **금액을 안 세움** — 절반짜리 단가가 제일 위험(묶음 줄과 같음). + ⚠ 수동 단가로 선 줄은 금액을 세우되 「미확정」으로 셈 — 구조물도 화면과 내역이 같은 값. + """ + from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import structure_price_code + + ref = structure_price_code(str(item.work_item_code), item.variant_value) + row = BillRow( + item_no=item_no, + level=1, + code=item.work_item_code, + name=item.name, + spec=item.spec, + unit=item.unit, + quantity=item.quantity, + in_bill=item.in_bill, + ) + entry = structure_prices.get(ref) + reason = "" + if entry is None: + reason = f"구조물도 일위대가 {ref} 를 못 받음 — 구조물도 탭에서 그 장의 일위대가를 확인" + elif entry["blocked"]: + reason = f"구조물도 일위대가 미완 — 막힌 줄 {entry['blocked']}: " + "; ".join( + entry["reasons"][:3] + ) + if reason: + row.add_note("unit_price_krw", reason) + result.missing.append( + { + "name": row.name, + "code": ref, + "unit": row.unit, + "quantity": str(item.quantity), + "reason": reason, + } + ) + return row + + # 단가 = 호표 계금(구조물도 화면과 같은 값) · 금액 = 호표 성분 소계 × 수량(명세 7장). + line = entry["money"].scaled(item.quantity) + row.unit_price_krw = entry["total"] + row.amount_krw = round_at(line.total, OutputPlace.BOQ_ROW) + row.material_krw = line.material + row.labor_krw = line.labor + row.expense_krw = line.expense + row.add_note("unit_price_krw", f"호표 {ref}") + if entry["unconfirmed"]: + row.add_note("unit_price_krw", f"⚠ 수동 단가 {entry['unconfirmed']}건 미확정") + result.unconfirmed.append({"name": row.name, "code": ref, "count": entry["unconfirmed"]}) + return row + + def _excluded_row(item: HandoffWorkItem) -> BillRow: """`in_bill=false` 줄. **수량만 보이고 단가·금액을 안 붙인다.** @@ -393,6 +440,7 @@ def _leaf_row( # 쓰인 차례를 기억한다 — 실무 참조번호(「단산 46」)가 그 차례다. if price_code not in result.used_unit_prices: result.used_unit_prices.append(price_code) + row.price_code = price_code unit_money = unit_prices.book.resolve(price_code) line = unit_money.scaled(item.quantity) diff --git a/B09_Estimation/B09_Estimation_CrewOutput.py b/B09_Estimation/B09_Estimation_CrewOutput.py index 81bc628b3..370d52258 100644 --- a/B09_Estimation/B09_Estimation_CrewOutput.py +++ b/B09_Estimation/B09_Estimation_CrewOutput.py @@ -35,6 +35,8 @@ from B09_Estimation.B09_Estimation_ResourceAxis import ( ) _RE_NUMBER = re.compile(r"^\d+(?:,\d{3})*(?:\.\d+)?$") +#: 두께 갈래 칸 — 「20㎝」. 넓히면 규격 칸(「0.6㎥」)을 갈래로 오해한다. +_RE_THICKNESS = re.compile(r"^\d+(?:\.\d+)?\s*(?:㎝|cm)$") #: 시공량 열을 알아보는 말. 「시 공 량 (㎡)」처럼 띄어쓰기가 섞여 온다. _OUTPUT_WORDS = ("시공량", "기준시공량", "적용시공량") @@ -101,6 +103,7 @@ def parse_crew_table( members: list[CrewMember] = [] outputs: list[Decimal] = [] labels: list[str] = [] + thickness: list[tuple[str, Decimal]] = [] for row in rows: if not row or not row[0]: @@ -117,6 +120,11 @@ def parse_crew_table( names = [part for part in _normalize(head).split(" ") if part] resolved = [_resolve(catalog, name) for name in names] if not all(resolved): + # 두께 갈래 줄 — 「30㎝ | 150」. 콘크리트 포장 12-6 은 두께 칸이 작업조 칸과 + # **줄을 엇갈려** 병합돼 있어(hwpx 확인) 마스터가 작업조 줄 사이에 두께 줄을 끼워 둔다. + if _RE_THICKNESS.match(head) and len(_numbers_in(" ".join(row[1:]))) == 1: + thickness.append((head, _numbers_in(" ".join(row[1:]))[0])) + continue # 자원이 아니면 유형 라벨 줄로 본다 — 「복 잡 | 보 통 | 간 단」. if not _numbers_in(" ".join(row)): if not members: @@ -143,9 +151,17 @@ def parse_crew_table( # 시공량은 보통 **첫 작업조 줄**에 붙어 온다. if not outputs: outputs = _outputs_of(row, counts) + # 작업조 줄에 두께 갈래가 붙어 온 자리 — 「포장공 | 3 | 20㎝ | 100 | …」. + for label, value in zip(row, row[1:]): + if _RE_THICKNESS.match(label) and _RE_NUMBER.match(value): + thickness.append((label, Decimal(value.replace(",", "")))) if not members: return "작업조 줄을 못 찾았습니다" + if thickness: + # 두께마다 시공량이 하나 — 그 옆 시공량 칸(「후진 진입·경운기 운반은 좌측의 50%까지 감」)은 + # **범위라 값이 아니다.** 유형 라벨은 그 두 칸의 머리라 두께 갈래와 짝이 아니다. + return CrewTable(members=tuple(members), outputs=tuple(thickness)) if not outputs: return "시공량 값을 못 찾았습니다" if labels and len(labels) != len(outputs): @@ -206,6 +222,16 @@ def match_crew_table( work_item_code = node.get("work_item_code", "") table_id = str(table.get("pum_table_id", "")) form = str(table.get("pum_form", "")) + from B09_Estimation.B09_Estimation_WorkItemUnit import unit_of + + if not unit and not unit_of(work_item_code): + # 밑수가 「일당」이라 비어 있는 표(12-6) — 1단위당 품의 단위는 **시공량 열 머리**의 단위다. + # ⚠ 공종 단위가 따로 적힌 자리는 그대로 둔다(12-2 에 딸린 비계 표가 m 를 들고 옴). + headers = table.get("condition_note") or [] + found = [ + re.search(r"[((]\s*([^))]+?)\s*[))]", str(headers[i])) for i in output_columns(table) + ] + unit = found[0].group(1) if found and found[0] else "" parsed = parse_crew_table(table, catalog) if isinstance(parsed, str): result.unmatched.append( diff --git a/B09_Estimation/B09_Estimation_Guards.py b/B09_Estimation/B09_Estimation_Guards.py index 997a726c6..14f30c2ad 100644 --- a/B09_Estimation/B09_Estimation_Guards.py +++ b/B09_Estimation/B09_Estimation_Guards.py @@ -20,6 +20,7 @@ from __future__ import annotations from decimal import Decimal +from typing import Any _TOLERANCE = Decimal("0.5") @@ -219,6 +220,67 @@ def check_excluded_rows_not_priced( ) +def check_handoff_boundaries(violations: list[str] | None) -> None: + """이중계상 경계 ①②③(명세 6장) — B08 인계가 갈 곳 칸으로 판정해 보낸 어긴 자리. + + 판정은 성분 칸을 가진 B08 한 곳에서 한다(`B08_Quantity_Engine_Handoff_Boundaries`). + 여기서는 **멈추기만** 한다 — 목록이 차 있는데 내역서를 세우면 같은 물량이 두 번 금액이 된다. + """ + if violations: + raise DoubleCountError("이중계상 경계를 어겼습니다 — " + " / ".join(violations)) + + +#: 품셈 [주]가 「재료량에 할증 포함」이라 적은 공종 — 그 재료가 일위대가 재료비로 붙으면 +#: **할증 뒤 값**이 들어가 자재총괄에서 한 번 더 붙는다(㉠). 원문 넷, 코드는 마스터가 붙인 자리. +SURCHARGE_INCLUDED_ITEMS: dict[str, str] = { + "FP-12-02": "용적 배합 콘크리트 참고표 「재료량에는 할증률이 포함」(마스터가 12-2 에 붙임)", + "FP-12-38-02": "유로폼 사용수량 [주]① 「재료량에는 재료의 할증 및 손율이 포함」", + "FP-13-11-04": "돌망태 사각형 [주]① 「자재비에는 재료의 할증을 포함」", + "AX-WK-c0842a0d": "모르타르 배합 참고자료 ※ 「위 재료량은 할증이 포함된 것이다」", +} + + +def check_materials_before_surcharge(book: Any) -> None: + """③ 일위대가 재료비는 할증 전 — 할증 포함 재료량을 준 공종에 자재 줄이 붙으면 멈춘다. + + ⚠ 지금은 그 재료들이 자재 단가 층이 없어 **안 붙은 줄**로만 보인다 — 층이 서는 날 여기서 걸린다. + """ + from B09_Estimation.B09_Estimation_PriceBook import PriceKind + + for title_code, details in book.details.items(): + code = title_code[2:].split("#", 1)[0] if title_code.startswith("B-") else "" + if code not in SURCHARGE_INCLUDED_ITEMS: + continue + for detail in details: + ref = book.titles.get(detail.ref_code) + if ref is not None and ref.kind is PriceKind.MATERIAL: + raise DoubleCountError( + f"{title_code}: 자재 「{ref.name}」이 일위대가 재료비로 붙었습니다 — " + f"{SURCHARGE_INCLUDED_ITEMS[code]}. 일위대가 재료비는 할증 전 값이고 " + "할증은 자재총괄 한 곳뿐입니다 (PLAN 8-7 ㉠)." + ) + + +def check_material_surcharge_once(materials: list[Any]) -> None: + """③ 자재 줄 합계 = 순수량 × (1 + 할증률) **한 번** — 율이 없거나 품셈 포함이면 순수량 그대로. + + 내역서 자재대는 `total_amount` 를 수량으로 쓴다. 인계 도중 어디서든 할증이 한 번 더 붙으면 + 그 값이 그대로 금액이 되므로, **금액을 만드는 자리**에서 순수량과 율로 되짚는다. + """ + for material in materials: + net = Decimal(str(material.net_amount)) + included = "할증 포함" in str(material.surcharge_note or "") + rate = None if included else material.surcharge_pct + expected = net if rate is None else net * (Decimal(1) + Decimal(str(rate)) / Decimal(100)) + total = Decimal(str(material.total_amount)) + if abs(total - expected) > max(Decimal("1e-6"), abs(expected) * Decimal("1e-9")): + raise DoubleCountError( + f"자재 「{material.material_name}」: 합계 {total} 가 순수량 {net} × " + f"(1+{rate or 0}%) = {expected} 와 다릅니다 — 할증이 두 번 붙었거나 빠졌습니다 " + "(PLAN 8-7 ㉠ 할증은 자재총괄 한 번)." + ) + + #: 제잡비 「윗단」 값을 쓴다는 뜻 — 물빼기 파이프를 **설치하는** 경우다. #: 품셈 13-6-2 [주]③ 「… 상단에는 물빼기 파이프 설치에 관계되는 노무비, 재료비를 #: 포함한다」. 그러므로 윗단을 쓰면 파이프를 **따로 세면 안 된다**. diff --git a/B09_Estimation/B09_Estimation_Lists.py b/B09_Estimation/B09_Estimation_Lists.py index 47a38509a..d2f067aec 100644 --- a/B09_Estimation/B09_Estimation_Lists.py +++ b/B09_Estimation/B09_Estimation_Lists.py @@ -31,7 +31,11 @@ from B09_Estimation.B09_Estimation_Rounding import ( OutputPlace, round_at, ) -from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build +from B09_Estimation.B09_Estimation_UnitPrice import ( + HOURLY_WAGE_SUFFIX, + UnitPriceBuild, + cached_build, +) _ZERO = Decimal(0) @@ -55,7 +59,8 @@ def catalog_list(build: UnitPriceBuild, kind: PriceKind) -> list[dict[str, Any]] rows: list[dict[str, Any]] = [] for code in sorted(build.book.titles): title = build.book.titles[code] - if title.kind is not kind: + # 조종원 시간당(`…#시간`)은 기초단가가 아니라 일당에서 **셈한** 값 — 노무비목록표엔 안 실음. + if title.kind is not kind or code.endswith(HOURLY_WAGE_SUFFIX): continue try: price: Decimal | None = title.adopted_price() @@ -144,22 +149,20 @@ def resource_summary( `quantities` = `{공종코드: 수량}` (내역서가 쓰는 것과 같은 모양). 한 자원이 여러 공종에 걸리면 **한 줄로 합친다** — 실무 시트가 그 모양이다. - ⚠ **일위대가 안쪽을 한 겹만 편다.** 일위대가 → 자원(노무·자재·기계 사용료)까지가 - 실무 집계표의 깊이다. 기계 사용료(`X-`)를 다시 손료·연료로 쪼개면 **중기 집계표와 - 이중으로 세는 것**이 된다. + ⚠ **자원(노무·자재·기계 사용료)까지 편다** — 일위대가 → 자원이 실무 집계표의 깊이다. + 기계 사용료(`X-`)를 다시 손료·연료로 쪼개면 **중기 집계표와 이중으로 세는 것**이 된다. + ⚠ 가운데 층(단계 합산 부모의 잎 일위대가 `B→B` · 단가산출 `B→D`)은 **끝까지 풀어** 자원에 닿음. + 종전엔 한 겹만 펴 그런 참조를 **조용히 버렸음**(암절취 부모 · 층 차례 뒤 기계 몫, 2026-09-13). """ prices = build or cached_build() book = prices.book #: 자원코드 → [수량, 제목] picked: dict[str, list[Any]] = {} missing: list[str] = [] + #: 풀어 내려갈 가운데 층 — 자원이 아니라 자원을 품은 표. + unfold = (PriceKind.UNIT_PRICE, PriceKind.PRICE_BASIS) - for raw_code, quantity in quantities.items(): - code = raw_code if raw_code.startswith("B-") else f"B-{raw_code}" - if code not in book.titles: - missing.append(raw_code) - continue - amount = Decimal(str(quantity)) + def walk(code: str, amount: Decimal, seen: tuple[str, ...]) -> None: for detail in book.details.get(code, []): if ( detail.percent_of_labor is not None @@ -168,11 +171,22 @@ def resource_summary( ): continue # 비율 줄은 자원이 아니다 — 목록표에 설 자재·노임이 없다 ref = detail.ref_code - if ref == code: + if ref == code or ref in seen: continue - slot = picked.setdefault(ref, [_ZERO, book.titles.get(ref)]) + title = book.titles.get(ref) + if title is not None and title.kind in unfold: + walk(ref, detail.quantity * amount, (*seen, ref)) + continue + slot = picked.setdefault(ref, [_ZERO, title]) slot[0] += detail.quantity * amount + for raw_code, quantity in quantities.items(): + code = raw_code if raw_code.startswith("B-") else f"B-{raw_code}" + if code not in book.titles: + missing.append(raw_code) + continue + walk(code, Decimal(str(quantity)), (code,)) + groups: dict[str, list[dict[str, Any]]] = { "labor": [], "material": [], @@ -190,6 +204,9 @@ def resource_summary( PriceKind.MACHINE_HOURLY: "machine", }.get(title.kind) if bucket is None: + # 일식(W)은 단가 0 이라 자원이 아님 — 그 밖의 종류가 오면 조용히 버리지 않고 드러냄. + if title.kind is not PriceKind.LUMPSUM: + missing.append(f"{ref} (집계 칸 없는 종류 {title.kind.value})") continue try: unit_money = book.resolve(ref) diff --git a/B09_Estimation/B09_Estimation_Lists_Sources.py b/B09_Estimation/B09_Estimation_Lists_Sources.py index 4325c6ea7..30f1f11cb 100644 --- a/B09_Estimation/B09_Estimation_Lists_Sources.py +++ b/B09_Estimation/B09_Estimation_Lists_Sources.py @@ -31,8 +31,8 @@ from decimal import Decimal from typing import Any from B09_Estimation.B09_Estimation_MachineCost import ( - OPERATOR_ALLOWANCE_FACTOR, OPERATOR_ALLOWANCE_NOTICE, + hourly_operator_wage, ) from B09_Estimation.B09_Estimation_PriceBook import PRICE_SLOT_COUNT, PriceKind from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build @@ -169,7 +169,10 @@ def base_reference_data( "day_wage_krw": _money(Decimal(str(wage))), # 제수당·상여금·퇴직급여충당금 계수를 곱한 값 (2026-09-09 사용자 확정 ③). # 근거·한계는 `MachineCost.OPERATOR_ALLOWANCE_FACTOR` 주석 한 곳에 모아 뒀다. - "hourly_krw": _money(Decimal(str(wage)) / Decimal(8) * OPERATOR_ALLOWANCE_FACTOR), + # 식 좌→우 순차 + 원 미만 절사(명세 7장 · STmate 18번 §2.1). + "hourly_krw": _money( + hourly_operator_wage(Decimal(str(wage)), digits=prices.operator_wage_digits) + ), "formula": "일당 ÷ 8시간 × 16/12 × 25/20", } ) diff --git a/B09_Estimation/B09_Estimation_MachineCost.py b/B09_Estimation/B09_Estimation_MachineCost.py index 3c65e5a2c..ca1615f19 100644 --- a/B09_Estimation/B09_Estimation_MachineCost.py +++ b/B09_Estimation/B09_Estimation_MachineCost.py @@ -23,8 +23,9 @@ from __future__ import annotations import json import os +import re from dataclasses import dataclass, field -from decimal import Decimal +from decimal import ROUND_FLOOR, Decimal from typing import Any from B09_Estimation.B09_Estimation_Guards import ( @@ -73,6 +74,30 @@ OPERATOR_HOURS_PER_DAY = 8 #: ⚠ **기계 감가상각도 여기가 아니다** — 상각비는 손료(경비) 쪽이다(품셈 8-1-5 1호). OPERATOR_ALLOWANCE_FACTOR = (Decimal(16) / Decimal(12)) * (Decimal(25) / Decimal(20)) +#: 조종원 **시간당 노임 식** — 위 8시간·계수를 **식 문자열 한 줄**로(STmate 설정 `RXNM_`). +#: ⚠⚠ **왼쪽부터 차례로** 평가하고 **원 미만 절사**(명세 7장 · STmate 18번 §2.1). +#: 계수로 미리 접으면 `267,360 × 0.20833…` = 55,699.99… → 절사 55,699 로 **1원 틀림**. +#: 차례로 하면 33,420 → 534,720 → 44,560 → 1,114,000 → **55,700**(6건×3직종 18/18 일치). +#: ⚠ 위 `OPERATOR_ALLOWANCE_FACTOR` 는 근거 문구·검사용으로만 남김 — 금액에 곱하지 말 것. +OPERATOR_WAGE_FORMULA = "1/8*16/12*25/20" + + +def hourly_operator_wage( + daily_wage: Decimal, formula: str = OPERATOR_WAGE_FORMULA, *, digits: int | None = 0 +) -> Decimal: + """일 노임 → 시간당 노임. 식을 **좌→우 순차**로 풀고 `digits` 자리 아래 절사(`None` 이면 안 자름). + + ⚠ 자르는 자리는 **실무 설정**임 — 실무 여섯 건 중 다섯이 원 미만 절사(55,700), + 2025 울진 소광 한 건이 0.1원 미만 절사(57,077.2). 기본은 다수인 0 자리. + """ + value = Decimal(daily_wage) + for op, number in re.findall(r"([*/]?)\s*(\d+(?:\.\d+)?)", formula): + value = value / Decimal(number) if op == "/" else value * Decimal(number) + if digits is None: + return value + return value.quantize(Decimal(1).scaleb(-digits), rounding=ROUND_FLOOR) + + #: 화면·표가 그대로 띄우는 노티스 한 줄. 계수를 쓴 자리마다 같은 문구가 서야 한다. OPERATOR_ALLOWANCE_NOTICE = ( "조종원 노임에 제수당·상여금·퇴직급여충당금 계수 1.667배(16/12 × 25/20)를 넣었습니다 — " @@ -227,8 +252,6 @@ def hourly_machine_cost( fuel_liters_per_hour: Decimal | None = None, fuel_price_per_liter: Decimal | None = None, operator_daily_wage: Decimal | None = None, - operator_hours_per_day: int = OPERATOR_HOURS_PER_DAY, - operator_allowance_factor: Decimal = OPERATOR_ALLOWANCE_FACTOR, efficiency_factor: Decimal | None = None, ) -> HourlyMachineCost: """시간당 사용료 한 시간분. @@ -257,15 +280,16 @@ def hourly_machine_cost( # TODO(미결 PLAN 9-6): `mach_operator_map` 0건 — 기종별 운전사 직종이 품셈 본문에만 있다. gaps.append("운전사 직종 매핑 미확보 — 노무비 성분 비어 있음") else: - # 나눗수는 8시간 그대로 두고 **계수를 곱한다** — 나눗수를 줄이는 것과 다르다. - labor = (operator_daily_wage / Decimal(operator_hours_per_day)) * operator_allowance_factor - # ㉣ 보조 — 나눗수를 몰래 줄이면 효율을 사용료에 넣은 것이 된다. + # 나눗수는 8시간 그대로 두고 **계수를 곱한다** — 식 문자열을 좌→우로 풂(명세 7장). + exact = hourly_operator_wage(operator_daily_wage, digits=None) + # ㉣ 보조 — 나눗수를 몰래 줄이면 효율을 사용료에 넣은 것이 된다(자르기 전 값으로 봄). check_operator_hours_basis( - labor_per_hour=labor, + labor_per_hour=exact, daily_wage=operator_daily_wage, - hours_per_day=operator_hours_per_day, - allowance_factor=operator_allowance_factor, + hours_per_day=OPERATOR_HOURS_PER_DAY, + allowance_factor=OPERATOR_ALLOWANCE_FACTOR, ) + labor = exact.to_integral_value(rounding=ROUND_FLOOR) return HourlyMachineCost( machine=machine, diff --git a/B09_Estimation/B09_Estimation_MachineExpenseSheet.py b/B09_Estimation/B09_Estimation_MachineExpenseSheet.py index 4d5d86928..46f756cf7 100644 --- a/B09_Estimation/B09_Estimation_MachineExpenseSheet.py +++ b/B09_Estimation/B09_Estimation_MachineExpenseSheet.py @@ -25,8 +25,7 @@ from functools import lru_cache from typing import Any from B09_Estimation.B09_Estimation_MachineCost import ( - OPERATOR_ALLOWANCE_FACTOR, - OPERATOR_HOURS_PER_DAY, + hourly_operator_wage, load_machine_catalog, ) from B09_Estimation.B09_Estimation_PriceBook import PriceKind @@ -150,8 +149,9 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]: ), "operator_code": occupation, "operator_daily_wage": _money(wage), + # 식 좌→우 순차 + 원 미만 절사(명세 7장) — 계수로 접으면 1원 틀림. "operator_krw_per_hour": _money( - (wage / Decimal(OPERATOR_HOURS_PER_DAY)) * OPERATOR_ALLOWANCE_FACTOR + hourly_operator_wage(wage, digits=getattr(build, "operator_wage_digits", 0)) if wage is not None else None ), diff --git a/B09_Estimation/B09_Estimation_MachineProductivity.py b/B09_Estimation/B09_Estimation_MachineProductivity.py index 530c92290..9dd9335a9 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity.py @@ -32,7 +32,7 @@ from __future__ import annotations import re from dataclasses import dataclass -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from typing import Any from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog @@ -75,6 +75,18 @@ class ProductivityError(ValueError): """시공능력을 못 세운 경우. 0 이나 가운데값으로 때우지 않는다.""" +_HUNDREDTH = Decimal("0.01") + + +def fix2(value: Decimal) -> Decimal: + """**소수 2자리로 먼저 확정**(사사오입) — 작업량 Q 와 토량환산계수 f(명세 7장 · STmate 18번 §2.2). + + ⚠ 원값으로 나누면 어긋남 — `55,700 ÷ 15.708 = 3,545.96` ≠ `55,700 ÷ 15.71 = 3,545.5`. + f 도 `1/1.175 = 0.85` 로 먼저 자른 뒤 곱함(봉화 2024 단가산출근거 제2호표). + """ + return Decimal(value).quantize(_HUNDREDTH, rounding=ROUND_HALF_UP) + + def parse_measure(cell: str) -> Decimal | None: """계수 셀 하나를 수로 읽는다. **확정값이 아니면 `None`.** @@ -117,7 +129,12 @@ class CycleFactors: def formula_text(self) -> str: return ( f"Q = 3600 ÷ {self.cycle_seconds} × {self.bucket_capacity_m3} × " - f"{self.bucket_coefficient} × {self.volume_factor} × {self.efficiency}" + f"{self.bucket_coefficient} × {fix2(self.volume_factor)} × {self.efficiency}" + + ( + f" = {hourly_output(self)} ㎥/hr (f·Q 소수 2자리 확정)" + if self.cycle_seconds > 0 + else "" + ) ) @@ -134,23 +151,23 @@ class FactorGap: def hourly_output(factors: CycleFactors) -> Decimal: """시간당 작업량 `Q` (㎥/hr). - `Q = (3600 ÷ Cm) · q · K · f · E` — 품셈 8-1-4. + `Q = (3600 ÷ Cm) · q · K · f · E` — 품셈 8-1-4. f 와 Q 는 **소수 2자리로 확정**(`fix2`). """ if factors.cycle_seconds <= 0: raise ProductivityError( f"{factors.work_item_code}: 1싸이클 시간(Cm)이 {factors.cycle_seconds} 입니다." ) - cycles_per_hour = _SECONDS_PER_HOUR / factors.cycle_seconds output = ( - cycles_per_hour + _SECONDS_PER_HOUR * factors.bucket_capacity_m3 * factors.bucket_coefficient - * factors.volume_factor + * fix2(factors.volume_factor) * factors.efficiency + / factors.cycle_seconds ) if output <= 0: raise ProductivityError(f"{factors.work_item_code}: 시간당 작업량이 {output} 입니다.") - return output + return fix2(output) def machine_hours_per_unit(factors: CycleFactors) -> Decimal: @@ -358,8 +375,6 @@ def attach_machine_share( if node is None: return _ZERO - from B09_Estimation.B09_Estimation_PriceBook import PriceDetail - for table in node.get("tables", []): factors = extract_cycle_factors(work_item_code, table, choices, machines) if not isinstance(factors, CycleFactors): @@ -381,19 +396,18 @@ def attach_machine_share( if factors.machine_ratio_pct is None else Decimal(str(factors.machine_ratio_pct)) / Decimal(100) ) - book.add_detail( - PriceDetail( - title_code, - hourly_code, - machine_hours_per_unit(factors) * share, - # 계수를 남의 절에서 빌려 왔으면 **그 사실을 줄 비고에 적는다.** - note=factors.formula_text - + ( - f" · {(sources or {}).get(work_item_code, '')}" - if (sources or {}).get(work_item_code) - else "" - ), - ) + # Q 로 선 장비 줄 — D(단가산출)에 달고 B 는 D 를 1 로 부름(층 차례 X → D → B). + book.add_output_detail( + title_code, + hourly_code, + machine_hours_per_unit(factors) * share, + # 계수를 남의 절에서 빌려 왔으면 **그 사실을 줄 비고에 적는다.** + note=factors.formula_text + + ( + f" · {(sources or {}).get(work_item_code, '')}" + if (sources or {}).get(work_item_code) + else "" + ), ) cycle_factors[work_item_code] = factors return share * Decimal(100) diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py index f8d866418..47235a4ad 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py @@ -29,6 +29,8 @@ import re from decimal import Decimal from typing import Any +from B09_Estimation.B09_Estimation_MachineProductivity import fix2 + _ZERO = Decimal(0) #: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이거나 「A」처럼 기호뿐이다. @@ -105,19 +107,13 @@ def capacity_per_hour(factors: dict[str, Decimal], formula: str = FORMULA_ROLLER """시간당 작업량(㎥/시간). **식이 둘**이라 어느 식인지 함께 받는다. 롤러 `Q = 1000 × V × W × E × D × f / N` · 콤펙터 `Q = A × N × H × f × E / P` + ⚠ f 와 Q 는 **소수 2자리로 확정**(명세 7장 — 원값으로 나누면 성분 단가가 어긋남). """ + f = fix2(factors["f"]) if formula == FORMULA_PLATE: - return ( - factors["A"] * factors["N"] * factors["H"] * factors["f"] * factors["E"] / factors["P"] - ) - return ( - Decimal(1000) - * factors["V"] - * factors["W"] - * factors["E"] - * factors["D"] - * factors["f"] - / factors["N"] + return fix2(factors["A"] * factors["N"] * factors["H"] * f * factors["E"] / factors["P"]) + return fix2( + Decimal(1000) * factors["V"] * factors["W"] * factors["E"] * factors["D"] * f / factors["N"] ) diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py b/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py index 8ff27a9bd..5c533fa7f 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py @@ -30,6 +30,7 @@ from B09_Estimation.B09_Estimation_MachineProductivity import ( ProductivityError, _first_measure, extract_cycle_factors, + fix2, parse_measure, ) @@ -112,12 +113,13 @@ class DozerFactors: def formula_text(self) -> str: return ( f"Q = 60 ÷ {self.cycle_minutes:.4f}분 × ({self.blade_capacity_m3} × " - f"{self.distance_factor}) × {self.volume_factor} × {self.efficiency}" + f"{self.distance_factor}) × {fix2(self.volume_factor)} × {self.efficiency}" + + (f" = {dozer_hourly_output(self)} ㎥/hr" if self.cycle_minutes > 0 else "") ) def dozer_hourly_output(factors: DozerFactors) -> Decimal: - """불도저 시간당 작업량 `Q` (㎥/hr). **밑수는 60(분)** 이다.""" + """불도저 시간당 작업량 `Q` (㎥/hr). **밑수는 60(분)** — f 와 Q 는 소수 2자리로 확정(명세 7장).""" if factors.cycle_minutes <= 0: raise ProductivityError(f"{factors.work_item_code}: 싸이클 시간이 0 이하입니다.") blade = factors.blade_capacity_m3 * factors.distance_factor @@ -125,12 +127,12 @@ def dozer_hourly_output(factors: DozerFactors) -> Decimal: _MINUTES_PER_HOUR / factors.cycle_minutes * blade - * factors.volume_factor + * fix2(factors.volume_factor) * factors.efficiency ) if output <= 0: raise ProductivityError(f"{factors.work_item_code}: 시간당 작업량이 {output} 입니다.") - return output + return fix2(output) def dozer_speeds( @@ -399,8 +401,6 @@ def attach_dozer_share( 붙으면 장비를 두 번 세는 것이 된다. 그래서 부르는 쪽이 **먼저 이쪽을 보고, 안 붙었을 때만** 굴착기 쪽으로 간다. """ - from B09_Estimation.B09_Estimation_PriceBook import PriceDetail - node = next( (w for w in master.get("work_items", []) if w.get("work_item_code") == work_item_code), None, @@ -427,13 +427,9 @@ def attach_dozer_share( note=f"{factors.machine_name} 의 시간당 사용료가 아직 안 섰습니다.", ) continue - book.add_detail( - PriceDetail( - title_code, - hourly_code, - dozer_machine_hours_per_unit(factors), - note=factors.formula_text, - ) + # Q 로 선 장비 줄 — D(단가산출)에 달고 B 는 D 를 1 로 부름(층 차례 X → D → B). + book.add_output_detail( + title_code, hourly_code, dozer_machine_hours_per_unit(factors), factors.formula_text ) return Decimal(100) return _ZERO diff --git a/B09_Estimation/B09_Estimation_ParentSteps.py b/B09_Estimation/B09_Estimation_ParentSteps.py index bb87f9a83..ab79c7dfa 100644 --- a/B09_Estimation/B09_Estimation_ParentSteps.py +++ b/B09_Estimation/B09_Estimation_ParentSteps.py @@ -20,6 +20,7 @@ import re from decimal import Decimal from typing import Any +from B09_Estimation.B09_Estimation_MachineProductivity import fix2 from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle ROCK_CLASSES = ("연암", "보통암", "경암") @@ -107,10 +108,9 @@ def _add_rock_leaf(build: Any, node: dict[str, Any]) -> None: 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]}") - ) + # 평균 Q 도 **소수 2자리로 확정**한 뒤 나눔(명세 7장) — (5.0+3.4+2.6)/3 = 3.67. + average = fix2(sum((q for _, q, _ in rows), Decimal(0)) / Decimal(len(rows))) + variants.append((AVERAGE_VARIANT, average, f"Q = {average} ㎥/hr — {AVERAGE_BASIS[code]}")) for variant, capacity, note in variants: title_code = f"B-{code}#{variant}" build.book.add_title( @@ -123,13 +123,9 @@ def _add_rock_leaf(build: Any, node: dict[str, Any]) -> None: ) ) for machine_code, _name in machines: - build.book.add_detail( - PriceDetail( - title_code, - f"X-{machine_code}", - Decimal(1) / capacity, - note=f"{note} · {source}", - ) + # Q 로 선 장비 줄 — D(단가산출)에 달고 B 는 D 를 1 로 부름(층 차례 X → D → B). + build.book.add_output_detail( + title_code, f"X-{machine_code}", Decimal(1) / capacity, f"{note} · {source}" ) build.variants.setdefault(code, []).append(variant) # 표 줄은 다 읽었다 — 남는 것은 치즐뿐(자재 카탈로그가 없어 금액에 안 붙는 알려진 미결). diff --git a/B09_Estimation/B09_Estimation_PriceBasis.py b/B09_Estimation/B09_Estimation_PriceBasis.py index dc35bb2b8..e24414234 100644 --- a/B09_Estimation/B09_Estimation_PriceBasis.py +++ b/B09_Estimation/B09_Estimation_PriceBasis.py @@ -1,11 +1,15 @@ -"""B09 원가계산 — ③ 단가산출서 `D` 층 (PLAN 9-1 · 9-3). +"""B09 원가계산 — ③ 단가산출서 `D` 층 (PLAN 9-1 · 9-3 · 6장 층 차례). -**무엇인가** — 내역서 한 줄의 단가가 **어떻게 나왔는지** 보이는 표다. 실무 내역서는 -줄마다 비고에 「단산 46 참조」처럼 **참조번호**를 적고, 그 번호의 산출서를 펴서 검산한다 -(8-13 관측). STC 실측도 `D01341 절토(토사) 굴삭기0.7㎥ m³ 1,939` 처럼 **`D` 가 `B` -(일위대가)를 참조하는 한 층 위**였다. +**무엇인가** — 시공능력 **Q** 로 선 기계 몫의 단가가 **어떻게 나왔는지** 보이는 표다. 실무 내역서는 +줄마다 비고에 「단산 46 참조」처럼 **참조번호**를 적고, 그 번호의 산출서를 펴서 검산한다(8-13 관측). - D 단가산출 → B 일위대가 → X 시간당 사용료 → S·M·L 카탈로그 + X 시간당 중기사용료 → D 단가산출(Q 식: 시간당 성분 ÷ Q) → B 일위대가 → 내역 + +⚠⚠ **층 차례 정정(2026-09-13 브레인 판정 · 명세 3장)** — 종전엔 D 가 **B 를 그대로 부르는 껍데기** + (D → B 수량 1)였고 B 가 X 를 바로 불렀음(뒤집힘). 이제 D 는 **Q 쓰는 자리에만** 서고 + (`PriceBook.add_output_detail`), B 가 D 를 수량 1 로 부름. 내역 코드 축은 B 하나(금액 같음). +⚠ 그래서 「단산 N」은 **D 를 품은 내역 줄에만** 붙음 — 사람 품만 있는 일위대가엔 없는 근거라 안 붙음 + (STmate 도 D 가 있는 줄에만). ⚠ **표를 세 벌 만들지 않는다** (PLAN 9-3). `PriceBook` 의 「제목 + 상세」 한 쌍에 `kind` 만 `PRICE_BASIS` 로 얹는다 — 일위대가와 같은 구조, 같은 화면 모양이다. @@ -20,16 +24,14 @@ from dataclasses import dataclass, field from decimal import Decimal from typing import Any -from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle +from B09_Estimation.B09_Estimation_PriceBook import PriceBook, PriceKind from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build -_ONE = Decimal(1) - @dataclass class PriceBasisEntry: - """단가산출서 한 장 — 참조번호 + 그 줄이 무엇을 참조하는지.""" + """단가산출서 한 장 — 참조번호 + 그 산출을 부르는 일위대가.""" number: int code: str @@ -37,8 +39,10 @@ class PriceBasisEntry: spec: str unit: str unit_price_krw: Decimal - #: 이 산출서가 참조하는 일위대가 코드. 화면에서 눌러 내려가는 자리. + #: 이 산출서를 **처음 부른** 일위대가 코드 — 화면에서 눌러 내려가는 자리. ref_code: str + #: 이 산출서를 부르는 내역 일위대가 전부(갈래·단계 합산 부모 포함). + unit_price_codes: list[str] = field(default_factory=list) @property def label(self) -> str: @@ -65,58 +69,66 @@ class PriceBasisSheet: entries: list[PriceBasisEntry] = field(default_factory=list) def by_unit_price(self, ref_code: str) -> PriceBasisEntry | None: - return next((entry for entry in self.entries if entry.ref_code == ref_code), None) + return next((e for e in self.entries if ref_code in e.unit_price_codes), None) + + def label_for(self, ref_code: str) -> str: + """그 일위대가가 품은 산출서 번호 전부 — 단계 합산 부모는 둘 이상일 수 있음(「단산 3·4」).""" + numbers = [str(e.number) for e in self.entries if ref_code in e.unit_price_codes] + return f"단산 {'·'.join(numbers)} 참조" if numbers else "" def as_dict(self) -> dict[str, Any]: return {"entries": [entry.as_dict() for entry in self.entries]} +def _bases_of(book: PriceBook, code: str, seen: tuple[str, ...] = ()) -> list[str]: + """일위대가가 품은 D — 단계 합산 부모(B → 잎 B)는 잎까지 내려가 찾음. 줄 차례 그대로.""" + found: list[str] = [] + for detail in book.details.get(code, []): + ref = detail.ref_code + title = book.titles.get(ref) + if title is None or ref == code or ref in seen: + continue + if title.kind is PriceKind.PRICE_BASIS: + found.append(ref) + elif title.kind is PriceKind.UNIT_PRICE: + found.extend(b for b in _bases_of(book, ref, (*seen, code)) if b not in found) + return found + + def build_price_basis( unit_price_codes: list[str], build: UnitPriceBuild | None = None, ) -> PriceBasisSheet: - """내역서에 쓰인 일위대가마다 산출서 한 장을 세운다. + """내역에 쓰인 일위대가가 품은 **단가산출(D)** 마다 한 장. 번호는 **쓰인 차례**로 매긴다 — 실무 참조번호가 그 내역서 안의 차례이기 때문이다. - 같은 일위대가가 두 줄에 쓰이면 **산출서는 한 장**이고 두 줄이 같은 번호를 가리킨다. + 같은 D 를 두 줄이 부르면 **산출서는 한 장**이고 두 줄이 같은 번호를 가리킨다. + ⚠ D 가 없는 일위대가(사람 품만)는 산출서가 없음 — 없는 근거를 가리키지 않음. """ prices = build or cached_build() + book = prices.book sheet = PriceBasisSheet() - seen: set[str] = set() - + numbered: dict[str, PriceBasisEntry] = {} for code in unit_price_codes: - if not code or code in seen or code not in prices.book.titles: + if not code or code not in book.titles: continue - seen.add(code) - title = prices.book.title(code) - money = prices.book.resolve(code) - basis_code = f"D-{code[2:]}" if code.startswith("B-") else f"D-{code}" - - if basis_code not in prices.book.titles: - prices.book.add_title( - PriceTitle( - code=basis_code, - kind=PriceKind.PRICE_BASIS, + for basis in _bases_of(book, code): + entry = numbered.get(basis) + if entry is None: + title = book.title(basis) + entry = PriceBasisEntry( + number=len(sheet.entries) + 1, + code=basis, name=title.name, spec=title.spec, unit=title.unit, + unit_price_krw=round_at(book.resolve(basis).total, OutputPlace.UNIT_PRICE_ROW), + ref_code=code, ) - ) - # ⚠ 지금은 **일위대가를 그대로 한 줄로** 참조한다. 할증·기타 비용이 붙는 - # 자리가 생기면 여기에 줄이 는다 — 구조를 미리 열어 둔다. - prices.book.add_detail(PriceDetail(basis_code, code, _ONE, note="일위대가 그대로")) - - sheet.entries.append( - PriceBasisEntry( - number=len(sheet.entries) + 1, - code=basis_code, - name=title.name, - spec=title.spec, - unit=title.unit, - unit_price_krw=round_at(money.total, OutputPlace.UNIT_PRICE_ROW), - ref_code=code, - ) - ) + sheet.entries.append(entry) + numbered[basis] = entry + if code not in entry.unit_price_codes: + entry.unit_price_codes.append(code) return sheet @@ -124,42 +136,13 @@ def price_basis_detail( code: str, build: UnitPriceBuild | None = None, ) -> dict[str, Any]: - """산출서 한 장의 본표 — 무엇을 참조해 그 단가가 나왔는지. + """산출서 한 장의 본표 — Q 식 줄(시간당 중기사용료 × 1/Q)과 성분 합. - 일위대가 본표와 **같은 모양**이라 화면이 같은 표를 쓴다. + 일위대가 본표와 **같은 모양**(`detail_of`)이라 화면이 같은 표를 쓴다. """ from B09_Estimation.B09_Estimation_UnitPrice import detail_of prices = build or cached_build() - title = prices.book.title(code) - rows: list[dict[str, Any]] = [] - for detail in prices.book.details.get(code, []): - child = prices.book.title(detail.ref_code) - money = prices.book.resolve(detail.ref_code).scaled(detail.quantity) - rows.append( - { - "code": detail.ref_code, - "name": child.name, - "spec": child.spec, - "unit": child.unit, - "quantity": str(detail.quantity), - "total": str(round_at(money.total, OutputPlace.UNIT_PRICE_ROW)), - "drillable": True, - "note": detail.note, - } - ) - - money = prices.book.resolve(code) - return { - "code": code, - "name": title.name, - "spec": title.spec, - "unit": title.unit, - "rows": rows, - "total": str(round_at(money.total, OutputPlace.UNIT_PRICE_ROW)), - "material": str(money.material), - "labor": str(money.labor), - "expense": str(money.expense), - # 한 층 아래(일위대가) 본표를 그대로 딸려 보낸다 — 화면이 두 번 물어보지 않게. - "unit_price": detail_of(prices, rows[0]["code"]) if rows else None, - } + if prices.book.title(code).kind is not PriceKind.PRICE_BASIS: + raise LookupError(f"단가산출서 코드가 아닙니다: {code}") + return detail_of(prices, code) diff --git a/B09_Estimation/B09_Estimation_PriceBook.py b/B09_Estimation/B09_Estimation_PriceBook.py index b5328f756..5b41c6dd9 100644 --- a/B09_Estimation/B09_Estimation_PriceBook.py +++ b/B09_Estimation/B09_Estimation_PriceBook.py @@ -20,7 +20,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from decimal import Decimal +from decimal import ROUND_FLOOR, Decimal from enum import Enum _ZERO = Decimal(0) @@ -57,6 +57,15 @@ class PriceKind(str, Enum): #: 카탈로그 층 — 상세를 갖지 않고 값이 바로 있는 종류. CATALOG_KINDS = frozenset({PriceKind.MATERIAL, PriceKind.LABOR, PriceKind.MACHINE_BASE}) +#: **호표 안에서 자르는** 층 — 줄 금액은 0.1원 미만, 성분 소계는 원 미만 절사(명세 7장). +#: 근거 STmate 17번 — 중기사용료 호표 성분 소계 398/398 절사 +#: (굴삭기 0.7㎥ 23,128 + 55,700 + 18,015 = 96,843 · 골든셋 실무 143 호표 전수). +#: D(단가산출)도 같음 — 줄 `중기 성분 ÷ Q` 0.1원 · 머리 성분 원 미만(봉화 2024 제2호표 3,545.5 → 3,545). +#: ⚠ 일위대가(B) 호표도 같은 규칙(345 중 94.2%)이나 **아래층(D)부터 맞춘 뒤** 붙임(PLAN 6장 판정). +TRUNCATED_KINDS = frozenset({PriceKind.MACHINE_HOURLY, PriceKind.PRICE_BASIS}) +_TENTH = Decimal("0.1") +_WON = Decimal(1) + class PriceBookError(LookupError): """단가 조립이 성립하지 않는 경우. 0 으로 때우지 않고 멈춘다.""" @@ -84,6 +93,15 @@ class Money3: def scaled(self, factor: Decimal) -> Money3: return Money3(self.material * factor, self.labor * factor, self.expense * factor) + def floored(self, unit: Decimal) -> Money3: + """성분마다 `unit` 자리 아래 절사 — 호표 줄(0.1원)·성분 소계(원) 자르기.""" + return Money3( + *( + v.quantize(unit, rounding=ROUND_FLOOR) + for v in (self.material, self.labor, self.expense) + ) + ) + @dataclass class PriceTitle: @@ -181,6 +199,30 @@ class PriceBook: def add_detail(self, detail: PriceDetail) -> None: self.details.setdefault(detail.parent_code, []).append(detail) + def add_output_detail( + self, parent_code: str, ref_code: str, quantity: Decimal, note: str = "" + ) -> str: + """**시공능력 Q** 로 선 장비 줄 — B 에 바로 안 달고 **D(단가산출)** 에 달고 B 는 D 를 1 로 부름. + + 층 차례 `X → D → B`(명세 3장 정정 · PLAN 6장 판정): D 는 Q 쓰는 자리에만 서고, 내역 코드 축은 + B 하나라 B → D 수량 1(금액 같음). 한 B 에 Q 줄이 여럿이면 **D 한 장**에 모임. D 코드를 돌려줌. + """ + parent = self.title(parent_code) + basis_code = f"D-{parent_code[2:]}" if parent_code.startswith("B-") else f"D-{parent_code}" + if basis_code not in self.titles: + self.add_title( + PriceTitle( + code=basis_code, + kind=PriceKind.PRICE_BASIS, + name=parent.name, + spec=parent.spec, + unit=parent.unit, + ) + ) + self.add_detail(PriceDetail(parent_code, basis_code, Decimal(1), note="단가산출(Q)")) + self.add_detail(PriceDetail(basis_code, ref_code, quantity, note=note)) + return basis_code + def title(self, code: str) -> PriceTitle: try: return self.titles[code] @@ -207,6 +249,8 @@ class PriceBook: raise PriceBookError(f"{code} ({title.name}): 상세 줄이 없어 단가를 조립할 수 없습니다") total = Money3() + # 호표 안에서 자르는 층이면 줄 금액을 0.1원 미만 절사해 쌓고 끝에 성분 소계를 원 미만 절사. + truncate = title.kind in TRUNCATED_KINDS # 제잡비 밑수로 쓸 **사람 품(직접노무비)** — 기계 줄 안의 조종원 노임은 안 센다. # 근거는 아래 `percent_of_labor` 자리 주석의 인용 셋. direct_labor = Decimal(0) @@ -264,9 +308,10 @@ class PriceBook: # ⚠ 밑수가 **명시된 잡재료를 뺀 주재료비**여야 하는데, 품셈이 명시한 잡재료는 # 이미 자원 줄로 서 있어 이 밑수에 함께 든다 — 그 공종에 잡재료가 명시돼 # 있으면 1-2-6 이 애초에 안 걸리는 자리이므로 **칸을 비워 두는 것이 맞다.** - total = total + Money3( - material=direct_material * row.percent_of_material / Decimal(100) - ) + # ⚠ 중기(X) 호표의 **잡품**도 이 줄 — 「주연료비 × 율」의 가산 행(STmate 17번 §3, + # 잡품 단가 칸 = 연료 금액). 수량 × 단가로 연료 수량에 접으면 줄이 사라짐. + share = Money3(material=direct_material * row.percent_of_material / Decimal(100)) + total = total + (share.floored(_TENTH) if truncate else share) continue child = self.resolve(row.ref_code, (*_seen, code)) @@ -275,12 +320,14 @@ class PriceBook: total = total + total.scaled(row.percent_of_parent / Decimal(100)) continue scaled = child.scaled(row.quantity) + if truncate: + scaled = scaled.floored(_TENTH) if self.titles[row.ref_code].kind is PriceKind.LABOR: direct_labor = direct_labor + scaled.labor if self.titles[row.ref_code].kind is PriceKind.MATERIAL: direct_material = direct_material + scaled.material total = total + scaled - return total + return total.floored(_WON) if truncate else total def material_base(self, code: str) -> Decimal: """공구손료·잡재료(산림품셈 1-2-6)의 **밑수** — 그 항목에 바로 붙은 자재 줄의 합. diff --git a/B09_Estimation/B09_Estimation_QuantityDigits.py b/B09_Estimation/B09_Estimation_QuantityDigits.py index 0141c6a18..edd3363cb 100644 --- a/B09_Estimation/B09_Estimation_QuantityDigits.py +++ b/B09_Estimation/B09_Estimation_QuantityDigits.py @@ -23,6 +23,7 @@ from __future__ import annotations +import logging from decimal import ROUND_HALF_UP, Decimal #: (이름에 들어 있는 말, 그 단위) → 소수자리. **원문 표 순서 그대로** 옮겼다. @@ -68,13 +69,22 @@ _DIGITS: tuple[tuple[tuple[str, ...], tuple[str, ...], int], ...] = ( #: 사면적·거푸집 면적까지 1자리로 자르면 틀린다. 확실한 ㎥ 만 잡는다. _UNIT_ONLY = {"㎥": 2, "m3": 2} +logger = logging.getLogger(__name__) +#: 못 맞춘 줄을 이미 알린 (이름, 단위) — 내역을 그릴 때마다 같은 로그가 쌓이지 않게. +_UNMATCHED_LOGGED: set[tuple[str, str]] = set() + def _tight(text: str) -> str: return "".join(str(text or "").split()) def digits_for(name: str, unit: str, spec: str = "") -> int | None: - """그 줄의 수량 소수자리. **표에 없으면 `None`** — 지어내지 않는다.""" + """그 줄의 수량 소수자리. **표에 없으면 `None`** — 지어내지 않는다. + + ⚠ 이름으로 종목을 찾는다 — 품셈 1-2-2 표 자체가 종목 이름 표라 코드로 옮길 대응표가 없음 + (금액이 아닌 표시 자리). **이름을 못 맞춘 줄은 로그**로 남긴다 — 마스터 갈래 키(1장)가 + 서면 함께 정리함(2026-09-13 브레인 판정: 이름은 그대로 두고 못 맞춘 것만 드러냄). + """ haystack = _tight(name) + _tight(spec) unit_tight = _tight(unit) for words, units, digits in _DIGITS: @@ -82,6 +92,16 @@ def digits_for(name: str, unit: str, spec: str = "") -> int | None: continue if any(word in haystack for word in words): return digits + key = (_tight(name), unit_tight) + if key not in _UNMATCHED_LOGGED: + _UNMATCHED_LOGGED.add(key) + fallback = _UNIT_ONLY.get(unit_tight) + logger.info( + "B09 수량 자리 — 종목 이름을 못 맞춤: 「%s」 %s → %s", + name, + unit, + "단위만으로 %s자리" % fallback if fallback is not None else "표시 자리 없음", + ) return _UNIT_ONLY.get(unit_tight) diff --git a/B09_Estimation/B09_Estimation_ResourceAxis.py b/B09_Estimation/B09_Estimation_ResourceAxis.py index 1ef60584c..b964d3d18 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis.py @@ -240,6 +240,8 @@ class ResourceRow: #: ⚠ **이 값을 안 보면 단가가 조용히 틀린다** — 인력 몫 원단위를 전량에 곱하게 된다 #: (2026-09-08 실측: 측구터파기 39,575.6원/㎥ 이 인력 10 % 몫만이었다). group_ratio_pct: Decimal | None = None + #: 이 줄을 읽어 낸 품셈 판 — `pum_table_id`·`work_item_code` 가 판에 묶임(명세 17장). + pum_edition: str = "" def as_dict(self) -> dict[str, Any]: return { @@ -258,6 +260,7 @@ class ResourceRow: None if self.alternative_amount is None else str(self.alternative_amount) ), "group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct), + "pum_edition": self.pum_edition, } @@ -441,6 +444,19 @@ def match_table( # 「구 분 | 콘크리트공(인) | 보통인부(인)」처럼 **열이 자원**이고 행은 규격 갈래 # (무근·철근·소형구조물)다. 행을 자원으로 읽는 길로 보내면 통째로 못 맞춘다 — # 콘크리트 타설(12-1)이 그래서 하나도 안 서고 있었다. + # 공정 줄을 더해 한 품이 되는 표(단끊기 5-16-1) — 사람이 적은 공종만. 아래 길은 표째 버린다. + from B09_Estimation.B09_Estimation_ResourceAxis_ProcessSum import match_process_sum_table + + if match_process_sum_table(node, table, catalog, result, basis_quantity, unit): + return + # 머리가 두 단으로 병합된 표(인력 돌쌓기 13-4-1·13-4-4) — hwpx 병합으로 확인한 공종만. + from B09_Estimation.B09_Estimation_ResourceAxis_TwoLevelHeader import ( + match_two_level_header_table, + ) + + if match_two_level_header_table(node, table, catalog, result, basis_quantity, unit): + return + from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import match_transposed_table if match_transposed_table(node, table, catalog, result, basis_quantity, unit): @@ -653,6 +669,8 @@ def build_resource_axis(master: dict[str, Any], catalog: ResourceCatalog) -> Axi for node in master.get("work_items", []): for table in node.get("tables", []): match_table(node, table, catalog, result) + for row in result.rows: + row.pum_edition = edition return result diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py b/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py new file mode 100644 index 000000000..8cdc0f831 --- /dev/null +++ b/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py @@ -0,0 +1,120 @@ +"""B09 원가계산 — **공정 줄을 더해 한 품이 되는 표** 읽기 (자원 축 보조, 2026-09-13). + + 공정별 | 보통인부(인) | 비고 + 보통토사 | 경질ㆍ고사점토 및 자갈섞인 점토 | 호박돌 섞인 토사 + 절 취 | 2.4 | 3.3 | 5.4 + 수평잡기 및 단정리 | 0.34 | 0.34 | 0.34 + … + 합계 | 2.03 | 2.09 | 2.23 + +행은 공정, 열은 토질 갈래, 자원은 머리 한 칸. 갈래마다 공정 줄을 **더한 것**이 한 품이다. +지금은 「숫자 칸이 자원 열보다 많다」로 표째 버려져 단끊기 5-16-1 이 한 줄도 안 섰다. + +⚠ **모양만으로는 「더하는 표」인지 못 가른다** — 뭉기기 13-12-1 은 같은 모양인데 줄마다 단위가 + 달라(절취 ㎥ · 면고르기 시간/㎡) 더하면 틀린다. 그래서 **사람이 적은 공종만** 더한다. +⚠ 원문 합계 줄은 안 읽는다 — 줄 합과 다르면 **까닭을 적어 둔 공종만** 서고, 아니면 막는다. +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_ResourceAxis import ( + AxisResult, + ResourceCatalog, + ResourceRow, + UnmatchedRow, + parse_amount, +) + +_TOTAL_LABELS = ("계", "합계", "소계", "총계") + +#: 공종 → 근거 · 원문 합계 줄과 다를 때의 까닭 · [주]가 더하라는 직종(코드, 보통인부 몇 인당 1인). +PROCESS_SUM: dict[str, dict[str, Any]] = { + "FP-05-16-01": { + "basis": ( + "품셈 5-16-1 표 머리 (인/100m당) · [주]④ 「수평잡기 및 단정리, 잡석 및 뿌리정리," + " 성토면고르기, 고르기품은 100m당 품」 · [주]③ 절취량 0.15㎥/m 기준" + ), + "total_mismatch": ( + "원문 합계 줄(2.03·2.09·2.23)은 절취를 ㎥당(2.4÷15=0.16)으로 더한 값이라 줄 합과 다름" + " — 100m당 줄을 더함(STmate 단끊기 산출과 같음)" + ), + "per_workers": ("1001", Decimal(20), "[주]⑤ 작업반장 1인/보통인부 20인 가산"), + }, +} + + +def _tight(text: Any) -> str: + return "".join(str(text or "").split()) + + +def match_process_sum_table( + node: dict[str, Any], + table: dict[str, Any], + catalog: ResourceCatalog, + result: AxisResult, + basis_quantity: Decimal | None, + unit: str, +) -> bool: + """적어 둔 공종의 공정 합산 표를 읽는다. 그 공종이 아니면 `False`.""" + from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import transposed_columns + + code = str(node.get("work_item_code") or "") + spec = PROCESS_SUM.get(code) + if spec is None: + return False + table_id = str(table.get("pum_table_id", "")) + rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])] + columns = transposed_columns(table, catalog) + + def block(cell: str, why: str) -> bool: + result.unmatched.append(UnmatchedRow(code, table_id, cell, why)) + result.partial_items[code] = why + return True + + labels = [cell for cell in (rows[0] if rows else []) if cell] + if len(columns) != 1 or len(labels) < 2 or basis_quantity in (None, 0): + return block(code, "공정 합산 표 모양이 아닙니다(자원 한 칸 · 갈래 줄 · 밑수)") + sums = [Decimal(0)] * len(labels) + totals: list[Decimal] | None = None + for cells in rows[1:]: + numbers = [value for value in (parse_amount(c) for c in cells[1:]) if value is not None] + if not cells or not cells[0] or not numbers: + continue + if len(numbers) != len(labels): + return block(cells[0], f"값 {len(numbers)} 개가 갈래 {len(labels)} 개와 안 맞습니다") + if _tight(cells[0]) in _TOTAL_LABELS: + totals = numbers + continue + sums = [total + value for total, value in zip(sums, numbers)] + if totals is not None and totals != sums and not spec.get("total_mismatch"): + return block("합계", f"원문 합계 {totals} 가 줄 합 {sums} 와 다릅니다") + + entry = columns[0][1] + members = [(entry, Decimal(1))] + if spec.get("per_workers"): + chief_code, workers, _note = spec["per_workers"] + chief = next((e for e in catalog.entries if e.code == chief_code), None) + if chief is None: + return block(chief_code, "[주]가 더하라는 직종이 카탈로그에 없습니다") + members.append((chief, Decimal(1) / workers)) + for label, total in zip(labels, sums): + for member, ratio in members: + result.rows.append( + ResourceRow( + work_item_code=code, + pum_table_id=table_id, + pum_form=str(table.get("pum_form", "")), + resource_kind=member.kind, + resource_code=member.code, + resource_name=member.name, + resource_spec=member.spec, + amount=total * ratio / basis_quantity, + amount_unit=unit, + raw_row_index=0, + variant=label, + ) + ) + return True diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_TwoLevelHeader.py b/B09_Estimation/B09_Estimation_ResourceAxis_TwoLevelHeader.py new file mode 100644 index 000000000..0eb28bcc7 --- /dev/null +++ b/B09_Estimation/B09_Estimation_ResourceAxis_TwoLevelHeader.py @@ -0,0 +1,126 @@ +"""B09 원가계산 — **머리가 두 단으로 병합된 표** 읽기 (자원 축 보조, 2026-09-13). + +인력 돌쌓기 13-4-1 메쌓기 · 13-4-4 찰쌓기 — hwpx 원본의 병합 칸으로 머리 모양을 확인함. + + 뒷길이(㎝) | 견치돌(4칸) | 깬돌(4칸) | 깬잡석(4칸) | 호박돌 및 야면석(2칸·두 줄 병합) + | 골쌓기 켜쌓기 | 골쌓기 켜쌓기 | 골쌓기 켜쌓기 | + | 석공 | 보통인부 × 7 벌 + 35 | 0.50 | 0.40 | 0.55 | 0.44 | … | 0.16 | 0.14 + +마스터는 병합을 풀어 칸만 늘어놓아 **어느 값이 어느 돌·쌓기인지**가 사라졌다 — 행 읽기 길은 +「값 묶음 0 개가 갈래 6 개와 안 맞습니다」로 표째 버렸다. + +읽는 법 — 자원 줄의 되풀이 폭(석공·보통인부 = 2)으로 **잎 칸 수**(14 ÷ 2 = 7)를 얻고, +소분류 줄의 되풀이(골·켜 × 3)가 **앞의 대분류 셋**을 두 잎씩 먹고, 남은 대분류가 한 잎씩 먹는다. +⚠ 「앞에서부터」는 hwpx 병합으로 확인한 두 표에만 참이다 — **적어 둔 공종만** 읽고, + 칸 수가 딱 맞지 않으면 한 줄도 안 세우고 막는다. +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_ResourceAxis import ( + AxisResult, + ResourceCatalog, + ResourceRow, + UnmatchedRow, + parse_amount, +) + +#: 공종 → 확인 근거. 여기 없는 공종은 이 길로 안 읽는다. +TWO_LEVEL_HEADER: dict[str, str] = { + "FP-13-04-01": "hwpx 병합 — 견치돌·깬돌·깬잡석 4칸(골·켜) · 호박돌 및 야면석 2칸(행 병합)", + "FP-13-04-04": "hwpx 병합 — 견치돌·깬돌·깬잡석 4칸(골·켜) · 호박돌 및 야면석 2칸(행 병합)", +} +_ABSENT = ("-", "-", "–", "—") + + +def _label(text: Any) -> str: + """「견 치 돌」처럼 벌려 쓴 한 낱말은 붙이고, 「호박돌 및 야면석」은 그대로.""" + parts = str(text or "").split() + return "".join(parts) if parts and all(len(p) == 1 for p in parts) else " ".join(parts) + + +def _period(items: list[str]) -> int: + """되풀이 폭 — 딱 나뉘는 가장 짧은 주기. 없으면 전체 길이.""" + for width in range(1, len(items) + 1): + if len(items) % width == 0 and items == items[:width] * (len(items) // width): + return width + return len(items) + + +def match_two_level_header_table( + node: dict[str, Any], + table: dict[str, Any], + catalog: ResourceCatalog, + result: AxisResult, + basis_quantity: Decimal | None, + unit: str, +) -> bool: + """적어 둔 공종의 두 단 머리 표를 읽는다. 그 공종이 아니면 `False`.""" + from B09_Estimation.B09_Estimation_CrewOutput import _resolve + from B09_Estimation.B09_Estimation_ResourceAxis_ThreeAxis import _axis_label + + code = str(node.get("work_item_code") or "") + if code not in TWO_LEVEL_HEADER: + return False + table_id = str(table.get("pum_table_id", "")) + head = list(table.get("condition_note") or []) + rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])] + + def block(why: str) -> bool: + result.unmatched.append(UnmatchedRow(code, table_id, " | ".join(head), why)) + result.partial_items[code] = why + return True + + names = [c for c in rows[1] if c] if len(rows) >= 3 else [] + entries = [_resolve(catalog, "".join(name.split())) for name in names] + if len(head) < 3 or len(names) < 2 or not all(entries): + # 같은 절에 딸린 다른 표(뒷길이 표준·표준경사)다 — 원래 길로 보낸다. + return False + if basis_quantity in (None, 0): + return block("두 단 머리 표에 밑수가 없습니다") + groups = [_label(g) for g in head[1:]] + subs = [_label(c) for c in rows[0] if c] + width = _period([e.code for e in entries]) + leaves = len(names) // width + cycle = _period(subs) + split = len(subs) // cycle + if split > len(groups) or split * cycle + (len(groups) - split) != leaves: + return block(f"대분류 {len(groups)} · 소분류 {len(subs)} · 잎 {leaves} 칸이 딱 안 나뉩니다") + leaf_labels = [[g, s] for g in groups[:split] for s in subs[:cycle]] + leaf_labels += [[g] for g in groups[split:]] + + staged: list[ResourceRow] = [] + for index, cells in enumerate(rows[2:], start=2): + values = [c for c in cells[1:] if c] + if not cells or not cells[0]: + continue + readable = all(v in _ABSENT or parse_amount(v) is not None for v in values) + if len(values) != len(names) or not readable: + return block( + f"뒷길이 {cells[0]} 줄의 값 {len(values)} 칸이 자원 {len(names)} 칸과 안 맞습니다" + ) + for position, (value, entry) in enumerate(zip(values, entries)): + if value in _ABSENT: + continue # 그 돌·뒷길이엔 품이 없다 + labels = leaf_labels[position // width] + staged.append( + ResourceRow( + work_item_code=code, + pum_table_id=table_id, + pum_form=str(table.get("pum_form", "")), + resource_kind=entry.kind, + resource_code=entry.code, + resource_name=entry.name, + resource_spec=entry.spec, + amount=parse_amount(value) / basis_quantity, + amount_unit=unit, + raw_row_index=index, + variant=" · ".join([*labels, _axis_label(_label(head[0]), cells[0])]), + ) + ) + result.rows.extend(staged) + return True diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 8b93f3fd5..8d84d9f40 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -275,6 +275,8 @@ async def _build_for(project_id: UUID): str(settings.get("transport_road") or ""), # 품 할인·할증(1-4) — **안 고르면 안 붙는다.** tuple(sorted(parse_labor_surcharge(settings.get("labor_surcharge")).items())), + # 조종원 시간당 노임 자르는 자리 — 실무마다 다름(명세 7장 정정). 안 정하면 원 미만. + str(settings.get("operator_wage_digits") or ""), ) @@ -753,9 +755,12 @@ async def get_bill(project_id: UUID) -> JSONResponse: ⚠ 단가가 없거나 밑수를 모르는 줄은 **0 으로 안 때우고** `missing` 으로 드러낸다 — 화면이 그 목록을 그대로 보인다. """ - from B08_Quantity.B08_Quantity_Router_Material import get_handoff + from B08_Quantity.B08_Quantity_Router_Material import get_handoff, structure_bill_prices try: + # ⚠ **그 프로젝트가 고른 값**으로 조립한 단가표 — 구조물도 미리보기(`_build_for`)와 한 벌. + # 기본값(`cached_build()`)을 쓰면 계수를 고른 프로젝트에서 화면과 내역이 갈림(브레인). + build = await _build_for(project_id) response = await get_handoff(project_id) payload = json.loads(bytes(response.body).decode("utf-8")) except Exception: @@ -767,9 +772,16 @@ async def get_bill(project_id: UUID) -> JSONResponse: if "work_items" not in payload: # B08 이 오류 응답을 준 경우 — 그 사유를 그대로 넘긴다(감추지 않는다). return JSONResponse(status_code=502, content={"status": "error", **payload}) + try: + # 구조물도 호표 금액 — **이 내역과 같은 단가표**로(PLAN 6장 ②). + structure_prices = await structure_bill_prices(project_id, build) + except Exception: + # 못 세면 호표 줄이 금액 없이 「못 받음」 사유로 섬 — 조용히 0 원 안 됨. + logger.exception("B09 내역서 — 구조물도 호표 금액 실패: project_id=%s", project_id) + structure_prices = {} try: - result = build_bill(payload) + result = build_bill(payload, build=build, structure_prices=structure_prices) except DoubleCountError as error: # 이중계상 감시에 걸린 경우 — 표를 그리지 않고 멈춘다. logger.warning("B09 내역서 이중계상 감지: project_id=%s, %s", project_id, error) @@ -803,7 +815,8 @@ async def get_price_basis_detail(project_id: UUID, code: str) -> JSONResponse: from B09_Estimation.B09_Estimation_PriceBasis import price_basis_detail try: - body = price_basis_detail(code) + # 내역서와 같은 단가표 — 번호·금액이 내역 줄과 갈리지 않게. + body = price_basis_detail(code, await _build_for(project_id)) except Exception: logger.exception("B09 단가산출서 조회 실패: project_id=%s, code=%s", project_id, code) return JSONResponse( diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 0207f4616..e0459e0da 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -17,6 +17,7 @@ from __future__ import annotations +import logging import re from dataclasses import dataclass, field from dataclasses import replace as dataclass_replace @@ -26,7 +27,8 @@ from typing import Any from B09_Estimation.B09_Estimation_Guards import check_column_sums, check_surcharge_once from B09_Estimation.B09_Estimation_MachineCost import ( - OPERATOR_ALLOWANCE_FACTOR, + OPERATOR_WAGE_FORMULA, + hourly_operator_wage, load_machine_catalog, ) from B09_Estimation.B09_Estimation_MachineProductivity import ( @@ -34,6 +36,7 @@ from B09_Estimation.B09_Estimation_MachineProductivity import ( FactorGap, attach_machine_share, extract_cycle_factors, + fix2, ) from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import ( attach_dozer_share, @@ -66,6 +69,9 @@ from B09_Estimation.B09_Estimation_WorkItemUnit import unit_of as work_item_unit from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_Transport import parse_distance_km +logger = logging.getLogger(__name__) +#: 조종원 **시간당 노임** 제목 꼬리 — 일 노임 제목(`L…`)과 갈라 둠. 노무비목록표엔 안 실림(일당만). +HOURLY_WAGE_SUFFIX = "#시간" _RE_EMPHASIS = re.compile(r"\*\*(.+?)\*\*") _ZERO = Decimal(0) #: 연료는 자재 카탈로그에 없어 합성 코드로 세운다 — 코드가 있어야 조인이 성립한다. @@ -89,6 +95,8 @@ def _slots(value: Decimal) -> list[Decimal | None]: @dataclass class UnitPriceBuild: book: PriceBook = field(default_factory=PriceBook) + #: 조종원 시간당 노임을 자른 자리(프로젝트 설정 `operator_wage_digits`, 기본 0 = 원 미만). + operator_wage_digits: int = 0 #: 세우지 못한 공종 — 값이 안 서는 것을 빈 줄로 두지 않는다. skipped: list[str] = field(default_factory=list) #: 기계 성분이 비어 있는 채로 쓰인 기종 (연료·조종원 미확보). @@ -216,31 +224,57 @@ def _apply_combined_misc_rate(book: PriceBook, work_item_titles: list[str]) -> i """ swapped = 0 for title_code in work_item_titles: - details = book.details.get(title_code) or [] - if not any(detail.ref_code.startswith(_ATTACHMENT_PREFIXES) for detail in details): + # 장비 줄은 B 에 바로(표 시간) 또는 그 D(단가산출, Q) 에 있음 — 둘을 한 묶음으로 봄(층 차례). + own = book.details.get(title_code) or [] + group = [ + own, + *(book.details.get(d.ref_code) or [] for d in own if d.ref_code.startswith("D-")), + ] + if not any( + d.ref_code.startswith(_ATTACHMENT_PREFIXES) for details in group for d in details + ): continue - for index, detail in enumerate(details): - if not detail.ref_code.startswith("X-") or detail.ref_code.startswith( - _ATTACHMENT_PREFIXES - ): - continue - combined = f"{detail.ref_code}#조합" - if combined not in book.titles: - continue - details[index] = dataclass_replace( - detail, - ref_code=combined, - note=( - (detail.note + " · " if detail.note else "") - + "조합 사용 — 잡재료 16% (품셈 제8장 [주]⑤)" - ), - ) - swapped += 1 + for details in group: + for index, detail in enumerate(details): + if not detail.ref_code.startswith("X-") or detail.ref_code.startswith( + _ATTACHMENT_PREFIXES + ): + continue + combined = f"{detail.ref_code}#조합" + if combined not in book.titles: + continue + details[index] = dataclass_replace( + detail, + ref_code=combined, + note=( + (detail.note + " · " if detail.note else "") + + "조합 사용 — 잡재료 16% (품셈 제8장 [주]⑤)" + ), + ) + swapped += 1 return swapped +#: 중기 호표 잡품 줄 이름표 — 화면이 「공구손료·잡재료(1-2-6)」와 가르는 데 씀. +MISC_NOTE_PREFIX = "잡품" + + +def _misc_row(hourly_code: str, percent: Decimal) -> PriceDetail: + """중기 호표 **잡품** — 주연료비의 % 가 재료비로 붙는 가산 행(자기 호표를 가리키는 비율 줄).""" + return PriceDetail( + hourly_code, + hourly_code, + Decimal(0), + note=f"{MISC_NOTE_PREFIX} — 주연료비 × {percent}%", + percent_of_material=percent, + ) + + def _add_machine_layers( - book: PriceBook, machine_codes: set[str], fuel_region: str | None = None + book: PriceBook, + machine_codes: set[str], + fuel_region: str | None = None, + operator_wage_digits: int = 0, ) -> list[str]: """`S`(취득가) · `L`(운전사) · `M`(연료) 을 세우고 그 위에 `X` 를 올린다. @@ -348,10 +382,11 @@ def _add_machine_layers( liters = record.fuel_liters_per_hour if liters is not None: + book.add_detail(PriceDetail(hourly_code, fuel_code, liters, note="주연료")) if record.misc_material_percent is not None: - # 잡재료는 **주연료의 %** — 유가와 같이 움직인다. - liters = liters * (Decimal(1) + record.misc_material_percent / Decimal(100)) - book.add_detail(PriceDetail(hourly_code, fuel_code, liters, note="주연료 + 잡재료")) + # 잡품은 **주연료비 × 율의 가산 행**(명세 7장 · STmate 17번 §3 「잡품 단가 칸 = 연료 + # 금액」) — 연료 수량에 접지 않음. 유가와 같이 움직이는 것은 그대로. + book.add_detail(_misc_row(hourly_code, record.misc_material_percent)) # 조합 사용(리퍼·브레이커·집게)일 때 쓸 **잡재료 16%** 짜리 층을 함께 세운다. # 같은 기계라도 조합이면 본체 잡재료가 줄어든다(품셈 제8장 [주]⑤). @@ -373,32 +408,42 @@ def _add_machine_layers( PriceDetail( combined_code, fuel_code, - record.fuel_liters_per_hour - * (Decimal(1) + COMBINED_MISC_PERCENT / Decimal(100)), - note=f"주연료 + 잡재료 {COMBINED_MISC_PERCENT}% (조합 사용)", + record.fuel_liters_per_hour, + note="주연료 (조합 사용)", ) ) + book.add_detail(_misc_row(combined_code, Decimal(COMBINED_MISC_PERCENT))) else: incomplete.append(f"{code} (연료소모량 없음)") wage_code = record.operator_occupation_code if wage_code and wage_code in wages and record.operator_person_days is not None: - # ㉣ 나눗수는 8시간 — `PriceDetail` 수량이 「1시간분 인」이 된다. - # 여기에 **제수당·상여금·퇴직급여충당금 계수**(1.667배)를 곱한다. 공표 노임이 - # 기본급여액뿐이라 별도 계상해야 하는 몫이다(`MachineCost` 상수 주석에 근거). - per_hour_person = (record.operator_person_days / Decimal(8)) * OPERATOR_ALLOWANCE_FACTOR - if wage_code not in book.titles: + # ㉣ 나눗수는 8시간 + **제수당·상여금·퇴직급여충당금 계수**(16/12×25/20) — 공표 노임이 + # 기본급여액뿐이라 별도 계상하는 몫(`MachineCost` 상수 주석에 근거). + # ⚠⚠ 계수를 **수량에 접지 않음** — `일당 × 0.20833…` 은 55,699.99… 로 1원 틀림. + # 시간당 노임을 식 좌→우 + 원 미만 절사로 **따로 세운 제목**(`#시간`)을 1시간분 인수로 부름 + # (STmate 중기사용료 호표 「운전원 1 × 55,700」 모양, 명세 7장). + per_hour_person = record.operator_person_days + hourly_wage_code = f"{wage_code}{HOURLY_WAGE_SUFFIX}" + if hourly_wage_code not in book.titles: book.add_title( PriceTitle( - code=wage_code, + code=hourly_wage_code, kind=PriceKind.LABOR, - name="조종원", - unit="인", - slots=_slots(wages[wage_code]), + name="조종원(시간당)", + unit="hr", + slots=_slots( + hourly_operator_wage(wages[wage_code], digits=operator_wage_digits) + ), # ⚠ 조종원도 노임이다 — 같은 플래그가 붙어야 한다. reliability=load_labor_reliability().get(wage_code, ""), ) ) + wage_code = hourly_wage_code + cut = ( + "원 미만" if operator_wage_digits == 0 else f"소수 {operator_wage_digits}자리 아래" + ) + operator_note = f"조종원 (일당 × {OPERATOR_WAGE_FORMULA} 좌→우 · {cut} 절사)" # 조합 층에도 조종원을 같이 단다 — 본체를 모는 사람은 하나뿐이다. combined_code = f"{hourly_code}#조합" if combined_code in book.titles: @@ -407,7 +452,7 @@ def _add_machine_layers( combined_code, wage_code, per_hour_person, - note="조종원 (1일 8시간 × 제수당·상여·퇴직충당 16/12 × 25/20)", + note=operator_note, ) ) book.add_detail( @@ -415,7 +460,7 @@ def _add_machine_layers( hourly_code, wage_code, per_hour_person, - note="조종원 (1일 8시간 × 제수당·상여·퇴직충당 16/12 × 25/20)", + note=operator_note, ) ) else: @@ -512,14 +557,34 @@ def find_variant_code( if _numbers_of(normalize_variant_key(code[len(prefix) :])) == numbers ] if len(hits) == 1: - return hits[0] + return _loose(work_item_code, variant_value, hits[0], "수의 짝") if len(numbers) == 1: # 저장 제원이 **한 값**으로 온다(뒷길이 45㎝). 갈래는 구간이므로 그 값을 담는 # 구간을 고른다 — 「45」 → 「55cm이하」. **가장 좁은 구간**을 고른다. - return _bracket_for(numbers[0], candidates, prefix) + picked = _bracket_for(numbers[0], candidates, prefix) + return _loose(work_item_code, variant_value, picked, "구간") if picked else None return None +#: 느슨한 맞춤을 이미 알린 짝 — 내역을 그릴 때마다 같은 줄이 로그를 채우지 않게. +_LOOSE_LOGGED: set[tuple[str, str, str]] = set() + + +def _loose(work_item_code: str, variant_value: str, picked: str, how: str) -> str: + """글자가 아니라 **수·구간으로** 갈래를 고른 자리 — 로그로 남기고 그대로 돌려줌. + + ⚠ 조용히 다른 갈래에 붙는 자리가 바로 여기라 드러내 둠(명세 14장 정정 · 브레인 판정 (나)). + 갈래의 정본은 지금 **B09 단가표 제목**이고 마스터 `variant_key` 는 첫 열만 담아 못 씀(1장 몫). + """ + key = (work_item_code, str(variant_value), picked) + if key not in _LOOSE_LOGGED: + _LOOSE_LOGGED.add(key) + logger.info( + "B09 갈래 느슨한 맞춤(%s): %s 「%s」 → %s", how, work_item_code, variant_value, picked + ) + return picked + + def _bracket_for(value: Decimal, candidates: list[str], prefix: str) -> str | None: """그 값을 담는 갈래 — 「N 이하」는 상한, 「A 이상~B 미만」은 범위로 본다.""" best: tuple[Decimal, str] | None = None @@ -551,6 +616,7 @@ def build_unit_prices( transport_distance_km: Decimal | None = None, transport_road: str | None = None, labor_surcharge_choices: dict[str, str] | None = None, + operator_wage_digits: int = 0, ) -> UnitPriceBuild: """자원 축을 일위대가(`B`)로 조립한다. @@ -570,6 +636,9 @@ def build_unit_prices( ⚠ `labor_surcharge_choices` — 품의 할인·할증 26계열(산림품셈 1-4). **안 고르면 안 붙는다.** 어느 공종에 붙일지는 원문 [주]가 작업을 지목하므로 **켜는 것은 사용자 몫**이다. + + ⚠ `operator_wage_digits` — 조종원 시간당 노임을 자르는 자리(0 = 원 미만 절사). 실무마다 다름 + (다섯 건 0 · 2025 울진 소광 1) — 규정이 없는 자리라 프로젝트 설정 칸(브레인 판정 2026-09-13). """ from B09_Estimation.B09_Estimation_FactorChoices import ( chosen_values, @@ -583,6 +652,12 @@ def build_unit_prices( ) master = load_work_item_master() + # 합산 단계·표 읽기 선언·가드 목록이 절 번호로 박혀 판에 묶임 — 판이 다르면 조립 안 함(명세 17장). + from common_util.common_util_pum_edition import require_code_edition + + require_code_edition( + str(master.get("effective_date", "")), "B09 단가 조립 — 코드에 박힌 절 번호" + ) if factor_choices is None: factor_choices = chosen_values(scan_range_factors(master)) # 「…와 동일」 참조로 이어 온 계수 — **사용자가 고른 값이 있으면 그것이 이긴다.** @@ -696,7 +771,10 @@ def build_unit_prices( from B09_Estimation.B09_Estimation_Transport import TRANSPORT_VARIANTS machine_codes |= {variant["machine_code"] for variant in TRANSPORT_VARIANTS} - build.incomplete_machines = _add_machine_layers(build.book, machine_codes, fuel_region) + build.operator_wage_digits = operator_wage_digits + build.incomplete_machines = _add_machine_layers( + build.book, machine_codes, fuel_region, operator_wage_digits + ) # 규격 갈래(`variant`)가 있으면 **갈래마다 따로 세운다** — 무근·철근·소형구조물은 # 품이 달라 한 일위대가로 뭉치면 어느 것도 안 맞는다. @@ -884,22 +962,22 @@ def build_unit_prices( if capacity["ratio_pct"] is None else Decimal(str(capacity["ratio_pct"])) / Decimal(100) ) - build.book.add_detail( - PriceDetail( - title_code, - hourly_code, - (Decimal(1) / capacity["capacity_per_hour"]) * group_share, - note=( - f"작업량을 표가 직접 줌 — {capacity['cell']} {capacity['capacity_per_hour']}" - f" (품셈 원문 표기 그대로)" - if not capacity.get("source_text") - # 표가 아니라 절 제목·[주]에서 온 값은 **어디서 왔는지 그대로** 싣는다. - else ( - f"작업량을 원문이 직접 줌 — {capacity['cell']}" - f" {capacity['capacity_per_hour']} · {capacity['source_text']}" - ) - ), - ) + # 작업량(Q)으로 선 장비 줄 — D(단가산출)에 달고 B 는 D 를 1 로 부름(층 차례 X → D → B). + build.book.add_output_detail( + title_code, + hourly_code, + # 작업량도 소수 2자리로 확정한 뒤 나눔(명세 7장) — 표·원문 값은 대개 이미 그 자리. + (Decimal(1) / fix2(capacity["capacity_per_hour"])) * group_share, + note=( + f"작업량을 표가 직접 줌 — {capacity['cell']} {capacity['capacity_per_hour']}" + f" (품셈 원문 표기 그대로)" + if not capacity.get("source_text") + # 표가 아니라 절 제목·[주]에서 온 값은 **어디서 왔는지 그대로** 싣는다. + else ( + f"작업량을 원문이 직접 줌 — {capacity['cell']}" + f" {capacity['capacity_per_hour']} · {capacity['source_text']}" + ) + ), ) attached_capacity = True @@ -929,6 +1007,10 @@ def build_unit_prices( from B09_Estimation.B09_Estimation_ParentSteps import attach_parent_steps attach_parent_steps(build, master) + # 산림 품셈에 절이 없는 공종(모르타르 배합 등) — 명세 2장 ② 갈래 WK. + from B09_Estimation.B09_Estimation_WorkItems_AX import attach_work_items_ax + + attach_work_items_ax(build) # 기계 수송비 — 기계경비의 셋째 몫(손료·운전경비·**수송비**). 거리가 있어야 선다. from B09_Estimation.B09_Estimation_Transport import attach_transport @@ -946,6 +1028,10 @@ def build_unit_prices( build.book, [code for code in build.book.titles if code.startswith("B-")] ) build.labor_reliability = _labor_reliability_in_use(build.book) + # ③ 할증 포함 재료량을 준 공종에 자재가 재료비로 붙지 않았는가(명세 6장 · ㉠). + from B09_Estimation.B09_Estimation_Guards import check_materials_before_surcharge + + check_materials_before_surcharge(build.book) return build @@ -1073,6 +1159,7 @@ def cached_build( transport_distance_km: str = "", transport_road: str = "", labor_surcharge: tuple[tuple[str, str], ...] = (), + operator_wage_digits: str = "", ) -> UnitPriceBuild: """조립 결과를 한 번만 만든다 — 품셈 3 MB 를 요청마다 다시 읽지 않는다. @@ -1099,9 +1186,16 @@ def cached_build( transport_distance_km=parse_distance_km(transport_distance_km), transport_road=transport_road or None, labor_surcharge_choices=dict(labor_surcharge), + operator_wage_digits=parse_operator_wage_digits(operator_wage_digits), ) +def parse_operator_wage_digits(value: Any) -> int: + """조종원 시간당 노임 자르는 자리 설정 — 0·1·2 만 받고 그 밖(빈 값 포함)은 기본 0.""" + text = str(value if value is not None else "").strip() + return int(text) if text in ("0", "1", "2") else 0 + + @dataclass class DirectCostBreakdown: """⑤ 공사원가계산서가 받는 **직접비 3분할**. diff --git a/B09_Estimation/B09_Estimation_UnitPrice_View.py b/B09_Estimation/B09_Estimation_UnitPrice_View.py index 9e41ef499..3d88c18fb 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice_View.py +++ b/B09_Estimation/B09_Estimation_UnitPrice_View.py @@ -224,18 +224,20 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict: _ZERO, ) amount = material_so_far * detail.percent_of_material / Decimal(100) + # 중기 호표의 잡품(주연료비 × 율)도 같은 비율 줄 — 이름표만 가름(품셈 제8장). + misc = detail.note.startswith("잡품") rows.append( { "code": detail.ref_code, - "name": "공구손료·잡재료", - "spec": f"주재료비의 {detail.percent_of_material}%", + "name": "잡품" if misc else "공구손료·잡재료", + "spec": f"{'주연료비' if misc else '주재료비'}의 {detail.percent_of_material}%", "unit": "%", "quantity": str(detail.percent_of_material), "material": str(amount), "labor": "0", "expense": "0", "total": _money_text(amount), - "source": "품셈 1-2-6", + "source": "품셈 제8장" if misc else "품셈 1-2-6", "drillable": False, "note": detail.note, } diff --git a/B09_Estimation/B09_Estimation_WorkItems_AX.py b/B09_Estimation/B09_Estimation_WorkItems_AX.py new file mode 100644 index 000000000..43f771280 --- /dev/null +++ b/B09_Estimation/B09_Estimation_WorkItems_AX.py @@ -0,0 +1,59 @@ +"""B09 원가계산 — **산림 품셈에 절이 없는 공종**(`AX-WK-*`)의 일위대가 (명세 2장 ② 갈래 WK). + +산림 품셈이 **부르기만 하고 품을 안 주는** 자리를 원문이 있는 다른 품셈에서 그대로 옮긴다. +⚠ 코드는 난수 8자리 · 동등 비교만(명세 2장 ④) — 읽기는 이름 칸이 맡음. +⚠ 값은 원문 그대로 · 줄마다 출처. 자재는 단가 층이 없어 **드러내기만** 함(치즐과 같은 자리). +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle + +#: 모르타르 배합 1:3 — 찰쌓기·찰붙임 줄눈메꿈(산림 품셈 13-4-4 [주]④ · 13-5-1 [주]③ 0.009㎥/㎡). +MORTAR_MIX = "AX-WK-c0842a0d" + +WORK_ITEMS: dict[str, dict[str, Any]] = { + MORTAR_MIX: { + "name": "모르타르 배합", + "spec": "1:3", + "unit": "㎥", + "basis": ( + "건설공사 표준품셈(2026) [건축부문] 9-1-1 모르타르 배합(㎥당)" + " — 산림 품셈엔 배합 절이 없음 · 배합비 1:3 은 돌쌓기 시방(시멘트:잔골재 부피비) · 실무 울진 대흥(2024)·소광(2025)" + " 「모르타르배합 1:3」 과 같은 짜임" + ), + # (노임 코드, 수량, 칸) — 원문 [주]② 「배합이 포함된 것이며, 비빔은 제외」. + "labor": (("1002", "0.66", "보통인부 — 모래체가름 포함 칸(제외 칸 0.43)"),), + # 원문 참고자료 1:3 줄 · ※ 「위 재료량은 할증이 포함된 것이다」 — 할증 전 값이 아님. + "materials": ("시멘트 510 kg", "모래 1.10 ㎥"), + }, +} + + +def attach_work_items_ax(build: Any) -> None: + """`B-AX-WK-*` 제목을 세운다 — 노임 층이 없으면 안 세우고 사유를 남긴다.""" + for code, item in WORK_ITEMS.items(): + missing = [ref for ref, _, _ in item["labor"] if ref not in build.book.titles] + if missing: + build.component_gaps[code] = f"노임 단가 층이 없습니다 — {', '.join(missing)}" + continue + title_code = f"B-{code}" + build.book.add_title( + PriceTitle( + code=title_code, + kind=PriceKind.UNIT_PRICE, + name=item["name"], + spec=item["spec"], + unit=item["unit"], + ) + ) + for ref, quantity, cell in item["labor"]: + build.book.add_detail( + PriceDetail(title_code, ref, Decimal(quantity), note=f"{cell} · {item['basis']}") + ) + build.unattached[code] = [ + f"{material} — 자재 단가 층 없음(원문 할증 포함 값)" for material in item["materials"] + ] diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index 74d0a170f..0b312aeed 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -39,6 +39,8 @@ from common_util.common_util_json import atomic_write_json from config.config_system_design import ( EARTHWORK_CONVERSION_C_RANGES, EARTHWORK_CONVERSION_FACTORS, + EARTHWORK_HAUL_EQUIPMENT_LIMITS_M, + EARTHWORK_HAUL_LIMIT_BASIS, ) SETTINGS_FILENAME = "project_settings.json" @@ -90,7 +92,10 @@ def default_settings() -> dict[str, Any]: # 「시공법 미지정」으로 드러난다(2026-09-07 일감 9 에서 드러난 자리). "rock_methods": {}, "conversion_factors_override": None, - "haul_limits_m_override": None, + # 도쟈 한계거리(m) — `None` 은 기본값(config 60 m, 근거 셋 일치)이 선다. + # 설계 조건이라 바꿀 수 있다(2026-09-13 판정). 종무대 20 m 는 규정이라 칸이 없다. + # ⚠ 종전 `haul_limits_m_override` 는 아무도 안 읽던 칸이라 이것으로 갈음했다. + "dozer_haul_limit_m": None, "application_ratios_pct": {key: 100 for key in APPLICATION_RATIO_KEYS}, # 자재총괄의 관급/사급 구분 — `{자재명: "owner_supplied"|"contractor_supplied"}` # 또는 `{자재명: {"supply": …, "install_by": "contractor"|"owner"}}`. @@ -135,6 +140,10 @@ def default_settings() -> dict[str, Any]: # 높은 장소로 **운반하고 쌓아두어야** 한다」. 제거만 세면 법이 요구하는 운반이 빠진다. # 거리는 현장값이라 품셈이 정하지 않는다 — 비면 운반 줄이 막힌 채로 선다. "topsoil_haul_distance_m": None, + # 표토제거 대상 — `None` 은 기본 「노면 + 절토」(별표2 문언) · `"road_only"` 는 노면만. + # ⚠ 노면만은 실무 영월 한 건(1m당 3.34㎡) 관측이라 일반화하지 않고 설계자가 고름 + # (2026-09-14 브레인 판정 — 3배 차이라 칸으로 둠). + "topsoil_target": None, # 임목축적 등급 — 품셈 9-21 제근이 **소·중·밀** 셋으로 갈리는 축. # [주]① 「소림 30~60㎥/㏊ · 중림 60~90 · 밀림 90 이상」 — 본수가 아니라 **축적**이다. # ⚠ 산림조사부·영림계획에서 옮겨 적는 값이라 프로그램이 만들 수 없다. 비면 막는다. @@ -148,6 +157,9 @@ def default_settings() -> dict[str, Any]: # 물을 일도 없다. 켜도 **부피는 지어내지 않는다**(아래 칸이 비면 줄만 서고 사유). "wood_chipping_enabled": False, "wood_chipping_volume_m3": None, + # 노체다짐(9-16-2) — **기본 꺼짐**(2026-09-13 판정). [주]⑤ 「대규모 성토지로서 + # 층다짐이 필요한 경우 적용」 조건부라 늘 서지 않는다. 켜면 토공집계에 별도 줄. + "subgrade_compaction_enabled": False, "dataset_versions": {}, }, "estimation": { @@ -262,6 +274,17 @@ def concrete_placing_method(settings: dict[str, Any]) -> tuple[str, bool]: return DEFAULT_CONCRETE_PLACING_METHOD, True +#: 표토제거 대상 — 기본은 법 문언(별표2 Ⅰ.2.차.(6) 「노면·절토대상지」). +TOPSOIL_TARGETS = ("road_and_cut", "road_only") +DEFAULT_TOPSOIL_TARGET = "road_and_cut" + + +def topsoil_target(settings: dict[str, Any]) -> str: + """표토제거 대상. 모르는 값·안 정함은 기본(노면 + 절토).""" + stored = settings.get("topsoil_target") + return str(stored) if stored in TOPSOIL_TARGETS else DEFAULT_TOPSOIL_TARGET + + def earthwork_conversion_factors(settings: dict[str, Any]) -> dict[str, dict[str, float]]: """이 프로젝트가 쓸 토량환산계수 — **기본값 위에 고른 값만 얹는다.** @@ -318,6 +341,38 @@ def earthwork_conversion_choices(settings: dict[str, Any]) -> dict[str, dict[str return choices +def haul_equipment_limits(settings: dict[str, Any]) -> list[tuple[str, float | None]]: + """이 프로젝트가 쓸 운반장비 거리 경계 — 기본값 위에 **도쟈 한계거리만** 얹는다. + + ⚠ 유토곡선(B06)·운반표가 같은 값으로 서야 한다 — 읽는 자리마다 상수를 들지 말고 + 이 함수를 거친다. + ⚠ 종무대 경계 이하 값은 받지 않는다(도쟈 몫이 사라짐) — 기본값이 선다. + """ + limits = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M) + value = settings.get("dozer_haul_limit_m") + if ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and float(value) > float(limits["free_haul"] or 0.0) + ): + limits["dozer"] = float(value) + return [(key, limits[key]) for key, _ in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M] + + +def haul_limit_choice(settings: dict[str, Any]) -> dict[str, Any]: + """도쟈 한계거리 칸 — 화면이 지금 값·기본값·근거를 함께 보인다.""" + default = dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["dozer"] + value = dict(haul_equipment_limits(settings))["dozer"] + return { + "value": value, + "default": default, + "chosen": value != default, + "basis": EARTHWORK_HAUL_LIMIT_BASIS["dozer"], + "free_haul_m": dict(EARTHWORK_HAUL_EQUIPMENT_LIMITS_M)["free_haul"], + "free_haul_basis": EARTHWORK_HAUL_LIMIT_BASIS["free_haul"], + } + + def application_ratio(settings: dict[str, Any], key: str) -> float: """반영률을 0~1 로. 없으면 100 %(=1.0) — 실무 관측치를 기본값으로 쓰지 않는다.""" raw = (settings.get("application_ratios_pct") or {}).get(key, 100) diff --git a/common_util/common_util_pum_edition.py b/common_util/common_util_pum_edition.py new file mode 100644 index 000000000..42321cf18 --- /dev/null +++ b/common_util/common_util_pum_edition.py @@ -0,0 +1,43 @@ +"""품셈 판(`pum_edition`) — `FP-*` 참조가 **어느 판을 보고 적혔나** (명세 17장 규칙 3·4). + +`FP-*` 는 절 번호라 **판 안에서만 유일**하다. 판이 바뀌면 같은 번호가 다른 공종일 수 있다. + + 공종 마스터 `effective_date` = 그 판 + 자원 축 줄 · 인계 줄 `pum_edition` 칸 — 그 줄이 본 판 + 코드에 박힌 절 번호 `CODE_PUM_EDITION` 한 곳 — 합산 단계·표 형태 판정·표 읽기 선언·가드 목록 + +⛔ 판이 다르면 **값을 쓰지 않는다** — 줄은 「확인 대기」로 세우고, 코드에 박힌 선언은 조립을 멈춘다. + 조용히 이웃 절에 붙으면 품이 통째로 달라진다(규칙 4). 판을 넘어 잇는 것은 대응표 몫이다. +⚠ 라이브러리 항목의 `pum_edition` 은 랩탑 서브가 붙였다(구조물도 양식). +""" + +from __future__ import annotations + +#: 코드 안에 박힌 `FP-*` 가 본 판 — 산림사업 표준품셈(산림청고시 제2025-82호, 2026-01-01 시행). +#: ⚠ 판을 올리는 날 이 값만 바꾸지 말 것 — 박힌 선언을 대응표로 옮긴 **뒤에** 바꿀 것. +CODE_PUM_EDITION = "2026-01-01" + + +class PumEditionError(RuntimeError): + """코드에 박힌 절 번호의 판과 공종 마스터의 판이 다를 때. 값을 쓰지 않고 멈춘다.""" + + +def require_code_edition(master_edition: str, where: str) -> None: + """코드에 박힌 선언을 쓰기 전에 부른다 — 판이 다르면 멈춘다.""" + if str(master_edition) != CODE_PUM_EDITION: + raise PumEditionError( + f"{where}: 코드에 박힌 절 번호는 {CODE_PUM_EDITION} 판을 보고 적혔는데 공종 마스터는 " + f"{master_edition or '(판 없음)'} 판입니다 — 판 대응표로 옮기기 전에는 쓰지 않습니다 " + "(명세 17장 규칙 4)" + ) + + +def edition_waiting_reason(row_edition: str | None, master_edition: str) -> str: + """판이 다른 줄의 「확인 대기」 사유. 어느 한쪽이 판을 안 적었거나 같으면 빈 글.""" + if not row_edition or not master_edition or str(row_edition) == str(master_edition): + return "" + return ( + f"품셈 판 확인 대기 — 이 줄의 공종 코드는 {row_edition} 판," + f" 공종 마스터는 {master_edition} 판 · 절 번호가 같아도" + " 같은 공종이라는 보장이 없어 값을 안 씀(명세 17장)" + ) diff --git a/config/config_system_design.py b/config/config_system_design.py index 7f6d6fa6b..3342217f7 100644 --- a/config/config_system_design.py +++ b/config/config_system_design.py @@ -422,6 +422,16 @@ EARTHWORK_HAUL_EQUIPMENT_LIMITS_M = ( ("dozer", 60.0), ("dump_truck", None), ) +# 경계값 근거 — **화면이 기본값 옆에 그대로 보인다**(2026-09-13 브레인 판정 · PLAN 10장). +# ⚠ 성격이 다르다: 종무대 20 m 는 품셈 규정이라 칸을 안 두고, 도쟈 60 m 는 설계 조건이라 +# 프로젝트 설정 `dozer_haul_limit_m` 으로 바꿀 수 있다. +EARTHWORK_HAUL_LIMIT_BASIS = { + "free_haul": "품셈 1-2-7 「소운반 20 m 이내는 품에 포함」 — 규정이라 안 바꿈", + "dozer": ( + "근거 셋 일치 — 표준품셈 8-1-1(60 m 이하 도저) · 산림과임업기술 5장 p.441·478" + "(60 m 이하 도저 / 초과 덤프) · 실무 오솔길 EARTH.DAT 6개 공사지 전부 60 m" + ), +} # ───────────────────────────────────────────────────────────────────────── diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index c3da42947..6c3bf1991 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -70,6 +70,14 @@ "master_name": "노체 > 노체포설", "note": "실무 내역 셋을 다 봤더니 「노체포설·노체다짐」으로 가른 줄이 **어디에도 없다**(2026-09-08 데스크탑 보조). 영월은 「암성토 BACK-HOE(0.7㎥)」 한 줄이고 그 단가산출 안에 적사→성토→다짐 3단계가 들어가며, 울진은 「유용성토」·「사토 및 다짐공」, 봉화는 성토사면다짐만 따로다. ⇒ **성토 본체 한 줄(포설) + 성토면다짐(㎡) 따로**가 실무 양식이라 부모(FP-09-16)에서 자식 포설로 내렸다. 다짐은 그 단가 안 단계로 둔다." }, + { + "group": "노체다짐", + "work_item_code": "FP-09-16-02", + "basis_unit": "㎥", + "basis_source": "산림사업 표준품셈(고시 2025-82) 원문 L5336 「Q=1000×V×W×E×D×f/N= ㎥/시간」 — 포설(9-16-1)과 같은 체적 축이라 성토량을 그대로 밑수로 씀.", + "master_name": "노체 > 노체다짐", + "note": "2026-09-13 브레인 판정 — 별도 줄 · 산출 조건 칸으로 켜고 끔 · 기본 꺼짐([주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우」 조건부). [주]③ 「굴착기와 진동롤러 조합」은 작업 방식 설명 — 굴착기는 포설(9-16-1) 줄, 롤러는 이 줄(B09 B-FP-09-16-02 롤러만). 갈래 없음(2026-09-14 「#조합」 철회 — X-<기계>#조합 잡재료 16% 층과 다른 뜻)." + }, { "group": "성토면다짐", "work_item_code": "FP-09-17-01", diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 85381040c..b4ba00c6c 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "dataset_id": "data_work_item_master_manifest", - "generated_at": "2026-09-13T18:54:39+09:00", + "generated_at": "2026-09-13T23:03:27+09:00", "built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py", "source": { "dataset_id": "pum_forest", @@ -12,18 +12,18 @@ "files": [ { "file": "work_item_master_2026-01-01.json", - "sha256": "11c49ecde0a611307a82fd98b3625ee6577c01c2f77db6e1f515b41bf83ce685", - "size_bytes": 861050 + "sha256": "1463ea14ec2d3d45ec18b3a7e4810333de8e5fc40aaeb0dd329c68f85c863e33", + "size_bytes": 835659 }, { "file": "form_undetermined_2026-01-01.json", - "sha256": "7334dab9385bc1615a9cdb557ba814482e9a63d83b9e1232946918bfd8b5577f", - "size_bytes": 37139 + "sha256": "c835699596bf57ffbda9f72a773a72116ebe48c2a2f79ae232ba5784ec9842c7", + "size_bytes": 6447 }, { "file": "basis_missing_2026-01-01.json", - "sha256": "cb97edff0f47183173e23549c77f98f0509441edecc291bfcabba23254e60e9d", - "size_bytes": 18770 + "sha256": "f850742268dcfb1bcc878e8b062ef44bc6a11d773074163ebb8ba1dec21a6d0d", + "size_bytes": 16790 } ] } \ No newline at end of file diff --git a/resources/data_work_item_master/basis_missing_2026-01-01.json b/resources/data_work_item_master/basis_missing_2026-01-01.json index 9f31bba84..1d66cb0e1 100644 --- a/resources/data_work_item_master/basis_missing_2026-01-01.json +++ b/resources/data_work_item_master/basis_missing_2026-01-01.json @@ -130,24 +130,6 @@ "pum_form": "requirement", "line": 2062 }, - { - "pum_table_id": "F0092", - "section": "5-1-1. 관목굴취", - "pum_form": "requirement", - "line": 2081 - }, - { - "pum_table_id": "F0093", - "section": "5-1-2. 교목굴취(나무높이)", - "pum_form": "requirement", - "line": 2099 - }, - { - "pum_table_id": "F0094", - "section": "5-1-3. 교목굴취(근원직경)", - "pum_form": "requirement", - "line": 2125 - }, { "pum_table_id": "F0095", "section": "5-1-3. 교목굴취(근원직경)", @@ -160,108 +142,18 @@ "pum_form": "requirement", "line": 2177 }, - { - "pum_table_id": "F0098", - "section": "5-2. 뿌리돌림", - "pum_form": "requirement", - "line": 2194 - }, - { - "pum_table_id": "F0099", - "section": "5-3-1. 나무식재", - "pum_form": "requirement", - "line": 2219 - }, - { - "pum_table_id": "F0102", - "section": "5-3-2. 관목식재(단식)", - "pum_form": "requirement", - "line": 2254 - }, - { - "pum_table_id": "F0103", - "section": "5-3-3. 관목식재(군식)", - "pum_form": "requirement", - "line": 2272 - }, - { - "pum_table_id": "F0104", - "section": "5-3-4. 교목식재(나무높이)", - "pum_form": "requirement", - "line": 2291 - }, - { - "pum_table_id": "F0106", - "section": "5-3-5. 교목식재(흉고직경)", - "pum_form": "requirement", - "line": 2322 - }, { "pum_table_id": "F0108", "section": "5-3-5. 교목식재(흉고직경)", "pum_form": "requirement", "line": 2355 }, - { - "pum_table_id": "F0109", - "section": "5-4. 파종조림", - "pum_form": "requirement", - "line": 2366 - }, - { - "pum_table_id": "F0110", - "section": "5-5. 천연하종갱신", - "pum_form": "requirement", - "line": 2383 - }, - { - "pum_table_id": "F0111", - "section": "5-6. 움싹갱신", - "pum_form": "requirement", - "line": 2396 - }, - { - "pum_table_id": "F0112", - "section": "5-7. 생태보완조림", - "pum_form": "requirement", - "line": 2409 - }, - { - "pum_table_id": "F0113", - "section": "5-8. 큰나무 공익조림", - "pum_form": "requirement", - "line": 2426 - }, - { - "pum_table_id": "F0114", - "section": "5-9. 해안조림", - "pum_form": "requirement", - "line": 2439 - }, - { - "pum_table_id": "F0116", - "section": "5-11. 사초심기", - "pum_form": "requirement", - "line": 2475 - }, { "pum_table_id": "F0117", "section": "5-12. 떼붙임(재배잔디)", "pum_form": "requirement", "line": 2496 }, - { - "pum_table_id": "F0118", - "section": "5-13. 떼심기", - "pum_form": "requirement", - "line": 2511 - }, - { - "pum_table_id": "F0121", - "section": "5-16-1. 단끊기", - "pum_form": "requirement", - "line": 2565 - }, { "pum_table_id": "F0126", "section": "5-19-1. 표토절취 및 모으기", @@ -364,24 +256,6 @@ "pum_form": "requirement", "line": 3244 }, - { - "pum_table_id": "F0164", - "section": "6-6. 가지치기 및 수형교정", - "pum_form": "requirement", - "line": 3276 - }, - { - "pum_table_id": "F0165", - "section": "6-7-1. 교목 시비", - "pum_form": "requirement", - "line": 3304 - }, - { - "pum_table_id": "F0166", - "section": "6-7-2. 관목 시비", - "pum_form": "requirement", - "line": 3318 - }, { "pum_table_id": "F0170", "section": "7-1-1. 수확", @@ -478,6 +352,18 @@ "pum_form": "requirement", "line": 3673 }, + { + "pum_table_id": "F0189", + "section": "7-11. 동력상하차기(우드그래플) 집재-수확", + "pum_form": "productivity", + "line": 3723 + }, + { + "pum_table_id": "F0190", + "section": "7-11. 동력상하차기(우드그래플) 집재-수확", + "pum_form": "productivity", + "line": 3731 + }, { "pum_table_id": "F0191", "section": "7-11. 동력상하차기(우드그래플) 집재-수확", @@ -491,10 +377,34 @@ "line": 3763 }, { - "pum_table_id": "F0201", - "section": "8-1-1. 약제주입기", - "pum_form": "requirement", - "line": 3945 + "pum_table_id": "F0196", + "section": "7-15-1. 수확", + "pum_form": "productivity", + "line": 3864 + }, + { + "pum_table_id": "F0197", + "section": "7-15-2 숲가꾸기, 병해충방제", + "pum_form": "productivity", + "line": 3878 + }, + { + "pum_table_id": "F0198", + "section": "7-16. 소형 포워더 운재", + "pum_form": "productivity", + "line": 3898 + }, + { + "pum_table_id": "F0199", + "section": "7-17. 소형트럭 운재", + "pum_form": "productivity", + "line": 3912 + }, + { + "pum_table_id": "F0200", + "section": "7-18. 지조운반(포워더 활용)", + "pum_form": "productivity", + "line": 3926 }, { "pum_table_id": "F0202", @@ -574,6 +484,12 @@ "pum_form": "requirement", "line": 4320 }, + { + "pum_table_id": "F0220", + "section": "8-3. 소각, 매몰, 훈증, 박피", + "pum_form": "productivity", + "line": 4348 + }, { "pum_table_id": "F0224", "section": "8-5. 페르몬 유인트랩", @@ -778,18 +694,6 @@ "pum_form": "requirement", "line": 7087 }, - { - "pum_table_id": "F0412", - "section": "13-4-4. 찰쌓기(인력)", - "pum_form": "requirement", - "line": 7174 - }, - { - "pum_table_id": "F0413", - "section": "13-4-4. 찰쌓기(인력)", - "pum_form": "requirement", - "line": 7185 - }, { "pum_table_id": "F0424", "section": "13-8. 막돌쌓기", diff --git a/resources/data_work_item_master/form_undetermined_2026-01-01.json b/resources/data_work_item_master/form_undetermined_2026-01-01.json index 9724f9098..33c46bed8 100644 --- a/resources/data_work_item_master/form_undetermined_2026-01-01.json +++ b/resources/data_work_item_master/form_undetermined_2026-01-01.json @@ -4,251 +4,6 @@ "effective_date": "2026-01-01", "note": "형태를 못 정한 표. 사람이 보고 productivity/requirement/coefficient 로 확정할 것.", "items": [ - { - "pum_table_id": "F0044", - "section": "2-1-1. 휘발유․오일", - "headers": [ - "기계명(주재료)", - "주연료 (ℓ/일,대)", - "잡품 (주연료비의 %)" - ], - "first_rows": [ - [ - "아키아윈치(휘발유)", - "6.5", - "30" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0045", - "section": "2-1-1. 휘발유․오일", - "headers": [ - "기계명(주재료)", - "주연료 (ℓ/일,대)", - "잡품 (주연료비의 %)" - ], - "first_rows": [ - [ - "2드럼 케이블윈치(휘발유)", - "9.8", - "30" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0047", - "section": "2-1-2. 경유", - "headers": [ - "기계명", - "주연료 (ℓ/일,대)", - "잡품 (주연료비의 %)", - "적용기준" - ], - "first_rows": [ - [ - "트랙터 부착 기계", - "26.0", - "40", - "" - ], - [ - "굴삭기 부착 기계", - "20.8", - "30", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0048", - "section": "2-1-3. 우드그래플 소모품", - "headers": [ - "품 명", - "소요비용(원)" - ], - "first_rows": [ - [ - "트랙 접지력 보강", - "800,000" - ], - [ - "블레이드 및 실린더 교체", - "3,000,000" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0054", - "section": "2-1-5. 약제(농약) 및 친환경비닐랩", - "headers": [ - "주두부직경(㎝)", - "2㎝미만", - "2~6㎝미만", - "6~8㎝미만", - "8㎝이상" - ], - "first_rows": [ - [ - "소금처리량(g)", - "20", - "40", - "60", - "80" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0063", - "section": "2-1-13. 파쇄", - "headers": [ - "소모품", - "소모율", - "가격(원)", - "기타" - ], - "first_rows": [ - [ - "메인파쇄기날", - "0.00125개/hr", - "-", - "" - ], - [ - "분쇄기날", - "0.005개/hr", - "-", - "42개 사용" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0100", - "section": "5-3-1. 나무식재", - "headers": [ - "수종별", - "소 묘", - "중 묘", - "대 묘" - ], - "first_rows": [ - [ - "소나무", - "1-1(노)", - "2-0(용)", - "1-1-2(노), 2-2(용)" - ], - [ - "낙엽송", - "", - "1-1, 2-0(용)", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0101", - "section": "5-3-1. 나무식재", - "headers": [ - "구분", - "침엽수", - "활엽수" - ], - "first_rows": [ - [ - "소묘", - "간장 20cm미만", - "간장 30cm미만" - ], - [ - "중묘", - "간장 20cm이상 40cm미만 또는 간장 40cm이상이고 근원경 8mm미만", - "간장 30cm이상 60cm미만 또는 간장 60cm이상이고 근원경 11mm미만" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0105", - "section": "5-3-4. 교목식재(나무높이)", - "headers": [ - "인력시공시", - "기계시공시" - ], - "first_rows": [ - [ - "인력품의 10%", - "인력품의 20%" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0107", - "section": "5-3-5. 교목식재(흉고직경)", - "headers": [ - "인력시공시", - "기계시공시" - ], - "first_rows": [ - [ - "인력품의 10%", - "인력품의 20%" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0137", - "section": "5-24-2. 뿜어붙이기-기계/마사토", - "headers": [ - "복원목표", - "식 생 구 분", - "종자배합비율(%)" - ], - "first_rows": [ - [ - "초본 위주형", - "관목류", - "20~40" - ], - [ - "초본, 야생화류", - "40~80", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0138", - "section": "5-24-2. 뿜어붙이기-기계/마사토", - "headers": [ - "복원목표", - "식 생 구 분", - "종자배합비율(%)" - ], - "first_rows": [ - [ - "초본 위주형", - "관목류", - "10∼40" - ], - [ - "초본, 야생화류", - "40∼80", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0140", "section": "5-26-1. 막갈이", @@ -326,28 +81,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0146", - "section": "5-29. 복사이식", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "K(버킷계수)", - "0.9", - "" - ], - [ - "f(체적환산계수)", - "1/1.3", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0169", "section": "7-1-1. 수확", @@ -373,116 +106,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0187", - "section": "7-9-2. 숲가꾸기, 소나무재선충병방제", - "headers": [ - "노선별 집재재적(㎥/ha)", - "집재거리" - ], - "first_rows": [ - [ - "100m 이하", - "150m 이하", - "200m 이하", - "250m 이하", - "" - ], - [ - "0~20㎥", - "17", - "19", - "23", - "27" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0189", - "section": "7-11. 동력상하차기(우드그래플) 집재-수확", - "headers": [ - "집 재 재 적 (㎥/ha)", - "최대집재거리 : 40m 이하", - "소요 인력" - ], - "first_rows": [ - [ - "원목의 길이", - "", - "", - "", - "", - "", - "" - ], - [ - "1.2m", - "1.8m", - "2.1m", - "2.7m", - "3.6m", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0190", - "section": "7-11. 동력상하차기(우드그래플) 집재-수확", - "headers": [ - "집 재 재 적 (㎥/ha)", - "최대집재거리 : 80m 이하", - "소요 인력" - ], - "first_rows": [ - [ - "원목의 길이", - "", - "", - "", - "", - "", - "" - ], - [ - "1.2m", - "1.8m", - "2.1m", - "2.7m", - "3.6m", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0194", - "section": "7-13. 검척", - "headers": [ - "재장", - "말구직경", - "낙엽송(개수)", - "경급×개수" - ], - "first_rows": [ - [ - "2.1m", - "6", - "2", - "12" - ], - [ - "8", - "11", - "88", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0195", "section": "7-14. 원목 운반-수확", @@ -514,471 +137,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0196", - "section": "7-15-1. 수확", - "headers": [ - "구분", - "주행거리(m이하)", - "인력구분" - ], - "first_rows": [ - [ - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "1000", - "1200", - "", - "" - ], - [ - "회수", - "10.4", - "9.97", - "9.51", - "9.06", - "8.60", - "8.14", - "7.69", - "7.22", - "6.77", - "6.31", - "5.85", - "건설기계운전기사1명" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0197", - "section": "7-15-2 숲가꾸기, 병해충방제", - "headers": [ - "구 분", - "운반거리(㎞ 이하)", - "인력구분" - ], - "first_rows": [ - [ - "0.5", - "1.0", - "1.5", - "2.0", - "2.5", - "3.0", - "3.5", - "4.0", - "4.5", - "5.0", - "", - "" - ], - [ - "작업회수", - "8.6", - "6.3", - "5.0", - "4.1", - "3.5", - "3.1", - "2.7", - "2.4", - "2.2", - "2.0", - "건설기계운전기사 1인" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0198", - "section": "7-16. 소형 포워더 운재", - "headers": [ - "구분", - "주행거리(m이하)", - "인력구분" - ], - "first_rows": [ - [ - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "1000", - "1200", - "", - "" - ], - [ - "회수", - "22.97", - "19.20", - "16.50", - "14.46", - "12.87", - "11.60", - "10.55", - "9.68", - "8.94", - "8.31", - "7.28", - "건설기계운전기사 1명" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0199", - "section": "7-17. 소형트럭 운재", - "headers": [ - "구분", - "운반거리(m이하)", - "인력구분" - ], - "first_rows": [ - [ - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "1000", - "", - "" - ], - [ - "회수", - "23.8", - "20.5", - "18.0", - "16.1", - "14.5", - "13.3", - "12.2", - "11.3", - "10.5", - "9.8", - "건설기계 운전기사 1명" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0200", - "section": "7-18. 지조운반(포워더 활용)", - "headers": [ - "구 분", - "운반거리(m이하)", - "인력구분" - ], - "first_rows": [ - [ - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - "1000", - "", - "" - ], - [ - "회 수", - "22.9", - "17.1", - "13.7", - "11.4", - "9.8", - "8.6", - "7.6", - "6.9", - "6.2", - "5.7", - "건설기계 운전기사 1명" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0205", - "section": "8-2-1. 소나무재선충병", - "headers": [ - "구 분", - "제 형" - ], - "first_rows": [ - [ - "아바멕틴", - "유제 1.8%, 분산성액제 1.8%, 미탁제 1.8%" - ], - [ - "에마멕틴벤조에이트", - "유제 2.15%, 액제 2%, 미탁제 2.15%" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0208", - "section": "8-2-2. 솔잎혹파리", - "headers": [ - "선정약제", - "원액 주입량", - "비고" - ], - "first_rows": [ - [ - "티아메톡삼 분산성액제 15%", - "0.2㎖/㎝", - "" - ], - [ - "디노테퓨란(15)․에마멕틴벤조에이트(4.5) 분산성액제 19.5%", - "0.2㎖/㎝", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0212", - "section": "8-2-3. 솔껍질깍지벌레", - "headers": [ - "선정약제", - "원액 주입량", - "비고" - ], - "first_rows": [ - [ - "디노테퓨란(15)․에마멕틴벤조에이트(4.5) 분산성액제 19.5%", - "0.5㎖/㎝", - "" - ], - [ - "이미다클로프리드 분산성액제 20%", - "0.6㎖/㎝", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0216", - "section": "8-2-4. 솔나방", - "headers": [ - "선정약제", - "원액 주입량", - "비고" - ], - "first_rows": [ - [ - "아바멕틴(1.8)·설폭사플로르(4.2) 분산성액제 6%", - "0.4㎖/㎝", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0218", - "section": "8-2-5. 푸사리움가지마름병", - "headers": [ - "선정약제", - "원액 주입량", - "비고" - ], - "first_rows": [ - [ - "테부코나졸 유탁제 25%", - "0.5㎖/㎝", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0220", - "section": "8-3. 소각, 매몰, 훈증, 박피", - "headers": [ - "경급별 (㎝)", - "벌목조재 (㎥/인)", - "소운반 (㎥/인)", - "무더기 훈증 (RM/인)", - "그루터기 훈증 (본/인)", - "소각 (㎥/인)", - "매몰 (㎥/인)", - "그루터기 박피 (본/인)", - "원목 박피 (본/인)" - ], - "first_rows": [ - [ - "6", - "1.13", - "5.41", - "14.40", - "169.5", - "7.22", - "1.50", - "100.0", - "19.5" - ], - [ - "8", - "1.18", - "5.50", - "14.73", - "151.5", - "7.39", - "1.50", - "100.0", - "19.5" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0221", - "section": "8-3. 소각, 매몰, 훈증, 박피", - "headers": [ - "수종 가슴 높이 지름", - "소나무", - "낙엽송", - "참나무", - "산오리 나무", - "들오리 나무", - "이태리 포플러", - "아까시 나무" - ], - "first_rows": [ - [ - "4cm", - "", - "32", - "", - "27", - "37", - "35", - "24" - ], - [ - "6", - "", - "28", - "", - "24", - "42", - "29", - "23" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0222", - "section": "8-3. 소각, 매몰, 훈증, 박피", - "headers": [ - "기 준", - "적용대상 수종", - "비율(수간재적의 %)", - "비 고" - ], - "first_rows": [ - [ - "중부지방소나무", - "소나무, 곰솔, 잣나무", - "25", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0227", - "section": "8-6-1. 유인헬기방제", - "headers": [ - "구 분", - "물탱크 용량", - "희석배수", - "유효 살포량/1회", - "원액량" - ], - "first_rows": [ - [ - "소형헬기 (AS350)", - "800", - "8배", - "500", - "62.5" - ], - [ - "16배", - "31.3", - "", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0231", - "section": "8-6-3. 지상방제", - "headers": [ - "물의양 희석 배수", - "10ℓ (0.5말)", - "20ℓ (1말)", - "50ℓ (2.5말)", - "100ℓ (5말)", - "200ℓ (10말)", - "400ℓ (20말)", - "500ℓ (25말)", - "600ℓ (30말)" - ], - "first_rows": [ - [ - "250배", - "40.0", - "80.0", - "200.0", - "400.0", - "800.0", - "1,600.0", - "2,000.0", - "2,400.0" - ], - [ - "500배", - "20.0", - "40.0", - "100.0", - "200.0", - "400.0", - "800.0", - "1,000.0", - "1,200.0" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0248", "section": "8-2-13 대형브레이커(2025)」, 「건설공사 표준품셈 공통부문, 1-2-3 토질. 3. 체적환산계수적용(2025)」 참조", @@ -998,186 +156,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0281", - "section": "9-15-1. 답(畓)구간", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "K(버킷계수)", - "0.9", - "" - ], - [ - "f(토량환산계수)", - "1/1.30", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0282", - "section": "9-15-2. 답(畓)외구간", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "T(표토두께)", - "0.2m", - "" - ], - [ - "L(운반거리)", - "20m", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0284", - "section": "9-16-2. 노체다짐", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "V(다짐속도,km/hr)", - "4", - "" - ], - [ - "W(롤러 유효폭,m)", - "1.9", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0285", - "section": "9-16-3. 살수", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "흡입준비(t1)", - "5분", - "" - ], - [ - "운반(t2)", - "15 km/hr", - "L/V×2×60" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0287", - "section": "9-18. 층따기", - "headers": [ - "적용장비", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "굴착기 (무한궤도, 0.7㎥)", - "K", - "0.9", - "" - ], - [ - "f", - "1/1.3", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0295", - "section": "9-22. 섞기", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "K(버킷계수)", - "0.9", - "" - ], - [ - "f(토량환산계수)", - "1/1.25", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0300", - "section": "10-3-3. 혼합골재", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "운반비", - "골재원 → 현장(덤프 15ton)", - "" - ], - [ - "하차비", - "1회", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0301", - "section": "10-4. 중기운반", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "운반", - "트레일러운반 (20TON)", - "t1=20min", - "" - ], - [ - "t2=운반시간 참조", - "", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0304", "section": "10-6-3. 기타 임업자재", @@ -1226,156 +204,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0308", - "section": "10-7-4. 모노레일 운반", - "headers": [ - "구 분", - "콘크리트", - "토사ㆍ석재", - "블록ㆍ제 자재 등" - ], - "first_rows": [ - [ - "차량구분", - "바스켓 차량", - "보통차량", - "" - ], - [ - "단궤도", - "0.3㎥", - "0.3㎥", - "600kg, 0.3㎥" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0309", - "section": "10-7-4. 모노레일 운반", - "headers": [ - "구 분", - "콘크리트", - "토사ㆍ석재 등", - "블록ㆍ제 자재 등" - ], - "first_rows": [ - [ - "시간(분)", - "4.0", - "4.0", - "6.0" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0316", - "section": "10-9-1. 자재의 1회당 표준 운반량", - "headers": [ - "규격 구분", - "1t미만", - "1t이상 2t미만", - "2t이상 3t미만", - "3t이상 4t미만", - "4t이상 5t미만", - "비고" - ], - "first_rows": [ - [ - "콘크리트 블 록", - "350kg(10.9개)", - "530kg(16.8개)", - "720kg(22.8개)", - "910kg(28.7개)", - "1100kg(34.7개)", - "" - ], - [ - "목제형틀", - "170kg(14㎡)", - "220kg(18㎡)", - "270kg(22㎡)", - "320kg(26㎡)", - "370kg(30㎡)", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0319", - "section": "10-11. 불도저 운반", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "L", - "20m", - "", - "" - ], - [ - "E", - "토사", - "0.55", - "자연상태,불량" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0320", - "section": "10-12-1. 토사", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "K", - "0.9(0.55)", - "동 일", - "" - ], - [ - "E0", - "토사", - "0.60(불량)", - "임 도" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0321", - "section": "10-12-1. 토사", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "적재(t1)", - "굴착기", - "적재방법에 따라 산출", - "" - ], - [ - "t2", - "V1(적재)", - "5 km/hr", - "왕복시간" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0322", "section": "10-13-1. 드론운반", @@ -1426,54 +254,6 @@ ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" }, - { - "pum_table_id": "F0324", - "section": "10-13-2. 인력운반", - "headers": [ - "항 목", - "내 용", - "적용치" - ], - "first_rows": [ - [ - "① 드론준비시간(분)", - "ㆍ이륙지점에 도착한 후 묘목운반 개시까지 준비시간 (점검, 보정, 테스트 비행을 포함)", - "60" - ], - [ - "② 왕복비행시간(분)", - "ㆍ운반거리에 대한 드론의 왕복 시간 (적재, 하적, 배터리 교환을 포함)", - "0.0098×수평거리+1.3264" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0328", - "section": "11-4. 쇄석·혼합석 부설", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "부설장비", - "유압식백호우 (무한궤도 0.7㎥)", - "q", - "0.7", - "" - ], - [ - "k", - "0.55", - "", - "", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, { "pum_table_id": "F0333", "section": "12-1-3. 인력비빔타설", @@ -1586,385 +366,6 @@ ] ], "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0358", - "section": "12-2 표면 마무리를 따른다.", - "headers": [ - "구 분", - "적용 기준" - ], - "first_rows": [ - [ - "Type-Ⅰ", - "매트기초 등 펌프차 작업에 제약이 없는 시설물" - ], - [ - "Type-Ⅱ", - "벽, 기둥, 보, 슬래브. 교대, 교각 등 펌프차 작업에 큰 지장이 없어 일반적인 시공이 가능한 시설물" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0360", - "section": "12-2 표면 마무리를 따른다.", - "headers": [ - "구 분", - "적용 기준" - ], - "first_rows": [ - [ - "Type-Ⅰ", - "대기 공간이 충분히 넓어 믹서트럭 2대가 병렬로 타설 준비가 가능하며 지속적인 타설을 수행하는 경우" - ], - [ - "Type-Ⅱ", - "믹서트럭이 1대씩 직렬로 대기하며 순차적으로 타설 준비하여 타설하는 일반적인 경우" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0388", - "section": "12-34-4. 채움재(T=20m/m)", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "재료비", - "JOINT FILLER", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0390", - "section": "12-36. P.C BOX 설치", - "headers": [ - "구 분", - "적 용", - "비 고" - ], - "first_rows": [ - [ - "제작비", - "견적처리", - "" - ], - [ - "운송비", - "견적처리", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0392", - "section": "12-38-1. 사용횟수", - "headers": [ - "구 분", - "사용 조작 회수" - ], - "first_rows": [ - [ - "패 널 류 보, 드롭헤드, 강관파이프, 훅  클래프, 웨지핀", - "12회 사용 잔존율 25% 25회 사용 잔존율 10%" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0394", - "section": "12-38-2. 사용수량", - "headers": [ - "구 분", - "간 단", - "보 통", - "복 잡" - ], - "first_rows": [ - [ - "요 율", - "24%", - "52%", - "79%" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0396", - "section": "12-38-3. 설치 및 해체", - "headers": [ - "구 분", - "유 형" - ], - "first_rows": [ - [ - "복 잡", - "토목 : 교대, 날개벽 등 복잡하고 보강이 많은 구조 건축 : 외부 벽체, 보/기둥" - ], - [ - "보 통", - "측구, 수로, 옹벽, 일반적인 벽체, 박스 등" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0400", - "section": "13-2-3. 큰돌 채집", - "headers": [ - "명칭", - "단위", - "규격(직경)" - ], - "first_rows": [ - [ - "40㎝이상∼60㎝미만", - "60㎝이상∼80㎝미만", - "80㎝이상∼100㎝이하", - "", - "" - ], - [ - "뒷길이", - "㎝", - "60 ~ 75", - "75 ~ 95", - "95 ~ 120" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0406", - "section": "13-3. 기초다짐 및 뒤채움’ 항을 적용한다.", - "headers": [ - "종별 뒷길이 단위", - "견치돌", - "깬돌 및 깬잡석", - "야면석" - ], - "first_rows": [ - [ - "25㎝(17×17)", - "개 ㎏", - "32 192", - "33 132", - "- -" - ], - [ - "30㎝(20×20)", - "개 ㎏", - "23 368", - "24 264", - "28 420" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0408", - "section": "13-4-3. 고임돌 소요량", - "headers": [ - "뒷길이 종별", - "25㎝", - "30㎝", - "35㎝", - "45㎝", - "55㎝", - "60㎝", - "75㎝" - ], - "first_rows": [ - [ - "야면석(㎥) 깬잡석(㎥) 깬 돌(㎥) 견치돌(㎥)", - "0.06 0.09 - -", - "0.07 0.11 0.10 -", - "0.09 0.13 0.12 0.12", - "0.11 0.16 0.15 0.15", - "0.14 0.19 0.18 0.18", - "0.15 0.21 0.20 0.20", - "- 0.26 0.25 0.25" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0410", - "section": "13-4-4. 찰쌓기(인력)", - "headers": [ - "뒷길이 종별", - "25㎝", - "30㎝", - "35㎝", - "45㎝", - "55㎝", - "60㎝", - "75㎝", - "비고" - ], - "first_rows": [ - [ - "야면석(㎥) 호박돌(㎥)", - "0.08 0.08", - "0.10 0.10", - "0.12 0.12", - "0.15 0.15", - "0.18 0.18", - "0.20 0.20", - "0.25 0.25", - "뒷길이 33.3% 〃" - ], - [ - "깬잡석(㎥) 깬 돌(㎥) 견치돌(㎥)", - "0.11 0.11 0.11", - "0.14 0.14 0.14", - "0.16 0.16 0.16", - "0.20 0.20 0.20", - "0.25 0.25 0.25", - "0.27 0.27 0.27", - "0.34 0.34 0.34", - "뒷길이 45% 〃 〃" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0411", - "section": "13-4-4. 찰쌓기(인력)", - "headers": [ - "직 고(直高)", - "∼1.5m", - "∼3.0m", - "∼5.0m", - "∼7.0m" - ], - "first_rows": [ - [ - "상부의 두께(㎝) 하부의 두께(㎝)", - "20~40 30~60", - "20~40 45~75", - "20~40 60~100", - "20~40 80~140" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0429", - "section": "13-10-2. 나무 말뚝박기", - "headers": [ - "관입률 =", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "0.8", - "비고" - ], - "first_rows": [ - [ - "계수", - "0.2", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0439", - "section": "13-12-2. 지오셀(사면보강)", - "headers": [ - "구 분 공정별", - "1:2 표준", - "1:2 ~ 1:1.5", - "1:1.5 ~ 1:1", - "1:1 이상" - ], - "first_rows": [ - [ - "4m 표준", - "1", - "10", - "15", - "30" - ], - [ - "4 ~ 10m", - "10", - "20", - "25", - "40" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0446", - "section": "13-15-2. 목책 설치", - "headers": [ - "작업조건", - "벌도목 직경", - "가지량", - "경사도" - ], - "first_rows": [ - [ - "어려운 조건", - "26 ~ 30cm", - "많음", - "15°이상" - ], - [ - "쉬운 조건", - "20 ~ 24cm", - "중간 이하", - "15°미만" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" - }, - { - "pum_table_id": "F0453", - "section": "8-2-3 굴착기(2025)」를 참조하여 적용계수를 달리 적용하도록 한다.", - "headers": [ - "사업종", - "구분", - "적용 가능 공종" - ], - "first_rows": [ - [ - "공종", - "", - "", - "", - "", - "" - ], - [ - "조림", - "시행 지침", - "3-5 예정지 정리작업", - "5-3-1 나무식재", - "5-30 표시봉설치", - "5-31 지주목설치" - ] - ], - "reason": "헤더·첫 행에 단위·밑수·직종 표지 없음" } ] } \ No newline at end of file diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index e5c9082c1..880bf1bc6 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_2026-01-01.json @@ -2,7 +2,8 @@ "schema_version": "1.0", "dataset_id": "work_item_master_forest", "effective_date": "2026-01-01", - "generated_at": "2026-09-13T18:54:39+09:00", + "pum_edition": "2026-01-01", + "generated_at": "2026-09-13T23:03:27+09:00", "dataset_version": { "dataset_id": "pum_forest", "effective_date": "2026-01-01", @@ -20,10 +21,10 @@ "tables_total": 475, "tables_attached": 456, "tables_orphan": 19, - "form_undetermined": 77, - "basis_found": 182, - "basis_missing": 138, - "basis_grouped": 37 + "form_undetermined": 13, + "basis_found": 204, + "basis_missing": 122, + "basis_grouped": 48 }, "orphan_tables": [ { @@ -112,7 +113,8 @@ "parent_code": null, "sort_order": 256, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-01", @@ -122,7 +124,8 @@ "parent_code": "FP-01", "sort_order": 512, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-01-01", @@ -131,7 +134,8 @@ "level": 3, "parent_code": "FP-01-01", "sort_order": 768, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-01-02", @@ -158,14 +162,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "2.5톤 차량", - "소 묘", - "중 묘", - "대 묘", - "소 2-2 (용기묘)", - "※ 묘목 1,000본당 무게 - 소나무 노지묘 1-1 (45㎏) - 낙엽송, 편백 용기묘 2-0 (200㎏) - 상수리 용기묘 1-0 (200㎏)" - ], + "variant_key": [], "condition_note": [ "규 격", "수종 및 묘령(형태)", @@ -217,7 +214,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-01-03", @@ -226,7 +224,8 @@ "level": 3, "parent_code": "FP-01-01", "sort_order": 1280, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-02", @@ -236,7 +235,8 @@ "parent_code": "FP-01", "sort_order": 1536, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-02-01", @@ -245,7 +245,8 @@ "level": 3, "parent_code": "FP-01-02", "sort_order": 1792, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-02-02", @@ -282,32 +283,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "단 위", - "공사연장", - "공사폭원", - "직공인부", - "공사 및 사업면적", - "용지면적", - "토 적(높이, 너비)", - "토 적(단면적)", - "토 적(체적)", - "토적(체적합계)", - "떼", - "모래,자갈", - "조약돌", - "견치돌, 깬돌", - "야 면 석(野面石)", - "cm", - "돌쌓기 및 돌붙임", - "사 석(捨石)", - "다 듬 돌(切石, 板石)", - "벽돌", - "블록", - "시멘트", - "모르타르", - "콘크리트" - ], + "variant_key": [], "condition_note": [ "종 목", "규 격", @@ -910,13 +886,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "설계서의 총액", - "설계서의 소계", - "설계서의 금액란", - "일위대가표의 계금", - "일위대가표의 금액란" - ], + "variant_key": [], "condition_note": [ "종 목", "단 위", @@ -956,7 +926,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-02-03", @@ -986,25 +957,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "경 암 ( 硬 岩 )", - "보 통 암 ( 普 通 硬 岩 )", - "연 암 ( 軟 岩 )", - "풍 화 암 ( 風 化 岩 )", - "폐 콘 크 리 트", - "호 박 돌 ( 玉 石 )", - "역 (礫 )", - "역 질 토 ( 礫 質 土 )", - "고결(固結)된 역질토(礫質土)", - "모 래 ( 砂 )", - "암괴(岩塊)나 호박돌이 섞인 모래", - "점 질 토", - "역(礫)이 섞인 점질토(粘質土)", - "암괴(岩塊)나 호박돌이 섞인 점토", - "점 토 ( 粘 土 )", - "역 이 섞 인 점 질 토", - "암 괴 ( 岩 塊 ) 나 호 박 돌 이 섞 인 점 토" - ], + "variant_key": [], "condition_note": [ "종 별", "L", @@ -1115,10 +1068,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자연상태의 체적", - "흐트러진 상태의 체적" - ], + "variant_key": [], "condition_note": [ "구하는 Q 기준이 되는 q", "자연상태의 체적", @@ -1140,7 +1090,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-02-04", @@ -1167,11 +1118,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "사용고재(시멘트공대 및 공드람 제외)", - "강재스크랩(Scrap)", - "기타발생재" - ], + "variant_key": [], "condition_note": [ "품 명", "공제율" @@ -1191,7 +1138,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-02-05", @@ -1200,7 +1148,8 @@ "level": 3, "parent_code": "FP-01-02", "sort_order": 2816, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-02-06", @@ -1209,7 +1158,8 @@ "level": 3, "parent_code": "FP-01-02", "sort_order": 3072, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-02-07", @@ -1239,32 +1189,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "6톤 차량", - "목재(원목)", - "목재(제재목)", - "경유ㆍ휘발유", - "아스팔트", - "새끼", - "벽돌", - "기와", - "보도블록", - "견치돌", - "블록", - "두께 15cm", - "두께 20cm", - "타일", - "크링커타일", - "합판 유리", - "페인트", - "아스타일", - "흄관", - "450 "", - "600 "", - "800 "", - "900 "", - "1,000 "" - ], + "variant_key": [], "condition_note": [ "종 별", "규격", @@ -1775,7 +1700,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-02-08", @@ -1784,7 +1710,8 @@ "level": 3, "parent_code": "FP-01-02", "sort_order": 3584, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-03", @@ -1794,7 +1721,8 @@ "parent_code": "FP-01", "sort_order": 3840, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-03-01", @@ -1821,14 +1749,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "시 멘 트", - "잔골재ㆍ채움재", - "굵 은 골 재", - "아 스 팔 트", - "석 분", - "혼 화 재" - ], + "variant_key": [], "condition_note": [ "종 류", "정 치 식 (%)", @@ -1884,12 +1805,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "모 래", - "부순돌ㆍ자갈ㆍ막자갈", - "석 분", - "점 질 토" - ], + "variant_key": [], "condition_note": [ "종 류", "할 증 률 (%)" @@ -1930,9 +1846,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "모 래" - ], + "variant_key": [], "condition_note": [ "종 류", "할 증 률 (%)" @@ -1961,19 +1875,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "원 형 철 근", - "이 형 철 근", - "이형철근 (교량·지하철 및 이와 유사한 복잡한 구조물의 주철근)", - "강 판", - "강관(옥외 수도용 강관 제외)", - "대형형강 (形 鋼)", - "소 형 형 강", - "봉 강 (棒 鋼)", - "평 강 대 강", - "경량형 강각(角) 파이프", - "리 벳 (제 품)" - ], + "variant_key": [], "condition_note": [ "종 류", "할 증 률 (%)" @@ -2042,25 +1944,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "목 재", - "판 재", - "합 판", - "수장용 합판", - "조립식구조물(U형플륨관 등)", - "레디믹스트콘크리트타설 (현장 플랜트 포함)", - "철근 구조물", - "철골 구조물", - "원 석 (마름돌용)", - "사방용 수목", - "떼 및 초화류", - "현장콘크리트 타설 (인력 및 믹서)", - "철근구조물", - "소형구조물", - "아스팔트콘크리트 포설(현장플랜트 포함)", - "콘크리트포장 포설", - "원심력철근콘크리트관" - ], + "variant_key": [], "condition_note": [ "재 료 별", "할 증 률 (%)" @@ -2153,7 +2037,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-03-02", @@ -2162,7 +2047,8 @@ "level": 3, "parent_code": "FP-01-03", "sort_order": 4352, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-04", @@ -2172,7 +2058,8 @@ "parent_code": "FP-01", "sort_order": 4608, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-04-01", @@ -2201,11 +2088,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "6월∼9월", - "10월∼12월", - "1∼5월" - ], + "variant_key": [], "condition_note": [ "작업시기", "할증률" @@ -2225,7 +2108,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-02", @@ -2252,11 +2136,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "3년차 이상", - "2년차", - "당해 연도(전년도 추기조림 포함)" - ], + "variant_key": [], "condition_note": [ "조림 후 경과연수", "할증률" @@ -2276,7 +2156,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-03", @@ -2305,11 +2186,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "집단화 정도", - "개소당 평균면적이 1~3ha 미만", - "개소당 평균면적이 3ha 이상" - ], + "variant_key": [], "condition_note": [ "구 분", "집단화 정도", @@ -2333,7 +2210,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-04", @@ -2362,15 +2240,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "작업구역", - "개소 평균 1~3ha 미만", - "개소 평균 3~5ha 미만", - "개소당 평균 5ha 이상", - "벌채구역 크기", - "개벌지 크기가 3,000㎡(또는 벌채폭 60m) 미만", - "개벌지 크기가 3,000㎡ 이상" - ], + "variant_key": [], "condition_note": [ "구 분", "집단화 정도", @@ -2414,7 +2284,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-05", @@ -2443,15 +2314,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "산지경사", - "중 (15~30°)", - "완 (15° 미만)", - "평균경사도", - "평균경사도 20%~50%미만", - "평균경사도 10%~20%미만", - "평균경사도 10%미만" - ], + "variant_key": [], "condition_note": [ "적용기준", "경사도", @@ -2495,7 +2358,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-06", @@ -2524,12 +2388,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "도 보", - "1.3㎞∼2.5㎞ 미만", - "1.3㎞ 미만", - "차 량" - ], + "variant_key": [], "condition_note": [ "구 분", "작업장까지 이동거리", @@ -2568,7 +2427,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-07", @@ -2595,11 +2455,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "식생을 제거하지 않고는 보행이 곤란하다", - "보행하는데 약간의 어려움이 있다", - "보행이 용이하다" - ], + "variant_key": [], "condition_note": [ "하층식생", "할증률" @@ -2619,7 +2475,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-08", @@ -2646,11 +2503,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "가슴높이 이상의 초본․관목", - "가슴높이 미만의 초본․관목", - "무릎높이 이하의 초본․관목" - ], + "variant_key": [], "condition_note": [ "장애물의 정도", "할증률" @@ -2670,7 +2523,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-09", @@ -2699,11 +2553,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "어렵다(높이 1.2m 이상이고, 직경이 4~6cm 이상)", - "보통이다(높이 1.2m 이상이고, 직경이 4~6cm 미만)", - "쉽다(높이 1.2m 미만이고, 낫으로도 제거가 용이)" - ], + "variant_key": [], "condition_note": [ "제거대상 식생", "할증률" @@ -2723,7 +2573,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-10", @@ -2752,11 +2603,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "돌 등 장애물 함량이 30% 이상이고, 나무뿌리가 밀하게 분포할 경우", - "돌 등 장애물 함량이 10~30%이고, 나무뿌리가 보통정도로 분포할 경우", - "식혈시 장애물이 거의 없음" - ], + "variant_key": [], "condition_note": [ "토양조건", "할증률" @@ -2776,7 +2623,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-11", @@ -2805,13 +2653,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "과밀(80%초과 피복)", - "밀(60~80%미만 피복)", - "보통(40~60%미만 피복)", - "소(20~40%미만 피복)", - "과소(20%미만 피복)" - ], + "variant_key": [], "condition_note": [ "덩굴 피복도", "할인․할증률" @@ -2839,7 +2681,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-12", @@ -2868,11 +2711,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "대 (임지의 71% 이상 분포)", - "중 (임지의 41%~70% 분포)", - "소 (임지의 40% 이하 분포)" - ], + "variant_key": [], "condition_note": [ "제거대상 피복도", "할인․할증률" @@ -2892,7 +2731,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-13", @@ -2919,11 +2759,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "돌, 도랑, 그루터기 등으로 주행이 매우 힘들다", - "돌, 도랑, 그루터기 등으로 주행이 다소 힘들다", - "주행에 어려움이 없다" - ], + "variant_key": [], "condition_note": [ "주행 장애물 상태", "할증률" @@ -2943,7 +2779,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-14", @@ -2972,15 +2809,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "가선을 이용한 기계장비의 원목․생산재 집재", - "상향집재", - "소나무재선충병 인력에 의한 원목 및 재해산물 수집", - "중(15~30°)", - "완(15° 미만)", - "소나무재선충병 방제 훈증더미 제거", - "수평 또는 하향집재" - ], + "variant_key": [], "condition_note": [ "구분", "집재방향", @@ -3031,7 +2860,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-15", @@ -3060,11 +2890,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "11~20m", - "6~10m", - "0~5m" - ], + "variant_key": [], "condition_note": [ "횡단운반거리", "할증률" @@ -3084,7 +2910,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-16", @@ -3111,9 +2938,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "규격재 생산을 위한 조재" - ], + "variant_key": [], "condition_note": [ "적용 조건", "할증률" @@ -3125,7 +2950,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-17", @@ -3154,17 +2980,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "산림병해충방제 (단목베기, 나무주사)", - "10~29본/ha", - "30~49본/ha", - "50본/ha 이상", - "소나무재선충병방제 (소각, 매몰, 훈증, 박피)", - "15본~25본/ha", - "25본~35본/ha", - "35본~45본/ha", - "45본/ha 이상" - ], + "variant_key": [], "condition_note": [ "구 분", "대상목 분포", @@ -3218,7 +3034,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-18", @@ -3247,13 +3064,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "산림병해충방제 (산물수집-기계장비 집재)", - "16~20cm", - "소나무재선충병방제 (그물망 피복)", - "18cm", - "20cm 이상" - ], + "variant_key": [], "condition_note": [ "구분", "평균경급", @@ -3304,9 +3115,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "매개충나무주사" - ], + "variant_key": [], "condition_note": [ "구분", "내 용", @@ -3320,7 +3129,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-19", @@ -3329,7 +3139,8 @@ "level": 3, "parent_code": "FP-01-04", "sort_order": 9472, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-04-20", @@ -3356,12 +3167,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "사면형", - "종방향 복합사면(가시거리 불량, 고소작업차 사용 필요)", - "횡방향 복합사면(가시거리 보통)", - "단순사면(가시거리 양호)" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -3390,7 +3196,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-21", @@ -3419,11 +3226,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "접근성", - "이착륙장~방재사업지까지의 이동거리 200m 이내 까지 차량접근 가능", - "이착륙장~방재사업지까지의 이동거리 100m 이내 까지 차량접근 가능" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -3447,7 +3250,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-22", @@ -3474,10 +3278,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "방제 수종", - "소나무, 해송" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -3496,7 +3297,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-23", @@ -3525,14 +3327,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "그물망 피복 시 임내 운반거리", - "101~200m", - "100m 이하", - "수라집재 시 목재 운반거리", - "6~10m", - "0~5m" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -3571,7 +3366,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-24", @@ -3598,10 +3394,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "장비의 규격 (굴삭기)", - "0.4㎥ 이상" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -3620,7 +3413,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-25", @@ -3647,13 +3441,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업 가능 시간", - "3시간 이하", - "4시간 이하", - "5시간 이하", - "6시간 이하" - ], + "variant_key": [], "condition_note": [ "구 분", "적용조건", @@ -3687,7 +3475,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-04-26", @@ -3714,10 +3503,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "1", - "2" - ], + "variant_key": [], "condition_note": [ "구분", "조 건", @@ -3738,7 +3524,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-05", @@ -3747,7 +3534,8 @@ "level": 2, "parent_code": "FP-01", "sort_order": 11520, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-06", @@ -3776,28 +3564,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "적용방법", - "순 원 가", - "간 접 재 료 비", - "소 계", - "노 무 비", - "간 접 노 무 비", - "경 비", - "산 업 재 해 보 상 보 험 료", - "고 용 보 험 료", - "국 민 건 강 보 험 료", - "국 민 연 금 보 험 료", - "노 인 장 기 요 양 보 험 료", - "산 업 안 전 보 건 관 리 비", - "기 타 경 비", - "기 타 법 정 경 비", - "일 반 관 리 비", - "이 윤", - "총 원 가", - "부 가 가 치 세", - "합 계" - ], + "variant_key": [], "condition_note": [ "구분 비목", "적용 기준" @@ -3981,7 +3748,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-07", @@ -3991,7 +3759,8 @@ "parent_code": "FP-01", "sort_order": 12032, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-01-07-01", @@ -4018,12 +3787,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "2회", - "3회", - "4회", - "6회" - ], + "variant_key": [], "condition_note": [ "사용횟수", "구 조 물" @@ -4047,7 +3811,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-01-07-02", @@ -4056,7 +3821,8 @@ "level": 3, "parent_code": "FP-01-07", "sort_order": 12544, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-07-03", @@ -4065,7 +3831,8 @@ "level": 3, "parent_code": "FP-01-07", "sort_order": 12800, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-01-07-04", @@ -4094,14 +3861,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "거 푸 집 씻 기", - "콘크리트혼합 및 양생", - "돌 쌓 기 모 르 타 르", - "돌 씻 기", - "모래씻기", - "잡 용 수" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -4140,7 +3900,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02", @@ -4150,7 +3911,8 @@ "parent_code": null, "sort_order": 13312, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-02-01", @@ -4182,22 +3944,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ 둘레베기", - "합 계", - "직접노무비", - "재 료 비", - "경비(기계경비)", - "<할인․할증률 적용>", - "구 분", - "둘레베기", - "o 집단화 정도(1-4-3)", - "o 경사도(1-4-5)" - ], + "variant_key": [], "condition_note": [ "ha당 풀베기(둘레베기) 단가산출서(예시)" ], @@ -4527,7 +4274,8 @@ ] } ], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-02-01-01", @@ -4554,11 +4302,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통휘발유 (주연료)", - "체인오일 (일반오일)", - "체인오일 (친환경오일)" - ], + "variant_key": [], "condition_note": [ "재료명", "소요량 (ℓ/대/일)", @@ -4603,9 +4347,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "예취기(휘발유)" - ], + "variant_key": [], "condition_note": [ "기계명(주재료)", "주연료 (ℓ/일,대)", @@ -4625,8 +4367,8 @@ "pum_table_id": "F0044", "section": "2-1-1. 휘발유․오일", "source_line": 1340, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 기계별 주연료(ℓ/일·대)·잡품(%) — 행이 자원이 아니라 기계, 재료비 산출 기준", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -4638,9 +4380,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "아키아윈치(휘발유)" - ], + "variant_key": [], "condition_note": [ "기계명(주재료)", "주연료 (ℓ/일,대)", @@ -4658,8 +4398,8 @@ "pum_table_id": "F0045", "section": "2-1-1. 휘발유․오일", "source_line": 1346, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 위와 같은 모양(2드럼 케이블윈치)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -4671,9 +4411,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "2드럼 케이블윈치(휘발유)" - ], + "variant_key": [], "condition_note": [ "기계명(주재료)", "주연료 (ℓ/일,대)", @@ -4704,9 +4442,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "천공기 (휘발유)" - ], + "variant_key": [], "condition_note": [ "기계명 (주재료)", "주연료 (ℓ/일,대)", @@ -4722,7 +4458,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-02", @@ -4736,8 +4473,8 @@ "pum_table_id": "F0047", "section": "2-1-2. 경유", "source_line": 1361, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 기계별 경유(ℓ/일·대)·잡품(%) — [주]③ 「1일당 소요량」, 행이 기계", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -4749,20 +4486,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "트랙터 부착 기계", - "굴삭기 부착 기계", - "타워야더(RME 300T)", - "콜라타워야더(K-301)", - "소형 포워더", - "무한궤도형 임내차", - "HAM300(임업용 트랙터 포함)", - "하베스터(헤드부착형)", - "스윙야더", - "소형트럭", - "우드그래플", - "임업용동력집재기" - ], + "variant_key": [], "condition_note": [ "기계명", "주연료 (ℓ/일,대)", @@ -4844,7 +4568,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-03", @@ -4858,8 +4583,8 @@ "pum_table_id": "F0048", "section": "2-1-3. 우드그래플 소모품", "source_line": 1387, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 머리 「소요비용(원)」 — 품이 아니라 교체 1회당 금액", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -4871,10 +4596,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "트랙 접지력 보강", - "블레이드 및 실린더 교체" - ], + "variant_key": [], "condition_note": [ "품 명", "소요비용(원)" @@ -4890,7 +4612,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-04", @@ -4917,11 +4640,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "주재료", - "페인트", - "마킹테이프" - ], + "variant_key": [], "condition_note": [ "재료명", "재료비(1km 소요량기준)", @@ -4968,10 +4687,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "주재료", - "페인트" - ], + "variant_key": [], "condition_note": [ "재료명", "재료비 (ha당 소요량)", @@ -5011,10 +4727,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "주재료", - "마킹테이프" - ], + "variant_key": [], "condition_note": [ "재료명", "재료비 (1km당 소요량)", @@ -5052,10 +4765,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "주재료", - "페인트" - ], + "variant_key": [], "condition_note": [ "재료명", "재료비 (ha당 소요량)", @@ -5078,7 +4788,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-05", @@ -5107,9 +4818,7 @@ "x(U+0078)" ], "capacity_formula_here": false, - "variant_key": [ - "농약 (Fluroxypyr -meptyl + Triclypyr-TEA 미탁제)" - ], + "variant_key": [], "condition_note": [ "재료명", "주재료(병)", @@ -5129,8 +4838,8 @@ "pum_table_id": "F0054", "section": "2-1-5. 약제(농약) 및 친환경비닐랩", "source_line": 1477, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "requirement", + "form_basis": "사람 판정 — 본문 「(본당)」 + 행 「소금처리량(g)」 — 본당 재료 소요량", "basis_quantity": 1.0, "basis_unit": "본", "basis_source": "본문", @@ -5142,9 +4851,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소금처리량(g)" - ], + "variant_key": [], "condition_note": [ "주두부직경(㎝)", "2㎝미만", @@ -5179,10 +4886,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "농약 (글리포세이트)", - "친환경 비닐랩" - ], + "variant_key": [], "condition_note": [ "재료명", "주재료 (병, 매, kg)", @@ -5204,7 +4908,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-06", @@ -5233,16 +4938,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "수량", - "천공테이프", - "흰색 페인트 (친환경성)", - "표식라벨", - "천공기날", - "약재주입기", - "방제복", - "약제배낭" - ], + "variant_key": [], "condition_note": [ "구 분", "주 재 료", @@ -5299,7 +4995,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-07", @@ -5326,13 +5023,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "훈증약제", - "훈증피복제", - "표식라벨", - "벌근훈증약제", - "벌근피복제" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -5372,7 +5063,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-08", @@ -5399,11 +5091,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "수 량", - "휘발유 (양수기)", - "깃 발" - ], + "variant_key": [], "condition_note": [ "구 분", "주 재 료", @@ -5430,7 +5118,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-09", @@ -5457,11 +5146,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "수 량", - "휘발유 (연료)", - "잡품" - ], + "variant_key": [], "condition_note": [ "구 분", "주 재 료", @@ -5488,7 +5173,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-10", @@ -5515,11 +5201,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "수 량", - "휘발유(발전기)", - "잡품" - ], + "variant_key": [], "condition_note": [ "구 분", "주 재 료", @@ -5546,7 +5228,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-11", @@ -5573,10 +5256,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "수 량", - "경유(차량살포)" - ], + "variant_key": [], "condition_note": [ "구 분", "주 재 료", @@ -5597,7 +5277,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-12", @@ -5624,10 +5305,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "그물망", - "표식라벨" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -5646,7 +5324,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-01-13", @@ -5660,8 +5339,8 @@ "pum_table_id": "F0063", "section": "2-1-13. 파쇄", "source_line": 1586, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 「소모율 0.00125개/hr」 — 기계 시간당 소모품, 공종 밑수가 아님", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -5673,10 +5352,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "메인파쇄기날", - "분쇄기날" - ], + "variant_key": [], "condition_note": [ "소모품", "소모율", @@ -5698,7 +5374,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02", @@ -5730,28 +5407,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ 묘목찾기", - "2. 모두베기", - "ㆍ 예취기 사용", - "- 재료비(예취기)", - "ㆍ보통휘발유(주연료)", - "ㆍ보통휘발유(잡품)", - "- 기계경비(예취기)", - "합 계", - "직접노무비", - "재 료 비", - "경비(기계경비)", - "<할인․할증률 적용>", - "구 분", - "묘목찾기", - "o 집단화 정도(1-4-3)", - "o 경사도(1-4-5)" - ], + "variant_key": [], "condition_note": [ "ha당 풀베기(모두베기) 단가산출서(예시)" ], @@ -6095,7 +5751,8 @@ ] } ], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-02-02-01", @@ -6124,9 +5781,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "45cc (배기량기준)" - ], + "variant_key": [], "condition_note": [ "규격", "손료계수 (1일 1대당)", @@ -6140,7 +5795,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-02", @@ -6169,9 +5825,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "예취기 (배기량 35cc)" - ], + "variant_key": [], "condition_note": [ "기계명 (규격)", "손료계수 (1일 1대당)", @@ -6185,7 +5839,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-03", @@ -6214,28 +5869,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "아키아윈치", - "2드럼 케이블윈치", - "파르미윈치(트랙터 포함)", - "타이푼윈치(트랙터 포함)", - "우드피싱(굴삭기 포함)", - "무한궤도형 임내차", - "스마트집재기(트랙터 포함)", - "HAM300(트랙터 포함)", - "콜라타워야더(K-301)", - "소형굴삭기+부착용집개", - "임업용 동력집재기(트랙터부착형)", - "HAM200, 춘천집재기, 스마트집재기", - "하베스터(헤드부착형)", - "0.6㎥급 무한궤도형 굴착기", - "동력상하차기, 임업용 굴착기(우드그래플)", - "타워야더(RME 300T)", - "스윙야더(기본차량 포함)", - "초소형 포워더", - "소형 포워더", - "소형트럭" - ], + "variant_key": [], "condition_note": [ "기계명", "손료계수 (1일 1대당)", @@ -6365,7 +5999,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-04", @@ -6394,9 +6029,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "배부식분무기" - ], + "variant_key": [], "condition_note": [ "기계명 (규격)", "손료계수 (1일 1대당)", @@ -6410,7 +6043,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-05", @@ -6439,9 +6073,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "45cc (배기량 기준)" - ], + "variant_key": [], "condition_note": [ "규 격", "손료계수", @@ -6455,7 +6087,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-06", @@ -6482,9 +6115,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "5HP(양수기)" - ], + "variant_key": [], "condition_note": [ "규 격", "손료계수(1일 1대당)", @@ -6498,7 +6129,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-07", @@ -6525,9 +6157,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "무인헬기(FAZER) 내용가동시간 1,400시간기준" - ], + "variant_key": [], "condition_note": [ "규 격", "1일 손료계수 (1일 1대당 4시간 가동)", @@ -6541,7 +6171,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-08", @@ -6568,10 +6199,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "무인멀티콥터(평균가) 내용가동시간 1,000시간 기준", - "리튬폴리머 배터리 (평균가) (16,000mA, 32000mA)" - ], + "variant_key": [], "condition_note": [ "규 격", "1일 손료계수 (1일 1대당 4시간 가동)", @@ -6590,7 +6218,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-09", @@ -6617,10 +6246,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "1톤(방제차량)", - "45HP(동력분무기)" - ], + "variant_key": [], "condition_note": [ "규 격", "손료계수 (1일 1대당)", @@ -6639,7 +6265,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-10", @@ -6666,9 +6293,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "-" - ], + "variant_key": [], "condition_note": [ "규격", "장비가격(천원)", @@ -6684,7 +6309,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-02-02-11", @@ -6713,13 +6339,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "손료계수(10⁻⁷)", - "굴착기(0.2~0.8㎥)", - "부착용집게(0.2~0.8㎥)", - "트럭탑재형크레인", - "타이어형크레인" - ], + "variant_key": [], "condition_note": [ "기 계 명", "기계손료(시간당 적용기준)" @@ -6747,7 +6367,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03", @@ -6757,7 +6378,8 @@ "parent_code": null, "sort_order": 20224, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-03-01", @@ -6784,10 +6406,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소요인력", - "0.2" - ], + "variant_key": [], "condition_note": [ "(단위 : 인/ha)" ], @@ -6821,32 +6440,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ단목", - "- 재료비(체인톱)", - "ㆍ보통휘발유(주연료)", - "ㆍ보통휘발유(잡품)", - "ㆍ체인오일(일반)", - "- 기계경비(체인톱)", - "2. 집재", - "ㆍ우드그래플", - "1.2m", - "1.8m", - "2.1m", - "2.7m", - "3.6m", - "- 재료비(우드그래플)", - "ㆍ경유(주연료)", - "ㆍ경유(잡품)", - "- 기계경비(우드그래플)", - "3. 운재", - "ㆍ소형트럭", - "- 재료비(소형트럭)" - ], + "variant_key": [], "condition_note": [ "ha당 임목수확 단가산출서(예시)" ], @@ -7735,7 +7329,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-02", @@ -7762,10 +7357,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구 분", - "작업로 예정선 선정 및 표식" - ], + "variant_key": [], "condition_note": [ "(단위 : 인/1일 1km당))" ], @@ -7801,32 +7393,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ전간", - "- 재료비(체인톱)", - "ㆍ보통휘발유(주연료)", - "ㆍ보통휘발유(잡품)", - "ㆍ체인오일(일반)", - "- 기계경비(체인톱)", - "2. 집재", - "2-1. 스마트집재기", - "ㆍ스마트집재기", - "50m이하", - "51~100m", - "101~15m", - "- 재료비(집재기)", - "ㆍ경유(주연료)", - "ㆍ경유(잡품)", - "- 기계경비(집재기)", - "2-2. 임업용굴착기", - "ㆍ우드그래플", - "- 재료비(우드그래플)", - "- 기계경비(우드그래플)" - ], + "variant_key": [], "condition_note": [ "ha당 임목수확 단가산출서(예시)" ], @@ -8827,7 +8394,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-03", @@ -8854,11 +8422,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구 분", - "소작업로", - "대작업로" - ], + "variant_key": [], "condition_note": [ "(단위 : 인/km당)" ], @@ -8899,32 +8463,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ하베스터", - "- 재료비(하베스터)", - "ㆍ경유(주연료)", - "ㆍ경유(잡품)", - "- 기계경비", - "ㆍ하베스터헤드", - "ㆍ0.6㎥굴착기", - "2. 집재", - "2-1. 타워야더", - "ㆍ타워야더", - "100m이하", - "150m이하", - "- 재료비(집재기)", - "- 기계경비(집재기)", - "2-2. 임업용굴착기", - "ㆍ우드그래플", - "- 재료비(우드그래플)", - "- 기계경비(우드그래플)", - "3. 운재", - "ㆍ소형포워더" - ], + "variant_key": [], "condition_note": [ "ha당 임목수확 단가산출서(예시)" ], @@ -9813,7 +9352,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-04", @@ -9823,7 +9363,8 @@ "parent_code": "FP-03", "sort_order": 21248, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-03-04-01", @@ -9832,7 +9373,8 @@ "level": 3, "parent_code": "FP-03-04", "sort_order": 21504, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-03-04-02", @@ -9841,7 +9383,8 @@ "level": 3, "parent_code": "FP-03-04", "sort_order": 21760, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-03-04-03", @@ -9868,10 +9411,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "노면정지", - "노면굴기" - ], + "variant_key": [], "condition_note": [ "종 별", "인 부", @@ -9890,7 +9430,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-05", @@ -9917,17 +9458,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "모든 벌채산물 임내존치지역", - "휴경지 등 정리", - "관목지 등 정리", - "산불 등 피해지 정리", - "불량림 정리", - "벌채부산물 임내존치지역", - "임업기계장비 이용 정리", - "벌채와 동시정리지역", - "모든 벌채부산물 반출" - ], + "variant_key": [], "condition_note": [ "구 분", "작업내용", @@ -9991,7 +9522,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-06", @@ -10018,10 +9550,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "10㎥ 미만", - "임내 정리" - ], + "variant_key": [], "condition_note": [ "구 분", "정리산물(㎥/ha)", @@ -10058,7 +9587,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-07", @@ -10085,12 +9615,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "10㎥ 미만", - "10m이하", - "20m이하", - "30m이하" - ], + "variant_key": [], "condition_note": [ "수 집 거 리", "재해산물 수집량(㎥/ha)", @@ -10155,7 +9680,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-03-08", @@ -10182,14 +9708,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "드론조종자 (건설기계조종원)", - "계", - "① 촬영 계획 수립", - "② 드론 촬영(RTK 포함)", - "③ 영상 처리 및 정사영상 생성", - "④ 결과물 점검 및 보고서 작성" - ], + "variant_key": [], "condition_note": [ "종 별", "소요인력(인)" @@ -10233,7 +9752,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04", @@ -10243,7 +9763,8 @@ "parent_code": null, "sort_order": 23296, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-04-01", @@ -10270,32 +9791,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "위치 및 임․소반", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ경급별(cm)", - "6", - "8", - "10", - "12", - "14", - "16", - "18", - "20", - "22", - "24", - "26", - "28", - "30", - "32", - "34", - "36", - "38", - "40", - "42" - ], + "variant_key": [], "condition_note": [ "소나무재선충병방제 단가산출서(예시)" ], @@ -11731,7 +11227,8 @@ ] } ], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-04-01-01", @@ -11758,12 +11255,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "벌도목 구분", - "단목", - "전간목", - "전목" - ], + "variant_key": [], "condition_note": [ "(단위 : ㎥/1인/1일)" ], @@ -11790,7 +11282,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-01-02", @@ -11817,10 +11310,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "벌도ㆍ조재 공정량(㎥)", - "59.20" - ], + "variant_key": [], "condition_note": [ "(단위 : ㎥/1인/1일)" ], @@ -11852,9 +11342,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "154" - ], + "variant_key": [], "condition_note": [ "조재 공정량(㎥)", "인력구분" @@ -11866,7 +11354,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-02", @@ -11895,32 +11384,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 임․소반", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ경급별(cm)", - "10cm이하", - "12~14㎝", - "16~18㎝", - "20~22㎝", - "24~26㎝", - "28~30㎝", - "32~34㎝", - "ㆍ작업보조", - "ㆍ벌목작업안전 보조", - "- 재료비(체인톱)", - "ㆍ 보통휘발유(주연료)", - "ㆍ 보통휘발유(잡품)", - "ㆍ 체인오일(일반오일)", - "- 기계경비(체인톱)", - "2. 작업로설치", - "ㆍ 소작업로", - "ㆍ 대작업로", - "3. 산물수집", - "4. 집적" - ], + "variant_key": [], "condition_note": [ "ha당 참나무시들음병방제 단가산출서(예시)" ], @@ -12698,7 +12162,8 @@ ] } ], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-04-02-01", @@ -12716,7 +12181,7 @@ "form_basis": "직종 표기((인)·인부·공)", "basis_quantity": 100.0, "basis_unit": "본", - "basis_source": "절 이름", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -12727,21 +12192,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "벌목", - "10㎝이하", - "12~14㎝", - "16~18㎝", - "20~22㎝", - "24~26㎝", - "28~30㎝", - "32~34㎝", - "36~38㎝", - "40~42㎝", - "44~46㎝", - "48㎝ 이상", - "작업보조" - ], + "variant_key": [], "condition_note": [ "제거 대상목 흉고직경", "소요인력(인)", @@ -12855,7 +12306,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-02-02", @@ -12873,7 +12325,7 @@ "form_basis": "직종 표기((인)·인부·공)", "basis_quantity": 1000.0, "basis_unit": "㎡", - "basis_source": "절 이름", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -12886,9 +12338,8 @@ "capacity_formula_here": false, "variant_key": [ "5m 미만", - "벌목부 보통인부", - "굴착기+부착용집게", - "비고" + "5m이상~8m미만", + "8m 이상" ], "condition_note": [ "구 분", @@ -12931,6 +12382,11 @@ ] ] } + ], + "variant_keys": [ + "5m 미만", + "5m이상~8m미만", + "8m 이상" ] }, { @@ -12960,14 +12416,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "16~20cm", - "22~30cm", - "32~40cm", - "42~50cm", - "52cm이상" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (cm)", "소요인력(인/본당)", @@ -13024,7 +12473,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-04", @@ -13051,9 +12501,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부" - ], + "variant_key": [], "condition_note": [ "명 칭", "1종", @@ -13067,7 +12515,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-05", @@ -13094,9 +12543,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "0.13인" - ], + "variant_key": [], "condition_note": [ "소요인력(인)", "인력구분" @@ -13108,7 +12555,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-04-06", @@ -13135,9 +12583,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "1인" - ], + "variant_key": [], "condition_note": [ "소요인력(인)", "인력구분" @@ -13149,7 +12595,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05", @@ -13159,7 +12606,8 @@ "parent_code": null, "sort_order": 26112, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-01", @@ -13169,7 +12617,8 @@ "parent_code": "FP-05", "sort_order": 26368, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-01-01", @@ -13185,9 +12634,9 @@ "source_line": 2081, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13200,8 +12649,9 @@ "capacity_formula_here": false, "variant_key": [ "0.3m 미만", - "특 별 인 부", - "보 통 인 부" + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ], "condition_note": [ "구 분", @@ -13231,6 +12681,12 @@ ] ] } + ], + "variant_keys": [ + "0.3m 미만", + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ] }, { @@ -13247,9 +12703,9 @@ "source_line": 2099, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13261,7 +12717,6 @@ ], "capacity_formula_here": false, "variant_key": [ - "특별인부", "1.0 이하", "1.1∼1.5", "1.6∼2.0", @@ -13270,8 +12725,7 @@ "3.1∼3.5", "3.6∼4.0", "4.1∼4.5", - "4.6∼5.0", - "비 고" + "4.6∼5.0" ], "condition_note": [ "나무높이(m)", @@ -13335,6 +12789,17 @@ ] ] } + ], + "variant_keys": [ + "1.0 이하", + "1.1∼1.5", + "1.6∼2.0", + "2.1∼2.5", + "2.6∼3.0", + "3.1∼3.5", + "3.6∼4.0", + "4.1∼4.5", + "4.6∼5.0" ] }, { @@ -13351,9 +12816,9 @@ "source_line": 2125, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13364,27 +12829,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "특별인부(인)", - "4이하", - "5(4이하)", - "6 ∼ 7(5 ∼ 6)", - "8 ∼ 9(7 ∼ 8)", - "10 ∼ 11(9)", - "12 ∼ 14(10 ∼ 12)", - "15 ∼ 17(13 ∼ 14)", - "18 ∼ 19(15 ∼ 16)", - "20 ∼ 24(17 ∼ 20)", - "25 ∼ 29(21 ∼ 24)", - "30 ∼ 34(25 ∼ 28)", - "35 ∼ 39(29 ∼ 32)", - "40 ∼ 44(33 ∼ 37)", - "45 ∼ 49(38 ∼ 41)", - "50 ∼ 54(42 ∼ 45)", - "55 ∼ 59(46 ∼ 49)", - "60(50)", - "비 고" - ], + "variant_key": [], "condition_note": [ "근원(흉고)직경(㎝)", "수량" @@ -13544,12 +12989,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼ 19", - "20 ∼ 26", - "27 ∼ 39", - "40 ∼ 60" - ], + "variant_key": [], "condition_note": [ "근원직경(cm)", "굴착기(㎥)", @@ -13578,7 +13018,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-01-04", @@ -13627,6 +13068,10 @@ ] ] } + ], + "variant_keys": [ + "줄떼", + "평떼" ] }, { @@ -13654,14 +13099,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "지게", - "리어카", - "우마차", - "2.5톤 트럭", - "6톤 트럭", - "8톤 트럭" - ], + "variant_key": [], "condition_note": [ "구 분", "줄떼(매)", @@ -13721,7 +13159,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-02", @@ -13737,9 +13176,9 @@ "source_line": 2194, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13748,20 +13187,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "3", - "5", - "7", - "9", - "11", - "13", - "15", - "18", - "21", - "24", - "30" - ], + "variant_key": [], "condition_note": [ "근원직경(㎝)", "수 량", @@ -13867,7 +13293,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-03", @@ -13877,7 +13304,8 @@ "parent_code": "FP-05", "sort_order": 28160, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-03-01", @@ -13893,9 +13321,9 @@ "source_line": 2219, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1000.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13904,11 +13332,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소묘식재", - "중묘식재", - "대묘식재" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -13940,8 +13364,8 @@ "pum_table_id": "F0100", "section": "5-3-1. 나무식재", "source_line": 2230, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 수종별 묘목 규격(소·중·대묘) 표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -13953,17 +13377,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소나무", - "낙엽송", - "잣나무", - "편백", - "삼나무", - "리기테다소나무", - "백합나무", - "가시나무류", - "※ 위 표에서 ‘노’는 ‘노지묘’이고, ‘용’은 ‘용기묘’이다. ※ 그 밖의 수종은 수종별 간장(최소 간장을 말한다) 및 근원경을 기준으로 아래와 같이 적용하되, 활엽수 삽목묘ㆍ접목묘 대묘는 이를 중묘로 본다. 구분침엽수활엽수소묘간장 20cm미만간장 30cm미만중묘간장 20cm이상 40cm미만 또는간장 40cm이상이고 근원경 8mm미만간장 30cm이상 60cm미만 또는간장 60cm이상이고 근원경 11mm미만대묘간장 40cm이상이고 근원경 8mm이상간장 60cm이상이고 근원경 11mm이상 구분 침엽수 활엽수 소묘 간장 20cm미만 간장 30cm미만 중묘 간장 20cm이상 40cm미만 또는 간장 40cm이상이고 근원경 8mm미만 간장 30cm이상 60cm미만 또는 간장 60cm이상이고 근원경 11mm미만 대묘 간장 40cm이상이고 근원경 8mm이상 간장 60cm이상이고 근원경 11mm이상" - ], + "variant_key": [], "condition_note": [ "수종별", "소 묘", @@ -14031,8 +13445,8 @@ "pum_table_id": "F0101", "section": "5-3-1. 나무식재", "source_line": 2242, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 간장·근원경 규격 기준 표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -14044,11 +13458,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소묘", - "중묘", - "대묘" - ], + "variant_key": [], "condition_note": [ "구분", "침엽수", @@ -14072,7 +13482,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-03-02", @@ -14088,9 +13499,9 @@ "source_line": 2254, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14103,8 +13514,9 @@ "capacity_formula_here": false, "variant_key": [ "0.3m 미만", - "특 별 인 부", - "보 통 인 부" + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ], "condition_note": [ "구 분", @@ -14134,6 +13546,12 @@ ] ] } + ], + "variant_keys": [ + "0.3m 미만", + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ] }, { @@ -14150,9 +13568,9 @@ "source_line": 2272, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14165,8 +13583,9 @@ "capacity_formula_here": false, "variant_key": [ "0.3m 미만", - "특 별 인 부", - "보 통 인 부" + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ], "condition_note": [ "구 분", @@ -14196,6 +13615,12 @@ ] ] } + ], + "variant_keys": [ + "0.3m 미만", + "0.3∼0.7m 이하", + "0.8∼1.1m 이하", + "1.2∼1.5m 이하" ] }, { @@ -14212,9 +13637,9 @@ "source_line": 2291, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14225,20 +13650,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "인 력 시 공", - "특별인부(인)", - "1.0 이하", - "1.1 ∼ 1.5", - "1.6 ∼ 2.0", - "2.1 ∼ 2.5", - "2.6 ∼ 3.0", - "3.1 ∼ 3.5", - "3.6 ∼ 4.0", - "4.1 ∼ 4.5", - "4.6 ∼ 5.0", - "비고" - ], + "variant_key": [], "condition_note": [ "나 무 높 이(m)", "수 량" @@ -14346,8 +13758,8 @@ "pum_table_id": "F0105", "section": "5-3-4. 교목식재(나무높이)", "source_line": 2306, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 지주목 미설치 감률 「인력품의 10%·20%」", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -14359,9 +13771,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력품의 10%" - ], + "variant_key": [], "condition_note": [ "인력시공시", "기계시공시" @@ -14373,7 +13783,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-03-05", @@ -14389,9 +13800,9 @@ "source_line": 2322, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14402,25 +13813,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "특별인부(인)", - "4(5)이하", - "5(6)", - "6 ∼ 7(7 ∼ 8)", - "8 ∼ 9(9 ∼ 11)", - "10 ∼ 11(12 ∼ 13)", - "12 ∼ 14(14 ∼ 17)", - "15 ∼ 17(18 ∼ 20)", - "18 ∼ 19(21 ∼ 23)", - "20 ∼ 24(24 ∼ 29)", - "25 ∼ 29(30 ∼ 35)", - "30 ∼ 34(36 ∼ 41)", - "35 ∼ 39(42 ∼ 47)", - "40 ∼ 44(48 ∼ 53)", - "45 ∼ 49(54 ∼ 59)", - "50(60)", - "비고" - ], + "variant_key": [], "condition_note": [ "흉고(근원)직경(㎝)", "수 량" @@ -14551,8 +13944,8 @@ "pum_table_id": "F0107", "section": "5-3-5. 교목식재(흉고직경)", "source_line": 2342, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 위와 같은 감률(흉고직경 표)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -14564,9 +13957,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력품의 10%" - ], + "variant_key": [], "condition_note": [ "인력시공시", "기계시공시" @@ -14597,12 +13988,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "8 ∼ 17", - "18 ∼ 22", - "23 ∼ 34", - "35 ∼ 50" - ], + "variant_key": [], "condition_note": [ "흉고직경(cm)", "굴착기(㎥)", @@ -14631,7 +14017,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-04", @@ -14647,9 +14034,9 @@ "source_line": 2366, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14660,13 +14047,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "파종상 만들기", - "80cm×80cm×5,000개", - "파종상에 점파", - "파종상 없이", - "점 파" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -14706,7 +14087,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-05", @@ -14722,9 +14104,9 @@ "source_line": 2383, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14736,10 +14118,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "지면긁기작업", - "폭 80cm × 열간거리 2m (전면적의 40%)" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -14761,7 +14140,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-06", @@ -14777,9 +14157,9 @@ "source_line": 2396, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14788,9 +14168,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "맹아근주 정리작업" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -14806,7 +14184,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-07", @@ -14822,9 +14201,9 @@ "source_line": 2409, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14833,11 +14212,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "움싹본수조절", - "치수이식", - "보완조림" - ], + "variant_key": [], "condition_note": [ "구 분", "내 용", @@ -14865,7 +14240,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-08", @@ -14881,9 +14257,9 @@ "source_line": 2426, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14894,9 +14270,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "큰나무 식재" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -14912,7 +14286,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-09", @@ -14928,9 +14303,9 @@ "source_line": 2439, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14939,9 +14314,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "식 혈", @@ -14957,7 +14330,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-10", @@ -14986,14 +14360,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "묘목", - "요소", - "인산", - "운반비", - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -15046,7 +14413,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-11", @@ -15062,9 +14430,9 @@ "source_line": 2475, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15073,9 +14441,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "구 분", "사초굴취", @@ -15091,7 +14457,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-12", @@ -15120,10 +14487,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "줄떼", - "평떼" - ], + "variant_key": [], "condition_note": [ "구 분", "보통인부(인)", @@ -15142,7 +14506,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-13", @@ -15158,9 +14523,9 @@ "source_line": 2511, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "㎡", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15191,6 +14556,10 @@ ] ] } + ], + "variant_keys": [ + "줄떼심기", + "띠떼심기" ] }, { @@ -15218,14 +14587,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "새채집", - "요소", - "인산", - "보통인부 (채집)", - "보통인부 (심기)", - "새운반" - ], + "variant_key": [], "condition_note": [ "공정별", "단 위", @@ -15271,7 +14633,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-15", @@ -15300,11 +14663,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "말뚝", - "보통인부", - "특별인부" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -15336,7 +14695,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-16", @@ -15346,7 +14706,8 @@ "parent_code": "FP-05", "sort_order": 32768, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-16-01", @@ -15362,9 +14723,9 @@ "source_line": 2565, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "m", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15375,11 +14736,8 @@ "capacity_formula_here": false, "variant_key": [ "보통토사", - "절 취", - "수평잡기 및 단정리", - "잡석 및 뿌리 정리", - "절·성토면 고르기", - "합계" + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사" ], "condition_note": [ "공정별", @@ -15431,6 +14789,11 @@ ] ] } + ], + "variant_keys": [ + "보통토사", + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사" ] }, { @@ -15458,12 +14821,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "단끊기", - "떼붙임", - "떼운반", - "떼 지게운반" - ], + "variant_key": [], "condition_note": [ "공정별", "단 위", @@ -15497,7 +14855,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-17", @@ -15507,7 +14866,8 @@ "parent_code": "FP-05", "sort_order": 33536, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-17-01", @@ -15534,12 +14894,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "단끊기", - "줄떼심기", - "떼운반", - "떼 지게운반" - ], + "variant_key": [], "condition_note": [ "공정별", "단 위", @@ -15573,7 +14928,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-17-02", @@ -15600,11 +14956,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "단끊기", - "돌쌓기", - "돌운반" - ], + "variant_key": [], "condition_note": [ "공정별", "단 위", @@ -15632,7 +14984,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-18", @@ -15659,16 +15012,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "종자", - "비료", - "비토", - "객토", - "골파기", - "씨덮기", - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "공정별", "단 위", @@ -15726,7 +15070,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-19", @@ -15736,7 +15081,8 @@ "parent_code": "FP-05", "sort_order": 34560, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-19-01", @@ -15763,10 +15109,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "6", - "표토절취" - ], + "variant_key": [], "condition_note": [ "구 분", "직 종", @@ -15793,7 +15136,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-19-02", @@ -15820,11 +15164,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "15", - "㎥ 당", - "100㎡" - ], + "variant_key": [], "condition_note": [ "구 분", "직 종", @@ -15855,7 +15195,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-20", @@ -15882,11 +15223,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "표토채취", - "굴착기(무한궤도, 0.7㎥)", - "표토붙이기" - ], + "variant_key": [], "condition_note": [ "공 정", "구 분", @@ -15920,7 +15257,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-21", @@ -15948,8 +15286,8 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "채 취", - "운 반", + "채 취", + "운 반", "붙이기" ], "condition_note": [ @@ -15975,6 +15313,11 @@ ] ] } + ], + "variant_keys": [ + "채 취", + "운 반", + "붙이기" ] }, { @@ -15985,7 +15328,8 @@ "parent_code": "FP-05", "sort_order": 35840, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-22-01", @@ -15994,7 +15338,8 @@ "level": 3, "parent_code": "FP-05-22", "sort_order": 36096, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-05-22-02", @@ -16021,10 +15366,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조경공", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -16046,7 +15388,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-22-03", @@ -16073,9 +15416,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -16091,7 +15432,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-22-04", @@ -16118,11 +15460,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조경공", - "보통인부", - "트럭" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -16154,7 +15492,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-23", @@ -16164,7 +15503,8 @@ "parent_code": "FP-05", "sort_order": 37120, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-23-01", @@ -16173,7 +15513,8 @@ "level": 3, "parent_code": "FP-05-23", "sort_order": 37376, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-05-23-02", @@ -16200,10 +15541,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조경공", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -16225,7 +15563,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-23-03", @@ -16252,9 +15591,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -16270,7 +15607,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-24", @@ -16280,7 +15618,8 @@ "parent_code": "FP-05", "sort_order": 38144, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-24-01", @@ -16314,18 +15653,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "비 료", - "피 복 제", - "침식안정제", - "색 소", - "장비", - "트 럭", - "물 탱 크", - "인력", - "보 통 인 부" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -16416,7 +15744,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-24-02", @@ -16449,18 +15778,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "비 료", - "피 복 제", - "침식안정제", - "색 소", - "장비", - "트 럭", - "물 탱 크", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -16555,8 +15873,8 @@ "pum_table_id": "F0137", "section": "5-24-2. 뿜어붙이기-기계/마사토", "source_line": 2814, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 머리 「종자배합비율(%)」 조견표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -16570,14 +15888,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "초본 위주형", - "초본, 야생화류", - "외래초종(양잔디류)", - "합 계", - "초본ㆍ관목 혼합형", - "목본 군락형" - ], + "variant_key": [], "condition_note": [ "복원목표", "식 생 구 분", @@ -16650,8 +15961,8 @@ "pum_table_id": "F0138", "section": "5-24-2. 뿜어붙이기-기계/마사토", "source_line": 2831, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 위와 같은 조견표(기타 적용지역)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -16665,14 +15976,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "초본 위주형", - "초본, 야생화류", - "외래초종(양잔디류)", - "합 계", - "초본·관목 혼합형", - "목본 군락형" - ], + "variant_key": [], "condition_note": [ "복원목표", "식 생 구 분", @@ -16741,7 +16045,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-25", @@ -16768,11 +16073,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -16803,7 +16104,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-26", @@ -16813,7 +16115,8 @@ "parent_code": "FP-05", "sort_order": 39168, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-26-01", @@ -16840,12 +16143,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "9", - "사토", - "양토", - "식토" - ], + "variant_key": [], "condition_note": [ "토성", "막갈이깊이(cm)" @@ -16885,7 +16183,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-26-02", @@ -16912,12 +16211,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "9", - "사토", - "양토", - "식토" - ], + "variant_key": [], "condition_note": [ "토성", "막갈이깊이(cm)" @@ -16957,7 +16251,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-26-03", @@ -16986,11 +16281,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10% 이내", - "개답", - "개전" - ], + "variant_key": [], "condition_note": [ "토성", "경토깊이(cm)" @@ -17016,7 +16307,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-27", @@ -17043,10 +16335,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조경공", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "수량", @@ -17065,7 +16354,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-28", @@ -17075,7 +16365,8 @@ "parent_code": "FP-05", "sort_order": 40448, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-05-28-01", @@ -17102,10 +16393,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특 별 인 부", - "보 통 인 부" - ], + "variant_key": [], "condition_note": [ "구 분", "수 량", @@ -17124,7 +16412,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-28-02", @@ -17156,8 +16445,7 @@ "capacity_formula_here": false, "variant_key": [ "폭 1.5m 이하", - "조 경 공", - "보 통 인 부" + "폭 2.0m 이하" ], "condition_note": [ "구 분", @@ -17189,6 +16477,10 @@ ] ] } + ], + "variant_keys": [ + "폭 1.5m 이하", + "폭 2.0m 이하" ] }, { @@ -17203,8 +16495,8 @@ "pum_table_id": "F0146", "section": "5-29. 복사이식", "source_line": 2941, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 K(버킷계수)·f·E·Cm — 굴착기 작업량 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -17218,12 +16510,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "K(버킷계수)", - "f(체적환산계수)", - "E(작업효율)", - "Cm(1회 사이클시간)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -17252,7 +16539,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-30", @@ -17279,9 +16567,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "표시봉 설치" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -17297,7 +16583,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-31", @@ -17324,9 +16611,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "지주목 설치" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -17342,7 +16627,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-32", @@ -17369,9 +16655,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "대 절" - ], + "variant_key": [], "condition_note": [ "구 분", "사용도구", @@ -17387,7 +16671,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-33", @@ -17414,10 +16699,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소묘, 중묘", - "대 묘" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력", @@ -17436,7 +16718,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-05-34", @@ -17463,11 +16746,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소 묘", - "중 묘", - "대 묘" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력", @@ -17491,7 +16770,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06", @@ -17501,7 +16781,8 @@ "parent_code": null, "sort_order": 42752, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-06-01", @@ -17528,11 +16809,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인공림(10년이하 조림지)", - "인공림(10년초과 성림지)", - "천연림" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력 (인/100kg)", @@ -17556,7 +16833,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-02", @@ -17566,7 +16844,8 @@ "parent_code": "FP-06", "sort_order": 43264, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-06-02-01", @@ -17593,9 +16872,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "낫" - ], + "variant_key": [], "condition_note": [ "사용도구", "단위", @@ -17611,7 +16888,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-02-02", @@ -17638,12 +16916,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "묘목찾기", - "줄 베 기", - "1,500본 이상 3,000본 미만", - "3,000본 이상" - ], + "variant_key": [], "condition_note": [ "공 법", "사용도구", @@ -17686,7 +16959,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-02-03", @@ -17713,12 +16987,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "묘목찾기", - "모두베기", - "조림 2년차", - "조림 3년차 이상" - ], + "variant_key": [], "condition_note": [ "공 법", "사용도구", @@ -17761,7 +17030,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-03", @@ -17788,9 +17058,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "괭이, 도끼 등" - ], + "variant_key": [], "condition_note": [ "사용도구", "단위", @@ -17806,7 +17074,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-04", @@ -17816,7 +17085,8 @@ "parent_code": "FP-06", "sort_order": 44544, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-06-04-01", @@ -17843,9 +17113,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "기계작업" - ], + "variant_key": [], "condition_note": [ "구 분", "사용도구", @@ -17863,7 +17131,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-04-02", @@ -17890,10 +17159,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "약제살포", - "작업보조" - ], + "variant_key": [], "condition_note": [ "구 분", "사용도구", @@ -17918,7 +17184,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-04-03", @@ -17947,13 +17214,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "소금 처리", - "2 ~ 6㎝미만", - "6 ~ 8㎝미만", - "8㎝ 이상", - "덩굴 제거지점 표시" - ], + "variant_key": [], "condition_note": [ "구 분", "주두부 직경", @@ -17999,7 +17260,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-04-04", @@ -18028,12 +17290,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "뿌리 고살", - "1 ~ 4㎝", - "4㎝ 초과", - "덩굴 제거지점 표시" - ], + "variant_key": [], "condition_note": [ "구 분", "주두부 직경", @@ -18091,13 +17348,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "80% 이상", - "60∼80%", - "40∼60%", - "20∼40%", - "20%이하" - ], + "variant_key": [], "condition_note": [ "덩굴 피복도(%)", "할인ㆍ할증율", @@ -18137,7 +17388,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-05", @@ -18164,11 +17416,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부 (체인톱 사용)", - "유령림 단계", - "보통인부 (작업 보조)" - ], + "variant_key": [], "condition_note": [ "인력구분", "대상지 유형", @@ -18200,7 +17448,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-06", @@ -18216,9 +17465,9 @@ "source_line": 3276, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18229,15 +17478,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "0~0.5m", - "0.5~1m", - "0~1m", - "0~2m", - "0~4m", - "2~4m", - "4~6m" - ], + "variant_key": [], "condition_note": [ "가지치기 높이", "소나무 낙엽송", @@ -18305,7 +17546,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-07", @@ -18315,7 +17557,8 @@ "parent_code": "FP-06", "sort_order": 46336, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-06-07-01", @@ -18331,9 +17574,9 @@ "source_line": 3304, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "주", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18344,11 +17587,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "11미만", - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "수량(근원직경 ㎝)" @@ -18380,7 +17619,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-07-02", @@ -18396,9 +17636,9 @@ "source_line": 3318, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "㎡", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18407,10 +17647,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특 별 인 부", - "보 통 인 부" - ], + "variant_key": [], "condition_note": [ "명 칭", "수 량", @@ -18429,7 +17666,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-07-03", @@ -18459,11 +17697,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특 별 인 부", - "보 통 인 부", - "트 럭(2.5t)" - ], + "variant_key": [], "condition_note": [ "명 칭", "단 위", @@ -18487,7 +17721,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-06-08", @@ -18517,11 +17752,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "우 드 칩", - "보 통 인 부", - "소 운 반" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -18549,7 +17780,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07", @@ -18559,7 +17791,8 @@ "parent_code": null, "sort_order": 47616, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-01", @@ -18569,7 +17802,8 @@ "parent_code": "FP-07", "sort_order": 47872, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-01-01", @@ -18596,11 +17830,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100m이하", - "200m이하", - "300m이하" - ], + "variant_key": [], "condition_note": [ "구 분", "어려움 (15˚ 미만)", @@ -18651,12 +17881,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "ha당원목재적 평균거리", - "100m이하", - "200m이하", - "300m이하" - ], + "variant_key": [], "condition_note": [ "경사", "어려움(15˚ 미만)", @@ -18730,12 +17955,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "ha당원목재적 평균거리", - "100m이하", - "200m이하", - "300m이하" - ], + "variant_key": [], "condition_note": [ "경사", "어려움(15˚미만)", @@ -18805,7 +18025,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-01-02", @@ -18832,15 +18053,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "8", - "10m이하", - "병해충방제", - "20m이하", - "30m이하", - "40m이하", - "50m이하" - ], + "variant_key": [], "condition_note": [ "집 재 거 리", "구분", @@ -18993,7 +18206,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-02", @@ -19022,13 +18236,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "20이하㎥", - "0~100m", - "0~150m", - "0~200m", - "0~250m" - ], + "variant_key": [], "condition_note": [ "집재거리", "ha당 집재재적", @@ -19082,7 +18290,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-03", @@ -19109,9 +18318,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "50m 내외" - ], + "variant_key": [], "condition_note": [ "집재거리", "수집량", @@ -19125,7 +18332,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-04", @@ -19135,7 +18343,8 @@ "parent_code": "FP-07", "sort_order": 49152, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-04-01", @@ -19162,19 +18371,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "50이하", - "0.1", - "0.2", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "0.8", - "0.9", - "1.0" - ], + "variant_key": [], "condition_note": [ "본당재적 (㎥)", "집재거리(m)", @@ -19271,7 +18468,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-04-02", @@ -19298,10 +18496,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "20m이하", - "집재량" - ], + "variant_key": [], "condition_note": [ "구 분", "집 재 거 리", @@ -19328,7 +18523,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-05", @@ -19338,7 +18534,8 @@ "parent_code": "FP-07", "sort_order": 49920, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-05-01", @@ -19365,19 +18562,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "30이하", - "0.1", - "0.2", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "0.8", - "0.9", - "1.0" - ], + "variant_key": [], "condition_note": [ "본당재적 (㎥)", "집재거리(m)", @@ -19474,7 +18659,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-05-02", @@ -19503,14 +18689,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "0.1∼0.2㎥", - "0∼40m", - "0∼60m", - "0∼80m", - "0∼100m", - "0∼120m" - ], + "variant_key": [], "condition_note": [ "집재거리", "본당 평균재적", @@ -19561,7 +18740,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-06", @@ -19588,10 +18768,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "60m 이하", - "26㎥" - ], + "variant_key": [], "condition_note": [ "집재거리(m)", "인력구분" @@ -19609,7 +18786,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-07", @@ -19619,7 +18797,8 @@ "parent_code": "FP-07", "sort_order": 50944, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-07-01", @@ -19646,19 +18825,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "50이하", - "0.1", - "0.2", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "0.8", - "0.9", - "1.0" - ], + "variant_key": [], "condition_note": [ "본당재적 (㎥)", "집재거리(m)", @@ -19755,7 +18922,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-07-02", @@ -19784,14 +18952,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "100m 이하", - "0∼20㎥", - "21∼40㎥", - "41∼60㎥", - "61∼80㎥", - "81∼100㎥" - ], + "variant_key": [], "condition_note": [ "집재재적 (㎥/ha)", "집재거리", @@ -19842,7 +19003,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-08", @@ -19852,7 +19014,8 @@ "parent_code": "FP-07", "sort_order": 51712, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-08-01", @@ -19879,11 +19042,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "120m 이하", - "상향집재", - "하향집재" - ], + "variant_key": [], "condition_note": [ "집재방식", "작업로(가선)의 길이", @@ -19922,7 +19081,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-08-02", @@ -19949,11 +19109,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "120m 이하", - "상향집재", - "하향집재" - ], + "variant_key": [], "condition_note": [ "집재방식", "작업로(가선)의 길이", @@ -19992,7 +19148,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-08-03", @@ -20019,19 +19176,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100m이하", - "0.1", - "0.2", - "0.3", - "0.4", - "0.5", - "0.6", - "0.7", - "0.8", - "0.9", - "1.0" - ], + "variant_key": [], "condition_note": [ "본당재적 (㎥)", "집재거리(m)", @@ -20128,7 +19273,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-09", @@ -20138,7 +19284,8 @@ "parent_code": "FP-07", "sort_order": 52736, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-09-01", @@ -20167,14 +19314,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "100m 이하", - "0~20㎥", - "21~40㎥", - "41~60㎥", - "61~80㎥", - "81~100㎥" - ], + "variant_key": [], "condition_note": [ "집재재적 (㎥/ha)", "집재거리", @@ -20231,7 +19371,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-09-02", @@ -20260,14 +19401,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "100m 이하", - "0~20㎥", - "21~40㎥", - "41~60㎥", - "61~80㎥", - "81~100㎥" - ], + "variant_key": [], "condition_note": [ "집재재적 (㎥/ha)", "집재거리", @@ -20328,8 +19462,8 @@ "pum_table_id": "F0187", "section": "7-9-2. 숲가꾸기, 소나무재선충병방제", "source_line": 3692, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 본문 「(단위 : %)」 — 집재재적·거리별 수집량 증가율", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -20343,14 +19477,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "100m 이하", - "0~20㎥", - "21~40㎥", - "41~60㎥", - "61~80㎥", - "81~100㎥" - ], + "variant_key": [], "condition_note": [ "노선별 집재재적(㎥/ha)", "집재거리" @@ -20400,7 +19527,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-10", @@ -20429,10 +19557,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "20㎥ 미만", - "작업량" - ], + "variant_key": [], "condition_note": [ "구 분", "ha당 평균 작업량", @@ -20455,7 +19580,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-11", @@ -20469,8 +19595,8 @@ "pum_table_id": "F0189", "section": "7-11. 동력상하차기(우드그래플) 집재-수확", "source_line": 3723, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 대당 일 작업량", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -20484,13 +19610,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "원목의 길이", - "1.2m", - "41∼60", - "61∼80", - "81이상" - ], + "variant_key": [], "condition_note": [ "집 재 재 적 (㎥/ha)", "최대집재거리 : 40m 이하", @@ -20548,8 +19668,8 @@ "pum_table_id": "F0190", "section": "7-11. 동력상하차기(우드그래플) 집재-수확", "source_line": 3731, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 위와 같음(최대집재거리 80m 이하)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -20563,13 +19683,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "원목의 길이", - "1.2m", - "41∼60", - "61∼80", - "81이상" - ], + "variant_key": [], "condition_note": [ "집 재 재 적 (㎥/ha)", "최대집재거리 : 80m 이하", @@ -20640,9 +19754,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "원목집재와 동시에 부산물 수집시" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력" @@ -20655,7 +19767,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-12", @@ -20684,13 +19797,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "9㎝ 이하", - "1.8m", - "2.1m", - "2.7m", - "3.6m" - ], + "variant_key": [], "condition_note": [ "원목(생산재)의 길이", "원목의 직경(말구 평균직경)", @@ -20749,7 +19856,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-13", @@ -20776,9 +19884,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "생산목 검척" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력(인/100㎥당)", @@ -20796,8 +19902,8 @@ "pum_table_id": "F0194", "section": "7-13. 검척", "source_line": 3834, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 검척 산출 예시(말구직경×개수) 표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -20809,18 +19915,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "2.1m", - "8", - "10", - "12", - "14", - "16", - "18", - "20", - "22", - "합계" - ], + "variant_key": [], "condition_note": [ "재장", "말구직경", @@ -20890,7 +19985,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-14", @@ -20919,12 +20015,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "1회 적 재 량", - "용 재 (원목)", - "침 엽 수 (포플러과 포함)", - "저 나 르 기 트 럭 집 재 뗏 목 토 장 집 재" - ], + "variant_key": [], "condition_note": [ "종 별", "공 정" @@ -20972,7 +20063,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-15", @@ -20982,7 +20074,8 @@ "parent_code": "FP-07", "sort_order": 54784, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-07-15-01", @@ -20996,8 +20089,8 @@ "pum_table_id": "F0196", "section": "7-15-1. 수확", "source_line": 3864, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량, 「회수」 줄은 참고", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21009,11 +20102,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100", - "회수", - "운반량" - ], + "variant_key": [], "condition_note": [ "구분", "주행거리(m이하)", @@ -21067,7 +20156,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-15-02", @@ -21081,8 +20171,8 @@ "pum_table_id": "F0197", "section": "7-15-2 숲가꾸기, 병해충방제", "source_line": 3878, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 「운재량」 줄이 작업량, [주]① 1대 1일", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21094,11 +20184,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "0.5", - "작업회수", - "운재량" - ], + "variant_key": [], "condition_note": [ "구 분", "운반거리(㎞ 이하)", @@ -21149,7 +20235,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-16", @@ -21163,8 +20250,8 @@ "pum_table_id": "F0198", "section": "7-16. 소형 포워더 운재", "source_line": 3898, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21176,11 +20263,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100", - "회수", - "운반량" - ], + "variant_key": [], "condition_note": [ "구분", "주행거리(m이하)", @@ -21234,7 +20317,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-17", @@ -21248,8 +20332,8 @@ "pum_table_id": "F0199", "section": "7-17. 소형트럭 운재", "source_line": 3912, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21261,11 +20345,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100", - "회수", - "운반량" - ], + "variant_key": [], "condition_note": [ "구분", "운반거리(m이하)", @@ -21316,7 +20396,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-07-18", @@ -21330,8 +20411,8 @@ "pum_table_id": "F0200", "section": "7-18. 지조운반(포워더 활용)", "source_line": 3926, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 「운반량」 줄이 작업량", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21343,11 +20424,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "100", - "회 수", - "운반량" - ], + "variant_key": [], "condition_note": [ "구 분", "운반거리(m이하)", @@ -21398,7 +20475,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08", @@ -21408,7 +20486,8 @@ "parent_code": null, "sort_order": 56320, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-08-01", @@ -21418,7 +20497,8 @@ "parent_code": "FP-08", "sort_order": 56576, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-08-01-01", @@ -21434,9 +20514,9 @@ "source_line": 3945, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -21445,15 +20525,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "600개 이하", - "1,300개 이하", - "2,000개 이하", - "2,700개 이하", - "3,400개 이하", - "4,100개 이하" - ], + "variant_key": [], "condition_note": [ "천공수", "소요인력(ha당)", @@ -21519,7 +20591,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-01-02", @@ -21546,12 +20619,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "300개 이하", - "900개 이하", - "1,500개 이하" - ], + "variant_key": [], "condition_note": [ "천공수", "소요인력", @@ -21593,7 +20661,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-02", @@ -21603,7 +20672,8 @@ "parent_code": "FP-08", "sort_order": 57344, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-08-02-01", @@ -21632,20 +20702,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10~12", - "14~16", - "18~20", - "22~24", - "26~28", - "30~32", - "34~36", - "38~40", - "42~44", - "46~48", - "50~52", - "54~56" - ], + "variant_key": [], "condition_note": [ "가슴높이 지름(㎝)", "천공수 (개)", @@ -21798,18 +20855,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10~12", - "14~16", - "18~20", - "22~24", - "26~28", - "30~32", - "34~36", - "38~40", - "42~44", - "46~48" - ], + "variant_key": [], "condition_note": [ "가슴높이 지름(㎝)", "천공수 (개)", @@ -21927,8 +20973,8 @@ "pum_table_id": "F0205", "section": "8-2-1. 소나무재선충병", "source_line": 4014, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 약제 이름·제형 목록", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -21940,14 +20986,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "아바멕틴", - "에마멕틴벤조에이트", - "아바멕틴(1.8) 설폭사플로르(4.2)", - "아세타미프리드(10) 에마멕틴벤조에이트(6)", - "아세타미프리드(8) 에마멕틴벤조에이트(2)", - "아바멕틴(1.6) 아세타미프리드(7)" - ], + "variant_key": [], "condition_note": [ "구 분", "제 형" @@ -21998,20 +21037,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10~12", - "14~16", - "18~20", - "22~24", - "26~28", - "30~32", - "34~36", - "38~40", - "42~44", - "46~48", - "50~52", - "54~56" - ], + "variant_key": [], "condition_note": [ "가슴높이 지름(㎝)", "천공수 (개)", @@ -22164,19 +21190,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10~16", - "18~22", - "24~26", - "28~32", - "34~36", - "38~42", - "44~46", - "48~52", - "54~56", - "58~62", - "64~66" - ], + "variant_key": [], "condition_note": [ "가슴높이지름(㎝)", "천공수 (개)", @@ -22300,7 +21314,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-02-02", @@ -22314,8 +21329,8 @@ "pum_table_id": "F0208", "section": "8-2-2. 솔잎혹파리", "source_line": 4063, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 선정약제 목록 + 원액 주입량(㎖/㎝) 기준 — 품 표는 따로 있음", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -22327,14 +21342,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "티아메톡삼 분산성액제 15%", - "디노테퓨란(15)․에마멕틴벤조에이트(4.5) 분산성액제 19.5%", - "이미다클로프리드 분산성액제 20%", - "아세타미프리드 액제 20%", - "디노테퓨란 액제 10%", - "아바멕틴(1.8)·설폭사플로르(4.2) 분산성액제 6%" - ], + "variant_key": [], "condition_note": [ "선정약제", "원액 주입량", @@ -22392,31 +21400,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -22583,31 +21567,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -22774,31 +21734,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -22946,7 +21882,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-02-03", @@ -22960,8 +21897,8 @@ "pum_table_id": "F0212", "section": "8-2-3. 솔껍질깍지벌레", "source_line": 4166, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 위와 같은 선정약제 목록", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -22973,13 +21910,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "디노테퓨란(15)․에마멕틴벤조에이트(4.5) 분산성액제 19.5%", - "이미다클로프리드 분산성액제 20%", - "티아메톡삼 분산성액제 15%", - "에마멕틴벤조에이트 유제 2.15%", - "아바멕틴(1.8)·설폭사플로르(4.2) 분산성액제 6%" - ], + "variant_key": [], "condition_note": [ "선정약제", "원액 주입량", @@ -23032,31 +21963,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -23223,31 +22130,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -23414,31 +22297,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -23603,32 +22462,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "사업 구분", - "1-4-1", - "6-4-4 뿌리제거", - "1-4-2", - "1-4-3", - "6-2-2 풀베기(줄베기)", - "6-4-1 덩굴걷기", - "1-4-4", - "1-4-5", - "5-5천연하종갱신", - "6-1 비료주기", - "숲가꾸기", - "4-2-1 단목베기", - "6-2-3 풀베기(모두베기)", - "6-4-2 덩굴약제 살포처리", - "6-5 어린나무가꾸기", - "7-2 수라집재", - "7-5-2 트랙터부착형집재(지면끌기)", - "7-9-2 부탁형 타워 (K-301, HAM300)", - "국유임산물 (수확)", - "7-6 스윙야더 집재", - "7-9-1 부탁형 타워 (K-301, HAM300)", - "산림병해충방제", - "7-1-2 인력집재" - ], + "variant_key": [], "condition_note": [ "번호", "할인․할증 요소", @@ -24273,8 +23107,8 @@ "pum_table_id": "F0453", "section": "8-2-3 굴착기(2025)」를 참조하여 적용계수를 달리 적용하도록 한다.", "source_line": 7907, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 〔부록 2〕 산림사업별 적용 가능 공종 목록 — 절 경계가 밀려 붙음", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -24286,32 +23120,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "공종", - "조림", - "5-32 대절작업", - "5-5 천연하종갱신", - "6-1 비료주기", - "지침 외", - "5-3-2 관목식재(단식)", - "10-13 드론운반공", - "숲가 꾸기", - "6-2-2 줄베기 (풀베기)", - "6-4-2 덩굴 약제 살포처리", - "4-2-1 단목베기", - "7-3아키야윈치 (임업용 윈치)", - "7-15-2 초소형포워더 운재", - "7-9-2 부착형타워 K-301, HAM300 (중형가선)", - "국유 임산물 수확", - "7-4-1 소형가선집재 (2드럼윈치)", - "7-8-3 타워야더 RME-300T", - "7-1-1 인력집재 (수확)", - "7-18 지조운반 (포워더 활용)", - "3-4-2 임산물 운반로 및 작업로 신설", - "산림 병해충 방제", - "8-1-2 약제주입병", - "8-2-5 약제주입기준 (푸사리움가지마름병)" - ], + "variant_key": [], "condition_note": [ "사업종", "구분", @@ -24899,32 +23708,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "위치 및 면적", - "구분", - "단 위 작 업 별", - "- 직접노무비", - "ㆍ벌채부산물 임내정리 (벌채와 동시 정리지역)", - "- 재료비(체인톱)", - "ㆍ보통휘발유(주연료)", - "ㆍ보통휘발유(잡품)", - "ㆍ체인오일(친환경)", - "- 기계경비(체인톱)", - "2. 식 재", - "ㆍ식재작업", - "ㆍ표시봉설치", - "ㆍ소운반", - "- 재료비", - "ㆍ묘목", - "ㆍ표시봉", - "- 경 비", - "ㆍ대운반", - "합 계", - "직접노무비", - "재 료 비", - "경비(기계경비)", - "<할인․할증률 적용>" - ], + "variant_key": [], "condition_note": [ "ha당 조림 단가산출서(예시)" ], @@ -25477,7 +24261,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-02-04", @@ -25491,8 +24276,8 @@ "pum_table_id": "F0216", "section": "8-2-4. 솔나방", "source_line": 4268, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 위와 같은 선정약제 목록", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -25504,9 +24289,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "아바멕틴(1.8)·설폭사플로르(4.2) 분산성액제 6%" - ], + "variant_key": [], "condition_note": [ "선정약제", "원액 주입량", @@ -25539,31 +24322,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -25711,7 +24470,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-02-05", @@ -25725,8 +24485,8 @@ "pum_table_id": "F0218", "section": "8-2-5. 푸사리움가지마름병", "source_line": 4309, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 위와 같은 선정약제 목록", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -25738,9 +24498,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "테부코나졸 유탁제 25%" - ], + "variant_key": [], "condition_note": [ "선정약제", "원액 주입량", @@ -25773,31 +24531,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "10∼12", - "14∼16", - "18∼20", - "22∼24", - "26∼28", - "30∼32", - "34∼36", - "38∼40", - "42∼44", - "46∼48", - "50∼52", - "54∼56", - "58∼60", - "62∼64", - "66∼68", - "70∼72", - "74∼76", - "78∼80", - "82∼84", - "86∼88", - "90∼92", - "94∼96", - "98∼100" - ], + "variant_key": [], "condition_note": [ "가슴높이지름 (㎝)", "천공수 (개)", @@ -25945,7 +24679,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-03", @@ -25959,8 +24694,8 @@ "pum_table_id": "F0220", "section": "8-3. 소각, 매몰, 훈증, 박피", "source_line": 4348, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "productivity", + "form_basis": "사람 판정 — 머리 「벌목조재 (㎥/인)·무더기 훈증 (RM/인)·그루터기 박피 (본/인)」 — 인당 작업량", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -25972,31 +24707,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "6", - "8", - "10", - "12", - "14", - "16", - "18", - "20", - "22", - "24", - "26", - "28", - "30", - "32", - "34", - "36", - "38", - "40", - "42", - "44", - "46", - "48", - "50이상" - ], + "variant_key": [], "condition_note": [ "경급별 (㎝)", "벌목조재 (㎥/인)", @@ -26268,8 +24979,8 @@ "pum_table_id": "F0221", "section": "8-3. 소각, 매몰, 훈증, 박피", "source_line": 4386, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 본문 「(단위: %)」 — 수종·직경별 지조율", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -26281,22 +24992,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "4cm", - "6", - "18", - "10", - "12", - "14", - "16", - "20", - "22", - "24", - "26", - "28", - "30", - "평균" - ], + "variant_key": [], "condition_note": [ "수종 가슴 높이 지름", "소나무", @@ -26464,8 +25160,8 @@ "pum_table_id": "F0222", "section": "8-3. 소각, 매몰, 훈증, 박피", "source_line": 4406, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 머리 「비율(수간재적의 %)」 — 근주재적 산정율", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -26477,9 +25173,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "중부지방소나무" - ], + "variant_key": [], "condition_note": [ "기 준", "적용대상 수종", @@ -26495,7 +25189,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-04", @@ -26524,11 +25219,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "8롤 미만", - "롤트랩 설치", - "롤트랩 제거" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -26566,7 +25257,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-05", @@ -26593,11 +25285,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "트랩설치", - "트랩통수거 및 교체", - "트랩철거" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력(조/ha당)", @@ -26621,7 +25309,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-06", @@ -26631,7 +25320,8 @@ "parent_code": "FP-08", "sort_order": 59648, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-08-06-01", @@ -26659,13 +25349,7 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "특별인부", "소형헬기 (160ha당)", - "사전조사", - "헬기장정리", - "깃발설치", - "양수기설치", - "약제조제", "대형헬기 (400ha당)" ], "condition_note": [ @@ -26771,16 +25455,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "산림항공방제", - "유인헬기", - "16배", - "30배", - "50배", - "60배", - "80배", - "100배" - ], + "variant_key": [], "condition_note": [ "구 분", "희석배수", @@ -26841,8 +25516,8 @@ "pum_table_id": "F0227", "section": "8-6-1. 유인헬기방제", "source_line": 4485, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 기종별 희석배수별 약제량(L, 1회) — 살포 계산표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -26854,17 +25529,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소형헬기 (AS350)", - "16배", - "30배", - "50배", - "60배", - "80배", - "100배", - "소형헬기 (Bell206)", - "대형헬기 (KA-32)" - ], + "variant_key": [], "condition_note": [ "구 분", "물탱크 용량", @@ -27022,6 +25687,10 @@ ] ] } + ], + "variant_keys": [ + "소형헬기 (160ha당)", + "대형헬기 (400ha당)" ] }, { @@ -27049,15 +25718,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "드론조종자 (건설기계 조종원)", - "무인헬기 (ha당)", - "이착륙장 정리(1개소/4ha)", - "취수 및 약제조제", - "약제살포", - "주유 및 약제충전 기체정비", - "비행준비" - ], + "variant_key": [], "condition_note": [ "구 분", "항 목", @@ -27139,15 +25800,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "드론조종자 (건설기계 조종원)", - "멀티콥터 (ha당)", - "이착륙장 정리(1개소/4ha)", - "취수 및 약제조제", - "약제살포", - "기체 및 약제충전 기체정비", - "비행준비" - ], + "variant_key": [], "condition_note": [ "구 분", "항 목", @@ -27212,7 +25865,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-06-03", @@ -27240,11 +25894,7 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "특별인부", - "차량살포 (동력분무기)", - "사전조사", - "운전", - "물주입, 살포" + "차량살포 (동력분무기)" ], "condition_note": [ "구 분", @@ -27288,8 +25938,8 @@ "pum_table_id": "F0231", "section": "8-6-3. 지상방제", "source_line": 4569, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 희석배수별 약제소요량(㎖) — 물의 양 대비 계산표", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -27301,17 +25951,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "250배", - "500배", - "1,000배", - "1,500배", - "2,000배", - "2,500배", - "3,000배", - "4,000배", - "5,000배" - ], + "variant_key": [], "condition_note": [ "물의양 희석 배수", "10ℓ (0.5말)", @@ -27425,6 +26065,9 @@ ] ] } + ], + "variant_keys": [ + "차량살포 (동력분무기)" ] }, { @@ -27452,9 +26095,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "방제실행 등록" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력(인)", @@ -27468,7 +26109,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-08", @@ -27478,7 +26120,8 @@ "parent_code": "FP-08", "sort_order": 60928, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-08-08-01", @@ -27507,12 +26150,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10m이하", - "1.0RM 이하", - "1.1~1.5RM", - "1.6~2.0RM" - ], + "variant_key": [], "condition_note": [ "층적부피", "수집거리", @@ -27557,7 +26195,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-08-02", @@ -27586,11 +26225,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "10개 이하", - "굴 삭 기 우드그랩", - "인 력" - ], + "variant_key": [], "condition_note": [ "구 분", "㏊당 훈증더미 갯수", @@ -27623,7 +26258,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-09", @@ -27650,9 +26286,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "잔가지 줍기" - ], + "variant_key": [], "condition_note": [ "구 분", "소요인력(인/본당)", @@ -27666,7 +26300,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-08-10", @@ -27694,10 +26329,7 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "벌목부", "1㎥", - "벌목조재", - "피복작업", "2㎥" ], "condition_note": [ @@ -27750,6 +26382,10 @@ ] ] } + ], + "variant_keys": [ + "1㎥", + "2㎥" ] }, { @@ -27777,9 +26413,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "이동식 임목 파쇄기" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -27795,7 +26429,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09", @@ -27805,7 +26440,8 @@ "parent_code": null, "sort_order": 62464, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-01", @@ -27814,7 +26450,8 @@ "level": 2, "parent_code": "FP-09", "sort_order": 62720, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-09-02", @@ -27841,9 +26478,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부" - ], + "variant_key": [], "condition_note": [ "구 분", "횡단경사 60% 미만", @@ -27857,7 +26492,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-03", @@ -27867,7 +26503,8 @@ "parent_code": "FP-09", "sort_order": 63232, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-03-01", @@ -27894,9 +26531,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "종류 직종", "보통토사", @@ -27914,7 +26549,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-03-02", @@ -27950,12 +26586,7 @@ "∼(U+223C)" ], "capacity_formula_here": true, - "variant_key": [ - "K", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -27984,7 +26615,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-04", @@ -28005,7 +26637,13 @@ "weight": "1" } ], - "steps_basis": "품셈 9-4 암절취 = 9-4-1 암파쇄 + 9-4-2 집토 — 같은 ㎥ 를 깨고 모음(지식DB 토공_수량 「암절취 (암파쇄ㆍ집토) | 9-4」)" + "steps_basis": "품셈 9-4 암절취 = 9-4-1 암파쇄 + 9-4-2 집토 — 같은 ㎥ 를 깨고 모음(지식DB 토공_수량 「암절취 (암파쇄ㆍ집토) | 9-4」)", + "variant_keys": [ + "연암", + "보통암", + "경암", + "평균" + ] }, { "work_item_code": "FP-09-04-01", @@ -28033,9 +26671,9 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "대형브레이커+ 유압식백호우 (무한궤도,0.7㎥)", + "연암", "보통암", - "경 암" + "경암" ], "condition_note": [ "적용기계", @@ -28068,6 +26706,12 @@ ] ] } + ], + "variant_keys": [ + "연암", + "보통암", + "경암", + "평균" ] }, { @@ -28102,12 +26746,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "K", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -28136,7 +26775,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-05", @@ -28161,7 +26801,12 @@ "weight": "1" } ], - "steps_basis": "품셈 9-5 발파암 — 절 제목 「9-5-1 육상, 암석 절취(발파 10%)」·「9-5-2 깎기(90%)」·「9-5-3 집토」(지식DB 토공_수량 「발파암 (절취 10 %ㆍ깎기 90 %ㆍ집토) | 9-5」)" + "steps_basis": "품셈 9-5 발파암 — 절 제목 「9-5-1 육상, 암석 절취(발파 10%)」·「9-5-2 깎기(90%)」·「9-5-3 집토」(지식DB 토공_수량 「발파암 (절취 10 %ㆍ깎기 90 %ㆍ집토) | 9-5」)", + "variant_keys": [ + "연암", + "보통암", + "경암" + ] }, { "work_item_code": "FP-09-05-01", @@ -28192,16 +26837,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "뇌 관", - "비 트", - "인력", - "착 암 공", - "보통인부", - "장비", - "공기압축기" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -28276,7 +26912,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-05-02", @@ -28304,9 +26941,9 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "연 암", + "연암", "보통암", - "경 암" + "경암" ], "condition_note": [ "암의 종류", @@ -28335,6 +26972,11 @@ ] ] } + ], + "variant_keys": [ + "연암", + "보통암", + "경암" ] }, { @@ -28369,12 +27011,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "K", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -28403,7 +27040,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-06", @@ -28413,7 +27051,8 @@ "parent_code": "FP-09", "sort_order": 65792, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-06-01", @@ -28442,9 +27081,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "대형브레이커+유압식백호우 (무한궤도,0.7㎥)" - ], + "variant_key": [], "condition_note": [ "적용기계", "작업능력(㎥/hr)", @@ -28458,7 +27095,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-07", @@ -28468,7 +27106,8 @@ "parent_code": "FP-09", "sort_order": 66304, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-07-01", @@ -28497,9 +27136,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "무근콘크리트" - ], + "variant_key": [], "condition_note": [ "구 분", "평균두께", @@ -28517,7 +27154,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-07-02", @@ -28546,9 +27184,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "무근콘크리트" - ], + "variant_key": [], "condition_note": [ "구 분", "평균두께", @@ -28566,7 +27202,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-08", @@ -28576,7 +27213,8 @@ "parent_code": "FP-09", "sort_order": 67072, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-08-01", @@ -28605,9 +27243,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "철근콘크리트" - ], + "variant_key": [], "condition_note": [ "구 분", "평균두께", @@ -28642,12 +27278,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력", - "보통인부", - "자재", - "산소" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -28685,7 +27316,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-08-02", @@ -28714,9 +27346,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "철근콘크리트" - ], + "variant_key": [], "condition_note": [ "구 분", "평균두께", @@ -28734,7 +27364,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-09", @@ -28761,11 +27392,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "메쌓기", - "뒷길이60㎝이상", - "찰쌓기" - ], + "variant_key": [], "condition_note": [ "구 분", "평균뒷길이", @@ -28797,7 +27424,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-10", @@ -28807,7 +27435,8 @@ "parent_code": "FP-09", "sort_order": 68096, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-10-01", @@ -28836,9 +27465,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "콘크리트" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -28854,7 +27481,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-10-02", @@ -28881,9 +27509,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "아스팔트" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -28899,7 +27525,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-11", @@ -28909,7 +27536,8 @@ "parent_code": "FP-09", "sort_order": 68864, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-11-01", @@ -28938,14 +27566,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력", - "보통인부", - "장비", - "자재", - "잡자재", - "경비" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -29004,7 +27625,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-12", @@ -29014,7 +27636,8 @@ "parent_code": "FP-09", "sort_order": 69376, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-12-01", @@ -29053,13 +27676,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력(10%)", - "장비(90%)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29103,7 +27720,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-12-02", @@ -29141,16 +27759,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모(본/hr)", - "유압식백호우 (무한궤도,0.7㎥)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29207,7 +27816,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-12-03", @@ -29245,16 +27855,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "유압식백호우 (무한궤도,0.7㎥)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29311,7 +27912,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13", @@ -29321,7 +27923,8 @@ "parent_code": "FP-09", "sort_order": 70400, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-13-01", @@ -29359,13 +27962,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29409,7 +28006,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-02", @@ -29443,10 +28041,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29467,7 +28062,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-03", @@ -29501,10 +28097,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29525,7 +28118,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-04", @@ -29564,13 +28158,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29614,7 +28202,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-05", @@ -29647,10 +28236,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29671,7 +28257,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-06", @@ -29704,10 +28291,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29728,7 +28312,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-07", @@ -29765,16 +28350,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29839,7 +28415,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-08", @@ -29871,13 +28448,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -29921,7 +28492,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-09", @@ -29953,13 +28525,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30003,7 +28569,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-10", @@ -30042,15 +28609,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30108,7 +28667,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-11", @@ -30141,12 +28701,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30183,7 +28738,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-12", @@ -30216,12 +28772,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30258,7 +28809,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-13", @@ -30297,16 +28849,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30371,7 +28914,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-14", @@ -30403,13 +28947,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30453,7 +28991,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-15", @@ -30485,13 +29024,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30535,7 +29068,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-16", @@ -30572,16 +29106,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30646,7 +29171,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-17", @@ -30679,13 +29205,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30729,7 +29249,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-13-18", @@ -30762,13 +29283,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "치즐소모량(본/hr)", - "들어 내기" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30812,7 +29327,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-14", @@ -30822,7 +29338,8 @@ "parent_code": "FP-09", "sort_order": 75264, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-14-01", @@ -30857,13 +29374,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력 (10%)", - "장비 (90%)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -30907,7 +29418,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-14-02", @@ -30939,14 +29451,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "A", - "N", - "H", - "f", - "E", - "P" - ], + "variant_key": [], "condition_note": [ "적 용", "비 고" @@ -30984,7 +29489,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-15", @@ -30994,7 +29500,8 @@ "parent_code": "FP-09", "sort_order": 76032, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-15-01", @@ -31008,8 +29515,8 @@ "pum_table_id": "F0281", "section": "9-15-1. 답(畓)구간", "source_line": 5273, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 K·f·E·Cm — [주]② Q1=3600×q×K×f×E/Cm", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -31023,12 +29530,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "K(버킷계수)", - "f(토량환산계수)", - "E(작업효율)", - "㎝(1회 사이클시간)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31057,7 +29559,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-15-02", @@ -31071,8 +29574,8 @@ "pum_table_id": "F0282", "section": "9-15-2. 답(畓)외구간", "source_line": 5290, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 T·L·E·q — 불도저 작업량 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -31088,18 +29591,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "T(표토두께)", - "L(운반거리)", - "E(작업효율)", - "q(삽날의 용량)", - "q0(거리를 고려하지 않은 삽날의 용량)", - "e(운반거리계수)", - "f(토량환산계수)", - "V1(전진속도)", - "V2(후진속도)", - "t(기어변속시간)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31158,7 +29650,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-16", @@ -31168,7 +29661,8 @@ "parent_code": "FP-09", "sort_order": 76800, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-16-01", @@ -31200,12 +29694,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "K", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31234,7 +29723,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-16-02", @@ -31248,8 +29738,8 @@ "pum_table_id": "F0284", "section": "9-16-2. 노체다짐", "source_line": 5326, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 V·W·E·D — [주]② Q=1000×V×W×E×D×f/N", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -31261,14 +29751,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "V(다짐속도,km/hr)", - "W(롤러 유효폭,m)", - "E(작업효율)", - "D(펴는 흙의 두께,m)", - "f(토량환산계수)", - "N(소요다짐횟수)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31307,7 +29790,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-16-03", @@ -31321,8 +29805,8 @@ "pum_table_id": "F0285", "section": "9-16-3. 살수", "source_line": 5344, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 흡입준비·운반·흡입·대기 시간 — 살수차 사이클 시간", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -31336,13 +29820,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "흡입준비(t1)", - "운반(t2)", - "흡입(t3)", - "대기((t4)", - "살수(t5)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31376,7 +29854,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-17", @@ -31386,7 +29865,8 @@ "parent_code": "FP-09", "sort_order": 77824, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-17-01", @@ -31395,7 +29875,8 @@ "level": 3, "parent_code": "FP-09-17", "sort_order": 78080, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-09-17-02", @@ -31427,16 +29908,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "L", - "E", - "q0", - "e", - "f", - "V1", - "V2", - "t" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -31485,7 +29957,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-18", @@ -31499,8 +29972,8 @@ "pum_table_id": "F0287", "section": "9-18. 층따기", "source_line": 5395, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 K·f·E·Cm — [주]① Q1=3600×q×K×f×E/Cm", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -31519,12 +29992,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "굴착기 (무한궤도, 0.7㎥)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "적용장비", "적 용", @@ -31557,7 +30025,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-19", @@ -31567,7 +30036,8 @@ "parent_code": "FP-09", "sort_order": 78848, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-19-01", @@ -31594,15 +30064,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부 (인)", - "모래ㆍ사질토ㆍ점토ㆍ점질토", - "연질토ㆍ불순자갈", - "호박돌 섞인 고결토ㆍ경질토", - "풍화암", - "연암", - "보통암ㆍ경암" - ], + "variant_key": [], "condition_note": [ "토 질 별", "구 분" @@ -31676,11 +30138,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력시공", - "모래 또는 사질토", - "기계시공" - ], + "variant_key": [], "condition_note": [ "시 공", "토 질", @@ -31716,7 +30174,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-19-02", @@ -31743,10 +30202,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력", - "장비" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -31770,7 +30226,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-19-03", @@ -31799,12 +30256,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인력", - "장비", - "소형브레이커", - "어어호스(3/4인치)" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -31842,7 +30294,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-20", @@ -31852,7 +30305,8 @@ "parent_code": "FP-09", "sort_order": 79872, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-09-20-01", @@ -31879,12 +30333,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "보통인부", - "굴착기(무한궤도)", - "제잡비율" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -31918,7 +30367,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-20-02", @@ -31945,10 +30395,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "굴착기(무한궤도)" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -31970,7 +30417,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-09-21", @@ -31998,9 +30446,12 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "굴착기 (무한궤도)", - "보통인부", - "굴착기(무한궤도,0.7㎥)" + "굴착기(무한궤도) 0.2 · 소", + "굴착기(무한궤도) 0.2 · 중", + "굴착기(무한궤도) 0.2 · 밀", + "굴착기(무한궤도) 0.7 · 소", + "굴착기(무한궤도) 0.7 · 중", + "굴착기(무한궤도) 0.7 · 밀" ], "condition_note": [ "종 류", @@ -32045,6 +30496,14 @@ ] ] } + ], + "variant_keys": [ + "굴착기(무한궤도) 0.2 · 소", + "굴착기(무한궤도) 0.2 · 중", + "굴착기(무한궤도) 0.2 · 밀", + "굴착기(무한궤도) 0.7 · 소", + "굴착기(무한궤도) 0.7 · 중", + "굴착기(무한궤도) 0.7 · 밀" ] }, { @@ -32059,8 +30518,8 @@ "pum_table_id": "F0295", "section": "9-22. 섞기", "source_line": 5531, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 K·f·E·Cm — [주]③ Q1=3600×q×K×f×E/Cm", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -32074,12 +30533,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "K(버킷계수)", - "f(토량환산계수)", - "E(작업효율)", - "Cm(1회 사이클시간)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32108,7 +30562,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10", @@ -32118,7 +30573,8 @@ "parent_code": null, "sort_order": 81152, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-01", @@ -32145,10 +30601,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "운반비", - "하차비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32167,7 +30620,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-02", @@ -32194,10 +30648,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "운반비", - "하차비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32216,7 +30667,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-03", @@ -32226,7 +30678,8 @@ "parent_code": "FP-10", "sort_order": 81920, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-03-01", @@ -32253,10 +30706,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구 입", - "운 반" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32275,7 +30725,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-03-02", @@ -32302,10 +30753,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "골재생산", - "운반" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32324,7 +30772,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-03-03", @@ -32338,8 +30787,8 @@ "pum_table_id": "F0300", "section": "10-3-3. 혼합골재", "source_line": 5600, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 「운반비·하차비」 적용 항목 — 값이 없는 구성 안내", "basis_quantity": 1.0, "basis_unit": "㎥", "basis_source": "본문", @@ -32351,10 +30800,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "운반비", - "하차비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32373,7 +30819,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-04", @@ -32387,8 +30834,8 @@ "pum_table_id": "F0301", "section": "10-4. 중기운반", "source_line": 5611, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — t1·t2·t3·t4 — 트레일러 운반 사이클 시간", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -32402,21 +30849,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "운반", - "t2=운반시간 참조", - "t3=20min", - "t4=0.42min", - "㎝=t1+t2+t3+t4", - "N=60×0.9/㎝", - "To=㎝-(t1+t3)", - "트럭운반 (10.5TON)", - "t3=10min", - "t4=5min", - "자주식 운반", - "콘크리트 믹서(6.0㎥)", - "크레인(트럭 10TON)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -32521,7 +30954,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-05", @@ -32530,7 +30964,8 @@ "level": 2, "parent_code": "FP-10", "sort_order": 83200, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-10-06", @@ -32540,7 +30975,8 @@ "parent_code": "FP-10", "sort_order": 83456, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-06-01", @@ -32567,13 +31003,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "토사", - "20", - "30", - "40", - "50" - ], + "variant_key": [], "condition_note": [ "거리 (m)", "보통인부(인)", @@ -32629,7 +31059,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-06-02", @@ -32656,9 +31087,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "거리(m)", "50", @@ -32692,7 +31121,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-06-03", @@ -32719,11 +31149,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "소 재 (조재목)", - "m", - "20 40 60 80 100 120 140 160 180 200" - ], + "variant_key": [], "condition_note": [ "종 별 거 리", "목 재", @@ -32782,7 +31208,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-07", @@ -32792,7 +31219,8 @@ "parent_code": "FP-10", "sort_order": 84480, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-07-01", @@ -32819,10 +31247,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장(인)", - "특별인부(인)" - ], + "variant_key": [], "condition_note": [ "경사도 구분", "30도 미만", @@ -32844,7 +31269,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-07-02", @@ -32871,11 +31297,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장(인)", - "특별인부(인)", - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "경사도 구분", "30도 미만", @@ -32903,7 +31325,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-07-03", @@ -32930,11 +31353,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장(인)", - "특별인부(인)", - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "경사도 구분", "30도 미만", @@ -32962,7 +31381,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-07-04", @@ -32976,8 +31396,8 @@ "pum_table_id": "F0308", "section": "10-7-4. 모노레일 운반", "source_line": 5741, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 1사이클당 운반량 q — 모노레일 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -32989,10 +31409,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "차량구분", - "단궤도" - ], + "variant_key": [], "condition_note": [ "구 분", "콘크리트", @@ -33018,8 +31435,8 @@ "pum_table_id": "F0309", "section": "10-7-4. 모노레일 운반", "source_line": 5755, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 본문 「(단위: 분/1회당)」 — 적재·짐부리기 시간", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -33031,9 +31448,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "시간(분)" - ], + "variant_key": [], "condition_note": [ "구 분", "콘크리트", @@ -33066,9 +31481,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "직 종", "콘크리트", @@ -33101,16 +31514,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "연장", - "작업반장", - "특별인부", - "보통인부", - "모노레일 본기계", - "차체를 받치고 있는 부분 (차바퀴, 용수철, 브레이크 등)", - "레일·지지대", - "기타비용" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -33196,10 +31600,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "연료비", - "보통인부" - ], + "variant_key": [], "condition_note": [ "명 칭", "단 위", @@ -33221,7 +31622,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-08", @@ -33231,7 +31633,8 @@ "parent_code": "FP-10", "sort_order": 85760, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-08-01", @@ -33260,12 +31663,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "짐내리는 인부", - "콘크리트", - "제 자재", - "〃" - ], + "variant_key": [], "condition_note": [ "구 분", "운반기구", @@ -33310,7 +31708,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-08-02", @@ -33339,9 +31738,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "운반대" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -33361,7 +31758,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-08-03", @@ -33388,12 +31786,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "보통인부", - "연료비", - "운반기구 손료" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -33432,7 +31825,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-09", @@ -33442,7 +31836,8 @@ "parent_code": "FP-10", "sort_order": 86784, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-09-01", @@ -33456,8 +31851,8 @@ "pum_table_id": "F0316", "section": "10-9-1. 자재의 1회당 표준 운반량", "source_line": 5861, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 규격별 1회당 표준 운반량(kg·개·㎡) — 적재 기준", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -33469,15 +31864,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트 블 록", - "목제형틀", - "철제형틀", - "토사", - "자갈", - "강재", - "통나무" - ], + "variant_key": [], "condition_note": [ "규격 구분", "1t미만", @@ -33553,7 +31940,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-10", @@ -33563,7 +31951,8 @@ "parent_code": "FP-10", "sort_order": 87296, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-10-01", @@ -33590,11 +31979,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장", - "보통인부", - "버켓 손료" - ], + "variant_key": [], "condition_note": [ "명 칭", "형태ㆍ치수", @@ -33626,7 +32011,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-10-02", @@ -33653,11 +32039,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장", - "보통인부", - "와이어 손료" - ], + "variant_key": [], "condition_note": [ "명 칭", "형태ㆍ치수", @@ -33689,7 +32071,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-11", @@ -33703,8 +32086,8 @@ "pum_table_id": "F0319", "section": "10-11. 불도저 운반", "source_line": 5902, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 L·E·q0 — [주]② q=3.2×e, Cm 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -33724,17 +32107,9 @@ "special_glyphs": [], "capacity_formula_here": true, "variant_key": [ - "L", - "E", - "암석", - "q0", - "e", - "f", + "토사", "파쇄암", - "발파암", - "V1", - "V2", - "t" + "발파암" ], "condition_note": [ "구 분", @@ -33810,6 +32185,11 @@ ] ] } + ], + "variant_keys": [ + "토사", + "파쇄암", + "발파암" ] }, { @@ -33820,7 +32200,8 @@ "parent_code": "FP-10", "sort_order": 88320, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-12-01", @@ -33834,8 +32215,8 @@ "pum_table_id": "F0320", "section": "10-12-1. 토사", "source_line": 5935, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 K·E0·Cm — 굴착기 적재 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -33849,12 +32230,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "K", - "E0", - "파쇄암", - "㎝(초)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -33891,8 +32267,8 @@ "pum_table_id": "F0321", "section": "10-12-1. 토사", "source_line": 5958, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 적재·t2·적하 시간 — 덤프트럭 사이클 공식 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -33904,14 +32280,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "적재(t1)", - "t2", - "V2(공차)", - "적하(t3)", - "대기(t4)", - "덮개(t5)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -33956,7 +32325,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-12-02", @@ -33965,7 +32335,8 @@ "level": 3, "parent_code": "FP-10-12", "sort_order": 88832, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-10-12-03", @@ -33974,7 +32345,8 @@ "level": 3, "parent_code": "FP-10-12", "sort_order": 89088, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-10-13", @@ -33984,7 +32356,8 @@ "parent_code": "FP-10", "sort_order": 89344, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-10-13-01", @@ -34011,9 +32384,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인원(명)" - ], + "variant_key": [], "condition_note": [ "수평거리", "100m", @@ -34035,7 +32406,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-10-13-02", @@ -34062,9 +32434,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인원(명)" - ], + "variant_key": [], "condition_note": [ "수평거리", "100m", @@ -34090,8 +32460,8 @@ "pum_table_id": "F0324", "section": "10-13-2. 인력운반", "source_line": 6013, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 드론운반 단가산출 항목(준비시간·왕복비행시간 식) — [주] 일본 산출 예", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -34105,12 +32475,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "① 드론준비시간(분)", - "② 왕복비행시간(분)", - "③ 1회 비행 운반 본수", - "④ 드론운반 작업인원" - ], + "variant_key": [], "condition_note": [ "항 목", "내 용", @@ -34139,7 +32504,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-11", @@ -34149,7 +32515,8 @@ "parent_code": null, "sort_order": 90112, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-11-01", @@ -34177,8 +32544,26 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "비계공", - "2.4M 3.0M 3.5M 4.8M 6.0M" + "길이 3M · 폭 2.4M", + "길이 3M · 폭 3.0M", + "길이 3M · 폭 3.5M", + "길이 3M · 폭 4.8M", + "길이 3M · 폭 6.0M", + "길이 6M · 폭 2.4M", + "길이 6M · 폭 3.0M", + "길이 6M · 폭 3.5M", + "길이 6M · 폭 4.8M", + "길이 6M · 폭 6.0M", + "길이 9M · 폭 2.4M", + "길이 9M · 폭 3.0M", + "길이 9M · 폭 3.5M", + "길이 9M · 폭 4.8M", + "길이 9M · 폭 6.0M", + "길이 12M · 폭 2.4M", + "길이 12M · 폭 3.0M", + "길이 12M · 폭 3.5M", + "길이 12M · 폭 4.8M", + "길이 12M · 폭 6.0M" ], "condition_note": [ "길이 폭", @@ -34215,6 +32600,28 @@ ] ] } + ], + "variant_keys": [ + "길이 3M · 폭 2.4M", + "길이 3M · 폭 3.0M", + "길이 3M · 폭 3.5M", + "길이 3M · 폭 4.8M", + "길이 3M · 폭 6.0M", + "길이 6M · 폭 2.4M", + "길이 6M · 폭 3.0M", + "길이 6M · 폭 3.5M", + "길이 6M · 폭 4.8M", + "길이 6M · 폭 6.0M", + "길이 9M · 폭 2.4M", + "길이 9M · 폭 3.0M", + "길이 9M · 폭 3.5M", + "길이 9M · 폭 4.8M", + "길이 9M · 폭 6.0M", + "길이 12M · 폭 2.4M", + "길이 12M · 폭 3.0M", + "길이 12M · 폭 3.5M", + "길이 12M · 폭 4.8M", + "길이 12M · 폭 6.0M" ] }, { @@ -34242,10 +32649,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "건축목공", - "보통인부" - ], + "variant_key": [], "condition_note": [ "종 류", "단 위", @@ -34264,7 +32668,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-11-03", @@ -34291,10 +32696,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "건축목공", - "보통인부" - ], + "variant_key": [], "condition_note": [ "종 류", "단 위", @@ -34313,7 +32715,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-11-04", @@ -34327,8 +32730,8 @@ "pum_table_id": "F0328", "section": "11-4. 쇄석·혼합석 부설", "source_line": 6071, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 행이 q·k·f·E — [주]① Q=3600×q×k×f×E/Cm", "basis_quantity": 1.0, "basis_unit": "㎥", "basis_source": "본문", @@ -34346,13 +32749,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "부설장비", - "k", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -34396,7 +32793,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12", @@ -34406,7 +32804,8 @@ "parent_code": null, "sort_order": 91392, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-01", @@ -34416,7 +32815,8 @@ "parent_code": "FP-12", "sort_order": 91648, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-01-01", @@ -34471,6 +32871,11 @@ ] ] } + ], + "variant_keys": [ + "무근구조물", + "철근구조물", + "소형구조물" ] }, { @@ -34526,6 +32931,11 @@ ] ] } + ], + "variant_keys": [ + "무근구조물", + "철근구조물", + "소형구조물" ] }, { @@ -34598,12 +33008,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "25", - "(B)", - "(C)", - "40" - ], + "variant_key": [], "condition_note": [ "골재의 최대치수(㎜)", "배합종류", @@ -34673,12 +33078,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "25", - "(B)", - "(C)", - "40" - ], + "variant_key": [], "condition_note": [ "온도 품종", "00C때", @@ -34731,6 +33131,11 @@ ] ] } + ], + "variant_keys": [ + "무근구조물", + "철근구조물", + "소형구조물" ] }, { @@ -34760,9 +33165,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "미 장 공" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -34795,12 +33198,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "무근콘크리트", - "8 ~ 12 cm", - "15 cm", - "18 cm 이상" - ], + "variant_key": [], "condition_note": [ "슬 럼 프", "기준 시공량" @@ -34845,9 +33243,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "f₁" - ], + "variant_key": [], "condition_note": [ "유 형", "Type-Ⅰ", @@ -34869,8 +33265,8 @@ "pum_table_id": "F0358", "section": "12-2 표면 마무리를 따른다.", "source_line": 6538, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -34882,12 +33278,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "Type-Ⅰ", - "Type-Ⅱ", - "Type-Ⅲ", - "Type-Ⅳ" - ], + "variant_key": [], "condition_note": [ "구 분", "적용 기준" @@ -34928,9 +33319,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "f₁" - ], + "variant_key": [], "condition_note": [ "유 형", "Type-Ⅰ", @@ -34950,8 +33339,8 @@ "pum_table_id": "F0360", "section": "12-2 표면 마무리를 따른다.", "source_line": 6551, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -34963,11 +33352,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "Type-Ⅰ", - "Type-Ⅱ", - "Type-Ⅲ" - ], + "variant_key": [], "condition_note": [ "구 분", "적용 기준" @@ -35004,12 +33389,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "시멘트(kg)", - "1:2:4", - "1:3:6", - "1:4:8" - ], + "variant_key": [], "condition_note": [ "배 합 비", "재 료", @@ -35069,7 +33449,7 @@ "capacity_formula_here": false, "variant_key": [ "설 치", - "비 계 공" + "철 거" ], "condition_note": [ "구 분", @@ -35094,6 +33474,10 @@ ] ] } + ], + "variant_keys": [ + "설 치", + "철 거" ] }, { @@ -35122,10 +33506,9 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "철근공(인)", - "간 단", - "보 통", - "복 잡", + "간 단", + "보 통", + "복 잡", "매우복잡" ], "condition_note": [ @@ -35182,6 +33565,12 @@ ] ] } + ], + "variant_keys": [ + "간 단", + "보 통", + "복 잡", + "매우복잡" ] }, { @@ -35214,18 +33603,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "횟수별", - "합 판", - "각 재", - "철 선", - "못", - "박 리 제", - "형틀목공", - "보통인부", - "사용고재 평가기준", - "비 고" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -35326,7 +33704,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-05", @@ -35353,11 +33732,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "문양 스티로폴(자재비 포함)", - "설치 및 해체", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -35388,7 +33763,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-06", @@ -35416,10 +33792,8 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "콘크리트믹서트럭 직접타설인경우", - "포장공", + "20㎝", "30㎝", - "보통인부", "40㎝" ], "condition_note": [ @@ -35465,6 +33839,11 @@ ] ] } + ], + "variant_keys": [ + "20㎝", + "30㎝", + "40㎝" ] }, { @@ -35475,7 +33854,8 @@ "parent_code": "FP-12", "sort_order": 93952, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-07-01", @@ -35504,11 +33884,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "명칭", - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "배치인원(인)", "사용기계(1대)", @@ -35541,7 +33917,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-07-02", @@ -35568,10 +33945,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "배치인원(인)", "시공량(m)" @@ -35589,7 +33963,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-08", @@ -35616,13 +33991,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "포장두께(㎝)", - "형틀목공 보통인부", - "20㎝ ≤ 포장두께 ≤ 25㎝", - "25㎝ ≤ 포장두께 ≤ 30㎝", - "30㎝ ≤ 포장두께 ≤ 40㎝" - ], + "variant_key": [], "condition_note": [ "배치인원(인)", "시공량(거푸집연장 m)" @@ -35660,7 +34029,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-09", @@ -35670,7 +34040,8 @@ "parent_code": "FP-12", "sort_order": 94976, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-09-01", @@ -35697,15 +34068,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트(레미콘)", - "거 푸 집", - "연결철근", - "수축줄눈", - "신축이음", - "비닐깔기", - "배수파이프 설치" - ], + "variant_key": [], "condition_note": [ "구 분", "규격", @@ -35765,7 +34128,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-09-02", @@ -35792,14 +34156,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트", - "거 푸 집", - "철근가공조립", - "수축줄눈", - "신축이음", - "비닐깔기" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -35852,7 +34209,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-09-03", @@ -35879,10 +34237,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트", - "거 푸 집" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -35907,7 +34262,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-10", @@ -35936,16 +34292,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "유공관 설치", - "배관공", - "특별인부", - "부직포", - "보통인부", - "잡석", - "소운반 인부", - "토공" - ], + "variant_key": [], "condition_note": [ "구 분", "규격", @@ -36028,7 +34375,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-11", @@ -36038,7 +34386,8 @@ "parent_code": "FP-12", "sort_order": 96256, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-11-01", @@ -36077,14 +34426,8 @@ "capacity_formula_here": false, "variant_key": [ "∅800mm", - "VR관", - "고무링", - "지수활제", - "기초콘크리트", - "거 푸 집", - "크레인", - "배관공", - "보통인부" + "∅1000mm", + "∅1200mm" ], "condition_note": [ "구 분", @@ -36177,6 +34520,11 @@ ] ] } + ], + "variant_keys": [ + "∅800mm", + "∅1000mm", + "∅1200mm" ] }, { @@ -36221,13 +34569,8 @@ "capacity_formula_here": false, "variant_key": [ "∅800mm", - "흄 관", - "기초콘크리트", - "거 푸 집", - "크레인", - "배관공", - "보통인부", - "접합몰탈" + "∅1000mm", + "∅1200mm" ], "condition_note": [ "구 분", @@ -36328,12 +34671,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "절단기", - "일반기계운전사", - "보통인부", - "잡재료비" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -36372,6 +34710,11 @@ ] ] } + ], + "variant_keys": [ + "∅800mm", + "∅1000mm", + "∅1200mm" ] }, { @@ -36411,12 +34754,8 @@ "capacity_formula_here": false, "variant_key": [ "∅800mm", - "파형강관", - "커플링밴드", - "크레인", - "배관공", - "보통인부", - "모래부설" + "∅1000mm", + "∅1200mm" ], "condition_note": [ "구 분", @@ -36491,6 +34830,11 @@ ] ] } + ], + "variant_keys": [ + "∅800mm", + "∅1000mm", + "∅1200mm" ] }, { @@ -36518,18 +34862,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트 (레미콘)", - "다짐:봉상후렉시블(45mm)", - "거 푸 집", - "철근", - "기초잡석 운반, 부설 및 다짐", - "소할 (30%) 브레이커", - "적사(굴착기 0.7㎥)", - "운반(덤프트럭 15톤)", - "부 설 다 짐", - "바닥정리" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -36610,7 +34943,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-13", @@ -36637,16 +34971,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "콘크리트 (레미콘)", - "다짐:봉상후렉시블(45mm)", - "거 푸 집", - "기초잡석 운반, 부설 및 다짐", - "소할 (30%) 브레이커", - "적사(굴착기 0.7㎥)", - "운반(덤프트럭 15톤)", - "부 설 다 짐" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -36713,7 +35038,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-14", @@ -36740,10 +35066,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "파형강관부설", - "철 거" - ], + "variant_key": [], "condition_note": [ "구 분", "관경(㎜)", @@ -36771,7 +35094,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-15", @@ -36798,15 +35122,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구체콘크리트 (레미콘)", - "다짐:봉상후렉시블(45mm)", - "버림콘크리트 (레미콘)", - "거 푸 집", - "철근", - "집수정 뚜껑", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -36866,7 +35182,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-16", @@ -36893,18 +35210,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구체콘크리트 (레미콘)", - "봉상후렉시블(45mm)", - "버림콘크리트 (레미콘)", - "원형거푸집 (PE10회)", - "설치비", - "보통인부", - "벽체", - "거 푸 집", - "철 근", - "사다리설치" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37010,7 +35316,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-17", @@ -37020,7 +35327,8 @@ "parent_code": "FP-12", "sort_order": 98560, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-17-01", @@ -37053,10 +35361,7 @@ "capacity_formula_here": false, "variant_key": [ "무근콘크리트", - "콘 크 리 트 공", - "특 별 인 부", - "보 통 인 부", - "콘크리트펌프차" + "철근콘크리트" ], "condition_note": [ "구 분", @@ -37102,6 +35407,10 @@ ] ] } + ], + "variant_keys": [ + "무근콘크리트", + "철근콘크리트" ] }, { @@ -37111,7 +35420,8 @@ "level": 3, "parent_code": "FP-12-17", "sort_order": 99072, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-12-17-02", @@ -37138,13 +35448,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "구체콘크리트 (철근)", - "타설", - "보통인부", - "펌프카(80㎥/hr)", - "콘크리트다짐" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37189,7 +35493,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-18", @@ -37218,12 +35523,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "결속선(R=0.9mm)", - "철 근 공", - "보통인부", - "기구손료(노무비의)" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37257,7 +35557,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-19", @@ -37288,15 +35589,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "자재", - "이음철물", - "조임철물", - "받침철물", - "철 물", - "인력", - "경비" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -37355,7 +35648,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-20", @@ -37384,13 +35678,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "자재", - "외관(60.6mm×2.3mm)", - "잡재료비(재료비의)", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37440,7 +35728,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-21", @@ -37469,11 +35758,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37504,7 +35789,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-22", @@ -37531,10 +35817,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -37553,7 +35836,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-23", @@ -37580,11 +35864,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "잡재료비(재료비의)", - "인력" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -37615,7 +35895,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-24", @@ -37625,7 +35906,8 @@ "parent_code": "FP-12", "sort_order": 101120, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-24-01", @@ -37634,7 +35916,10 @@ "level": 3, "parent_code": "FP-12-24", "sort_order": 101376, - "tables": [] + "tables": [], + "variant_keys": [ + "뒷채움 자재비 및 운반비 별산" + ] }, { "work_item_code": "FP-12-24-02", @@ -37672,16 +35957,7 @@ "special_glyphs": [], "capacity_formula_here": true, "variant_key": [ - "뒷채움 자재비 및 운반비 별산", - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "f", - "E", - "㎝(sec)", - "살수", - "다짐", - "타이어 로울러(8-15ton)" + "뒷채움 자재비 및 운반비 별산" ], "condition_note": [ "구 분", @@ -37771,6 +36047,9 @@ ] ] } + ], + "variant_keys": [ + "뒷채움 자재비 및 운반비 별산" ] }, { @@ -37807,15 +36086,7 @@ "×(U+00D7)" ], "capacity_formula_here": true, - "variant_key": [ - "소할(30%)", - "적사", - "E", - "f", - "㎝(sec)", - "운반", - "부설 및 다짐" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -37873,7 +36144,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-26", @@ -37900,12 +36172,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "기계", - "디젤엔진(15HP)", - "운반 및 설치 (목도운반)", - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -37938,7 +36205,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-27", @@ -37948,7 +36216,8 @@ "parent_code": "FP-12", "sort_order": 102400, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-24-01", @@ -37986,15 +36255,7 @@ "special_glyphs": [], "capacity_formula_here": true, "variant_key": [ - "뒷채움 자재비 및 운반비 별산", - "인력 (10%)", - "보통인부(인)", - "장비 (90%)", - "f", - "E", - "㎝(sec)", - "살수", - "다짐" + "뒷채움 자재비 및 운반비 별산" ], "condition_note": [ "구 분", @@ -38076,6 +36337,9 @@ ] ] } + ], + "variant_keys": [ + "뒷채움 자재비 및 운반비 별산" ] }, { @@ -38103,11 +36367,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "접착제", - "인력(설치비)" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38138,7 +36398,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-27-03", @@ -38167,10 +36428,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력(설치비)" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38194,7 +36452,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-28", @@ -38221,11 +36480,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "시너", - "인력" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38256,7 +36511,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-29", @@ -38283,10 +36539,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -38305,7 +36558,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-30", @@ -38315,7 +36569,8 @@ "parent_code": "FP-12", "sort_order": 103936, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-30-01", @@ -38342,13 +36597,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "철근가공조립(간단)", - "P.V.C 파이프(Ø35mm)", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38393,7 +36642,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-30-02", @@ -38424,17 +36674,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "재료", - "탄성고무받침", - "스티로폴", - "TAR PAPER", - "채움재(아스팔트)", - "다웰바캡", - "조립", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "규격", @@ -38517,7 +36757,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-31", @@ -38544,10 +36785,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -38566,7 +36804,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-32", @@ -38593,10 +36832,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -38615,7 +36851,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-33", @@ -38642,10 +36879,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료", - "인력" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38669,7 +36903,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-34", @@ -38679,7 +36914,8 @@ "parent_code": "FP-12", "sort_order": 105472, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-12-34-01", @@ -38706,13 +36942,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력", - "보통인부", - "기계", - "봉상후렉시블(45mm)" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38757,7 +36987,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-34-02", @@ -38784,10 +37015,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비", - "노무비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -38806,7 +37034,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-34-03", @@ -38833,14 +37062,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력", - "보통인부", - "고철대(감)", - "철근", - "운반" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38892,7 +37114,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-34-04", @@ -38906,8 +37129,8 @@ "pum_table_id": "F0388", "section": "12-34-4. 채움재(T=20m/m)", "source_line": 6877, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 「재료비 JOINT FILLER」 항목만 — 값이 없는 구성 안내", "basis_quantity": 1.0, "basis_unit": "㎡", "basis_source": "본문", @@ -38919,9 +37142,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "재료비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -38935,7 +37156,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-35", @@ -38962,11 +37184,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "설치", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -38997,7 +37215,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-36", @@ -39011,8 +37230,8 @@ "pum_table_id": "F0390", "section": "12-36. P.C BOX 설치", "source_line": 6895, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 제작비·운송비·설치비 「견적처리」", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -39024,11 +37243,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "제작비", - "운송비", - "설치비" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -39052,7 +37267,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-37", @@ -39079,11 +37295,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "인력", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단위", @@ -39114,7 +37326,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-38", @@ -39135,7 +37348,12 @@ "weight": "1" } ], - "steps_basis": "품셈 12-38 유로폼 = 12-38-2 사용수량(자재) + 12-38-3 설치 및 해체(품) — 12-38-1 사용횟수는 금액 단계가 아니라 사용수량의 잔존율 조건" + "steps_basis": "품셈 12-38 유로폼 = 12-38-2 사용수량(자재) + 12-38-3 설치 및 해체(품) — 12-38-1 사용횟수는 금액 단계가 아니라 사용수량의 잔존율 조건", + "variant_keys": [ + "복 잡", + "보 통", + "간 단" + ] }, { "work_item_code": "FP-12-38-01", @@ -39149,8 +37367,8 @@ "pum_table_id": "F0392", "section": "12-38-1. 사용횟수", "source_line": 6915, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 사용횟수별 잔존율(12회 25%·25회 10%)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -39162,9 +37380,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "패 널 류 보, 드롭헤드, 강관파이프, 훅  클래프, 웨지핀" - ], + "variant_key": [], "condition_note": [ "구 분", "사용 조작 회수" @@ -39176,7 +37392,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-38-02", @@ -39208,12 +37425,7 @@ "x(U+0078)" ], "capacity_formula_here": false, - "variant_key": [ - "패 널", - "내 부 패 널", - "부 자 재 (웨지핀, 플랫타이, 강관파이프, 훅 등)", - "소모자재(박리제 등)" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -39251,8 +37463,8 @@ "pum_table_id": "F0394", "section": "12-38-2. 사용수량", "source_line": 6932, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — 부자재 요율 「주자재비의 24%·52%·79%」", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -39264,9 +37476,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "요 율" - ], + "variant_key": [], "condition_note": [ "구 분", "간 단", @@ -39282,7 +37492,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-12-38-03", @@ -39311,8 +37522,8 @@ "capacity_formula_here": false, "variant_key": [ "복 잡", - "형틀목공 보통인부", - "비 고" + "보 통", + "간 단" ], "condition_note": [ "구 분", @@ -39351,8 +37562,8 @@ "pum_table_id": "F0396", "section": "12-38-3. 설치 및 해체", "source_line": 6954, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 설치 유형(복잡·보통·간단) 적용 시설 설명", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -39364,11 +37575,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "복 잡", - "보 통", - "간 단" - ], + "variant_key": [], "condition_note": [ "구 분", "유 형" @@ -39388,6 +37595,11 @@ ] ] } + ], + "variant_keys": [ + "복 잡", + "보 통", + "간 단" ] }, { @@ -39398,7 +37610,8 @@ "parent_code": null, "sort_order": 108544, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-01", @@ -39407,7 +37620,8 @@ "level": 2, "parent_code": "FP-13", "sort_order": 108800, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-13-02", @@ -39417,7 +37631,8 @@ "parent_code": "FP-13", "sort_order": 109056, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-02-01", @@ -39444,10 +37659,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "규격", - "보통인부(인)" - ], + "variant_key": [], "condition_note": [ "명 칭", "친모래", @@ -39478,7 +37690,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-02-02", @@ -39506,7 +37719,7 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "보통인부", + "㎡당", "㎥당" ], "condition_note": [ @@ -39529,6 +37742,10 @@ ] ] } + ], + "variant_keys": [ + "㎡당", + "㎥당" ] }, { @@ -39556,13 +37773,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작업반장", - "굴착기", - "집게 장치", - "(회전식 돌집게) 굴착기 운전", - "제 잡비 비율" - ], + "variant_key": [], "condition_note": [ "명 칭", "규 격", @@ -39612,8 +37823,8 @@ "pum_table_id": "F0400", "section": "13-2-3. 큰돌 채집", "source_line": 7006, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 큰돌 직경별 뒷길이 기준(㎝)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -39628,10 +37839,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "40㎝이상∼60㎝미만", - "뒷길이" - ], + "variant_key": [], "condition_note": [ "명칭", "단위", @@ -39654,7 +37862,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-02-04", @@ -39682,8 +37891,16 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "인 부", - "㎥당" + "25㎝·㎡당", + "35㎝·㎡당", + "45㎝·㎡당", + "55㎝·㎡당", + "60㎝·㎡당", + "25㎝·㎥당", + "35㎝·㎥당", + "45㎝·㎥당", + "55㎝·㎥당", + "60㎝·㎥당" ], "condition_note": [ "뒷 길 이(㎝)", @@ -39714,6 +37931,18 @@ ] ] } + ], + "variant_keys": [ + "25㎝·㎡당", + "35㎝·㎡당", + "45㎝·㎡당", + "55㎝·㎡당", + "60㎝·㎡당", + "25㎝·㎥당", + "35㎝·㎥당", + "45㎝·㎥당", + "55㎝·㎥당", + "60㎝·㎥당" ] }, { @@ -39743,9 +37972,7 @@ "∼(U+223C)" ], "capacity_formula_here": false, - "variant_key": [ - "증가율(%)" - ], + "variant_key": [], "condition_note": [ "높 이(m)", "3 ∼ 4까지", @@ -39767,8 +37994,8 @@ "pum_table_id": "F0406", "section": "13-3. 기초다짐 및 뒤채움’ 항을 적용한다.", "source_line": 7094, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 〈돌쌓기의 개수 및 중량의 표준〉 — [주] 인력 돌쌓기·돌붙임 기준", "basis_quantity": 1.0, "basis_unit": "㎡", "basis_source": "본문", @@ -39782,15 +38009,7 @@ "×(U+00D7)" ], "capacity_formula_here": false, - "variant_key": [ - "25㎝(17×17)", - "30㎝(20×20)", - "35㎝(25×25)", - "45㎝(30×30)", - "55㎝(35×35)", - "60㎝(40×40)", - "75㎝(50×50)" - ], + "variant_key": [], "condition_note": [ "종별 뒷길이 단위", "견치돌", @@ -39868,8 +38087,8 @@ "capacity_formula_here": false, "variant_key": [ "35cm 이하", - "석공 보통인부", - "굴착기+부착용 집게" + "55cm 이하", + "75cm 이하" ], "condition_note": [ "명 칭", @@ -39905,7 +38124,12 @@ ] } ], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [ + "35cm 이하", + "55cm 이하", + "75cm 이하" + ] }, { "work_item_code": "FP-13-03-01", @@ -39935,12 +38159,9 @@ ], "capacity_formula_here": false, "variant_key": [ - "모래 기초다짐", "두 께 3㎝", "두 께 6㎝", - "자갈 기초다짐 지름 1~3㎝", - "조약돌 기초다짐 지름 9~15㎝", - "돌쌓기 뒤채움 지름 9~15㎝" + "자갈 기초다짐 지름 1~3㎝" ], "condition_note": [ "종 별", @@ -39980,6 +38201,11 @@ ] ] } + ], + "variant_keys": [ + "두 께 3㎝", + "두 께 6㎝", + "자갈 기초다짐 지름 1~3㎝" ] }, { @@ -40007,11 +38233,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "(mm)", - "기초다짐 뒷채움", - "75이상" - ], + "variant_key": [], "condition_note": [ "종 별", "규격", @@ -40047,7 +38269,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-04", @@ -40057,7 +38280,8 @@ "parent_code": "FP-13", "sort_order": 111104, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-04-01", @@ -40085,15 +38309,46 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "골쌓기", - "석공 (인)", - "25", - "30", - "35", - "45", - "55", - "60", - "75" + "깬잡석 · 골쌓기 · 뒷길이 25㎝", + "깬잡석 · 켜쌓기 · 뒷길이 25㎝", + "호박돌 및 야면석 · 뒷길이 25㎝", + "깬돌 · 골쌓기 · 뒷길이 30㎝", + "깬돌 · 켜쌓기 · 뒷길이 30㎝", + "깬잡석 · 골쌓기 · 뒷길이 30㎝", + "깬잡석 · 켜쌓기 · 뒷길이 30㎝", + "호박돌 및 야면석 · 뒷길이 30㎝", + "견치돌 · 골쌓기 · 뒷길이 35㎝", + "견치돌 · 켜쌓기 · 뒷길이 35㎝", + "깬돌 · 골쌓기 · 뒷길이 35㎝", + "깬돌 · 켜쌓기 · 뒷길이 35㎝", + "깬잡석 · 골쌓기 · 뒷길이 35㎝", + "깬잡석 · 켜쌓기 · 뒷길이 35㎝", + "호박돌 및 야면석 · 뒷길이 35㎝", + "견치돌 · 골쌓기 · 뒷길이 45㎝", + "견치돌 · 켜쌓기 · 뒷길이 45㎝", + "깬돌 · 골쌓기 · 뒷길이 45㎝", + "깬돌 · 켜쌓기 · 뒷길이 45㎝", + "깬잡석 · 골쌓기 · 뒷길이 45㎝", + "깬잡석 · 켜쌓기 · 뒷길이 45㎝", + "호박돌 및 야면석 · 뒷길이 45㎝", + "견치돌 · 골쌓기 · 뒷길이 55㎝", + "견치돌 · 켜쌓기 · 뒷길이 55㎝", + "깬돌 · 골쌓기 · 뒷길이 55㎝", + "깬돌 · 켜쌓기 · 뒷길이 55㎝", + "깬잡석 · 골쌓기 · 뒷길이 55㎝", + "깬잡석 · 켜쌓기 · 뒷길이 55㎝", + "호박돌 및 야면석 · 뒷길이 55㎝", + "견치돌 · 골쌓기 · 뒷길이 60㎝", + "견치돌 · 켜쌓기 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 60㎝", + "깬돌 · 켜쌓기 · 뒷길이 60㎝", + "깬잡석 · 골쌓기 · 뒷길이 60㎝", + "깬잡석 · 켜쌓기 · 뒷길이 60㎝", + "호박돌 및 야면석 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 75㎝", + "깬돌 · 켜쌓기 · 뒷길이 75㎝", + "깬잡석 · 골쌓기 · 뒷길이 75㎝", + "깬잡석 · 켜쌓기 · 뒷길이 75㎝" ], "condition_note": [ "뒷길이 (㎝)", @@ -40258,6 +38513,48 @@ ] ] } + ], + "variant_keys": [ + "깬잡석 · 골쌓기 · 뒷길이 25㎝", + "깬잡석 · 켜쌓기 · 뒷길이 25㎝", + "호박돌 및 야면석 · 뒷길이 25㎝", + "깬돌 · 골쌓기 · 뒷길이 30㎝", + "깬돌 · 켜쌓기 · 뒷길이 30㎝", + "깬잡석 · 골쌓기 · 뒷길이 30㎝", + "깬잡석 · 켜쌓기 · 뒷길이 30㎝", + "호박돌 및 야면석 · 뒷길이 30㎝", + "견치돌 · 골쌓기 · 뒷길이 35㎝", + "견치돌 · 켜쌓기 · 뒷길이 35㎝", + "깬돌 · 골쌓기 · 뒷길이 35㎝", + "깬돌 · 켜쌓기 · 뒷길이 35㎝", + "깬잡석 · 골쌓기 · 뒷길이 35㎝", + "깬잡석 · 켜쌓기 · 뒷길이 35㎝", + "호박돌 및 야면석 · 뒷길이 35㎝", + "견치돌 · 골쌓기 · 뒷길이 45㎝", + "견치돌 · 켜쌓기 · 뒷길이 45㎝", + "깬돌 · 골쌓기 · 뒷길이 45㎝", + "깬돌 · 켜쌓기 · 뒷길이 45㎝", + "깬잡석 · 골쌓기 · 뒷길이 45㎝", + "깬잡석 · 켜쌓기 · 뒷길이 45㎝", + "호박돌 및 야면석 · 뒷길이 45㎝", + "견치돌 · 골쌓기 · 뒷길이 55㎝", + "견치돌 · 켜쌓기 · 뒷길이 55㎝", + "깬돌 · 골쌓기 · 뒷길이 55㎝", + "깬돌 · 켜쌓기 · 뒷길이 55㎝", + "깬잡석 · 골쌓기 · 뒷길이 55㎝", + "깬잡석 · 켜쌓기 · 뒷길이 55㎝", + "호박돌 및 야면석 · 뒷길이 55㎝", + "견치돌 · 골쌓기 · 뒷길이 60㎝", + "견치돌 · 켜쌓기 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 60㎝", + "깬돌 · 켜쌓기 · 뒷길이 60㎝", + "깬잡석 · 골쌓기 · 뒷길이 60㎝", + "깬잡석 · 켜쌓기 · 뒷길이 60㎝", + "호박돌 및 야면석 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 75㎝", + "깬돌 · 켜쌓기 · 뒷길이 75㎝", + "깬잡석 · 골쌓기 · 뒷길이 75㎝", + "깬잡석 · 켜쌓기 · 뒷길이 75㎝" ] }, { @@ -40287,8 +38584,8 @@ "capacity_formula_here": false, "variant_key": [ "35cm 이하", - "석공 보통인부", - "굴착기+부착용 집게" + "55cm 이하", + "75cm 이하" ], "condition_note": [ "명 칭", @@ -40323,6 +38620,11 @@ ] ] } + ], + "variant_keys": [ + "35cm 이하", + "55cm 이하", + "75cm 이하" ] }, { @@ -40337,8 +38639,8 @@ "pum_table_id": "F0408", "section": "13-4-3. 고임돌 소요량", "source_line": 7128, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 고임돌 소요량(㎥/㎡) — 자재 원단위표(B08 고임돌표가 씀). 품으로 세우면 자재를 두 번 셈", "basis_quantity": 1.0, "basis_unit": "㎡", "basis_source": "본문", @@ -40350,9 +38652,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "야면석(㎥) 깬잡석(㎥) 깬 돌(㎥) 견치돌(㎥)" - ], + "variant_key": [], "condition_note": [ "뒷길이 종별", "25㎝", @@ -40376,7 +38676,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-04-04", @@ -40404,15 +38705,46 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "골쌓기", - "석 공 (인)", - "25", - "30", - "35", - "45", - "55", - "60", - "75" + "깬잡석 · 골쌓기 · 뒷길이 25㎝", + "깬잡석 · 켜쌓기 · 뒷길이 25㎝", + "호박돌 및 야면석 · 뒷길이 25㎝", + "깬돌 · 골쌓기 · 뒷길이 30㎝", + "깬돌 · 켜쌓기 · 뒷길이 30㎝", + "깬잡석 · 골쌓기 · 뒷길이 30㎝", + "깬잡석 · 켜쌓기 · 뒷길이 30㎝", + "호박돌 및 야면석 · 뒷길이 30㎝", + "견치돌 · 골쌓기 · 뒷길이 35㎝", + "견치돌 · 켜쌓기 · 뒷길이 35㎝", + "깬돌 · 골쌓기 · 뒷길이 35㎝", + "깬돌 · 켜쌓기 · 뒷길이 35㎝", + "깬잡석 · 골쌓기 · 뒷길이 35㎝", + "깬잡석 · 켜쌓기 · 뒷길이 35㎝", + "호박돌 및 야면석 · 뒷길이 35㎝", + "견치돌 · 골쌓기 · 뒷길이 45㎝", + "견치돌 · 켜쌓기 · 뒷길이 45㎝", + "깬돌 · 골쌓기 · 뒷길이 45㎝", + "깬돌 · 켜쌓기 · 뒷길이 45㎝", + "깬잡석 · 골쌓기 · 뒷길이 45㎝", + "깬잡석 · 켜쌓기 · 뒷길이 45㎝", + "호박돌 및 야면석 · 뒷길이 45㎝", + "견치돌 · 골쌓기 · 뒷길이 55㎝", + "견치돌 · 켜쌓기 · 뒷길이 55㎝", + "깬돌 · 골쌓기 · 뒷길이 55㎝", + "깬돌 · 켜쌓기 · 뒷길이 55㎝", + "깬잡석 · 골쌓기 · 뒷길이 55㎝", + "깬잡석 · 켜쌓기 · 뒷길이 55㎝", + "호박돌 및 야면석 · 뒷길이 55㎝", + "견치돌 · 골쌓기 · 뒷길이 60㎝", + "견치돌 · 켜쌓기 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 60㎝", + "깬돌 · 켜쌓기 · 뒷길이 60㎝", + "깬잡석 · 골쌓기 · 뒷길이 60㎝", + "깬잡석 · 켜쌓기 · 뒷길이 60㎝", + "호박돌 및 야면석 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 75㎝", + "깬돌 · 켜쌓기 · 뒷길이 75㎝", + "깬잡석 · 골쌓기 · 뒷길이 75㎝", + "깬잡석 · 켜쌓기 · 뒷길이 75㎝" ], "condition_note": [ "뒷길이 (㎝)", @@ -40581,8 +38913,8 @@ "pum_table_id": "F0410", "section": "13-4-4. 찰쌓기(인력)", "source_line": 7152, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — [주]① 채움 콘크리트 소요량(㎥/㎡) — 자재 원단위표(B08 채움표가 씀). 품이면 두 번 셈", "basis_quantity": 1.0, "basis_unit": "㎡", "basis_source": "본문", @@ -40594,10 +38926,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "야면석(㎥) 호박돌(㎥)", - "깬잡석(㎥) 깬 돌(㎥) 견치돌(㎥)" - ], + "variant_key": [], "condition_note": [ "뒷길이 종별", "25㎝", @@ -40638,8 +38967,8 @@ "pum_table_id": "F0411", "section": "13-4-4. 찰쌓기(인력)", "source_line": 7166, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — [주]⑨ 성토 뒤채움 조약돌 두께(㎝) 기준", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -40653,9 +38982,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "상부의 두께(㎝) 하부의 두께(㎝)" - ], + "variant_key": [], "condition_note": [ "직 고(直高)", "∼1.5m", @@ -40677,8 +39004,8 @@ "pum_table_id": "F0412", "section": "13-4-4. 찰쌓기(인력)", "source_line": 7174, - "pum_form": "requirement", - "form_basis": "값 단위 '(m)'", + "pum_form": "reference", + "form_basis": "사람 판정 — [주]⑩ 뒷길이 표준(㎝) — 자동 판정이 「(m)」에 걸려 소요량형으로 오판", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -40693,10 +39020,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "∼1.5", - "메쌓기(㎝) 찰쌓기(㎝)" - ], + "variant_key": [], "condition_note": [ "높이단위 공종별", "돌쌓기 높이(m)", @@ -40727,8 +39051,8 @@ "pum_table_id": "F0413", "section": "13-4-4. 찰쌓기(인력)", "source_line": 7185, - "pum_form": "requirement", - "form_basis": "값 단위 '(m)'", + "pum_form": "reference", + "form_basis": "사람 판정 — [주]⑪ 표준경사(1:n) — 자동 판정이 소요량형으로 오판, 찰쌓기(인력)를 막음", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -40740,11 +39064,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "메쌓기", - "절토", - "찰쌓기" - ], + "variant_key": [], "condition_note": [ "직 고(直高)(m)", "∼1.5", @@ -40792,6 +39112,48 @@ ] ] } + ], + "variant_keys": [ + "깬잡석 · 골쌓기 · 뒷길이 25㎝", + "깬잡석 · 켜쌓기 · 뒷길이 25㎝", + "호박돌 및 야면석 · 뒷길이 25㎝", + "깬돌 · 골쌓기 · 뒷길이 30㎝", + "깬돌 · 켜쌓기 · 뒷길이 30㎝", + "깬잡석 · 골쌓기 · 뒷길이 30㎝", + "깬잡석 · 켜쌓기 · 뒷길이 30㎝", + "호박돌 및 야면석 · 뒷길이 30㎝", + "견치돌 · 골쌓기 · 뒷길이 35㎝", + "견치돌 · 켜쌓기 · 뒷길이 35㎝", + "깬돌 · 골쌓기 · 뒷길이 35㎝", + "깬돌 · 켜쌓기 · 뒷길이 35㎝", + "깬잡석 · 골쌓기 · 뒷길이 35㎝", + "깬잡석 · 켜쌓기 · 뒷길이 35㎝", + "호박돌 및 야면석 · 뒷길이 35㎝", + "견치돌 · 골쌓기 · 뒷길이 45㎝", + "견치돌 · 켜쌓기 · 뒷길이 45㎝", + "깬돌 · 골쌓기 · 뒷길이 45㎝", + "깬돌 · 켜쌓기 · 뒷길이 45㎝", + "깬잡석 · 골쌓기 · 뒷길이 45㎝", + "깬잡석 · 켜쌓기 · 뒷길이 45㎝", + "호박돌 및 야면석 · 뒷길이 45㎝", + "견치돌 · 골쌓기 · 뒷길이 55㎝", + "견치돌 · 켜쌓기 · 뒷길이 55㎝", + "깬돌 · 골쌓기 · 뒷길이 55㎝", + "깬돌 · 켜쌓기 · 뒷길이 55㎝", + "깬잡석 · 골쌓기 · 뒷길이 55㎝", + "깬잡석 · 켜쌓기 · 뒷길이 55㎝", + "호박돌 및 야면석 · 뒷길이 55㎝", + "견치돌 · 골쌓기 · 뒷길이 60㎝", + "견치돌 · 켜쌓기 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 60㎝", + "깬돌 · 켜쌓기 · 뒷길이 60㎝", + "깬잡석 · 골쌓기 · 뒷길이 60㎝", + "깬잡석 · 켜쌓기 · 뒷길이 60㎝", + "호박돌 및 야면석 · 뒷길이 60㎝", + "깬돌 · 골쌓기 · 뒷길이 75㎝", + "깬돌 · 켜쌓기 · 뒷길이 75㎝", + "깬잡석 · 골쌓기 · 뒷길이 75㎝", + "깬잡석 · 켜쌓기 · 뒷길이 75㎝" ] }, { @@ -40821,8 +39183,8 @@ "capacity_formula_here": false, "variant_key": [ "35cm 이하", - "석공 보통인부", - "굴착기+부착용 집게" + "55cm 이하", + "75cm 이하" ], "condition_note": [ "명 칭", @@ -40857,6 +39219,11 @@ ] ] } + ], + "variant_keys": [ + "35cm 이하", + "55cm 이하", + "75cm 이하" ] }, { @@ -40912,6 +39279,11 @@ ] ] } + ], + "variant_keys": [ + "25㎝", + "30㎝", + "35㎝" ] }, { @@ -40922,7 +39294,8 @@ "parent_code": "FP-13", "sort_order": 112896, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-05-01", @@ -40950,9 +39323,46 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "종 별", - "뒷길이 (㎝)", - "25 30 35 45 55 60 70" + "메 붙 임 · 깬 돌 · 뒷길이 25㎝", + "메 붙 임 · 깬 돌 · 뒷길이 30㎝", + "메 붙 임 · 깬 돌 · 뒷길이 35㎝", + "메 붙 임 · 깬 돌 · 뒷길이 45㎝", + "메 붙 임 · 깬 돌 · 뒷길이 55㎝", + "메 붙 임 · 깬 돌 · 뒷길이 60㎝", + "메 붙 임 · 깬 돌 · 뒷길이 70㎝", + "메 붙 임 · 깬잡석 · 뒷길이 25㎝", + "메 붙 임 · 깬잡석 · 뒷길이 30㎝", + "메 붙 임 · 깬잡석 · 뒷길이 35㎝", + "메 붙 임 · 깬잡석 · 뒷길이 45㎝", + "메 붙 임 · 깬잡석 · 뒷길이 55㎝", + "메 붙 임 · 깬잡석 · 뒷길이 60㎝", + "메 붙 임 · 깬잡석 · 뒷길이 70㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 25㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 30㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 35㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 45㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 55㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 60㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 25㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 30㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 35㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 45㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 55㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 60㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 70㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 25㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 30㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 35㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 45㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 55㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 60㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 70㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 25㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 30㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 35㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 45㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 55㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 60㎝" ], "condition_note": [ "구 분", @@ -41007,6 +39417,48 @@ ] ] } + ], + "variant_keys": [ + "메 붙 임 · 깬 돌 · 뒷길이 25㎝", + "메 붙 임 · 깬 돌 · 뒷길이 30㎝", + "메 붙 임 · 깬 돌 · 뒷길이 35㎝", + "메 붙 임 · 깬 돌 · 뒷길이 45㎝", + "메 붙 임 · 깬 돌 · 뒷길이 55㎝", + "메 붙 임 · 깬 돌 · 뒷길이 60㎝", + "메 붙 임 · 깬 돌 · 뒷길이 70㎝", + "메 붙 임 · 깬잡석 · 뒷길이 25㎝", + "메 붙 임 · 깬잡석 · 뒷길이 30㎝", + "메 붙 임 · 깬잡석 · 뒷길이 35㎝", + "메 붙 임 · 깬잡석 · 뒷길이 45㎝", + "메 붙 임 · 깬잡석 · 뒷길이 55㎝", + "메 붙 임 · 깬잡석 · 뒷길이 60㎝", + "메 붙 임 · 깬잡석 · 뒷길이 70㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 25㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 30㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 35㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 45㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 55㎝", + "메 붙 임 · 조약돌 및 야면석 · 뒷길이 60㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 25㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 30㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 35㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 45㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 55㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 60㎝", + "찰 붙 임 · 깬 돌 · 뒷길이 70㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 25㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 30㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 35㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 45㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 55㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 60㎝", + "찰 붙 임 · 깬잡석 · 뒷길이 70㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 25㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 30㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 35㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 45㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 55㎝", + "찰 붙 임 · 호박돌 및 야면석 · 뒷길이 60㎝" ] }, { @@ -41036,8 +39488,8 @@ "capacity_formula_here": false, "variant_key": [ "35cm 이하", - "석공 보통인부", - "굴착기+부착용 집게" + "55cm 이하", + "75cm 이하" ], "condition_note": [ "명 칭", @@ -41072,6 +39524,11 @@ ] ] } + ], + "variant_keys": [ + "35cm 이하", + "55cm 이하", + "75cm 이하" ] }, { @@ -41082,7 +39539,8 @@ "parent_code": "FP-13", "sort_order": 113664, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-06-01", @@ -41115,11 +39573,8 @@ "capacity_formula_here": false, "variant_key": [ "직경 40㎝이상 ∼60㎝미만", - "작업반장", - "특별인부", - "보통인부", - "굴 삭 기 (무한궤도)", - "제잡비 비율" + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ], "condition_note": [ "명칭", @@ -41178,6 +39633,11 @@ ] ] } + ], + "variant_keys": [ + "직경 40㎝이상 ∼60㎝미만", + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ] }, { @@ -41209,11 +39669,8 @@ "capacity_formula_here": false, "variant_key": [ "직경 40㎝이상 ∼60㎝미만", - "작업반장", - "특별인부", - "보통인부", - "굴착기 (무한궤도)", - "제잡비 비율" + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ], "condition_note": [ "명칭", @@ -41272,6 +39729,11 @@ ] ] } + ], + "variant_keys": [ + "직경 40㎝이상 ∼60㎝미만", + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ] }, { @@ -41303,11 +39765,8 @@ "capacity_formula_here": false, "variant_key": [ "직경 40㎝이상 ~60㎝미만", - "작업반장", - "특별인부", - "보통인부", - "굴착기 (무한궤도)", - "제잡비 비율" + "직경 60㎝이상 ~80㎝미만", + "직경 80㎝이상 ~100㎝이하" ], "condition_note": [ "명칭", @@ -41366,6 +39825,11 @@ ] ] } + ], + "variant_keys": [ + "직경 40㎝이상 ~60㎝미만", + "직경 60㎝이상 ~80㎝미만", + "직경 80㎝이상 ~100㎝이하" ] }, { @@ -41376,7 +39840,8 @@ "parent_code": "FP-13", "sort_order": 114688, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-07-01", @@ -41407,11 +39872,8 @@ "capacity_formula_here": false, "variant_key": [ "직경 40㎝이상 ~60㎝미만", - "작업반장", - "특별인부", - "보통인부", - "굴착기 (무한궤도)", - "제 잡비 비율" + "직경 60㎝이상 ~80㎝미만", + "직경 80㎝이상 ~100㎝이하" ], "condition_note": [ "명칭", @@ -41470,6 +39932,11 @@ ] ] } + ], + "variant_keys": [ + "직경 40㎝이상 ~60㎝미만", + "직경 60㎝이상 ~80㎝미만", + "직경 80㎝이상 ~100㎝이하" ] }, { @@ -41503,11 +39970,8 @@ "capacity_formula_here": false, "variant_key": [ "직경 40㎝이상 ∼60㎝미만", - "작업반장", - "특별인부", - "보통인부", - "굴 삭 기 (무한궤도)", - "제잡비 비율" + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ], "condition_note": [ "명칭", @@ -41566,6 +40030,11 @@ ] ] } + ], + "variant_keys": [ + "직경 40㎝이상 ∼60㎝미만", + "직경 60㎝이상 ∼80㎝미만", + "직경 80㎝이상 ∼100㎝이하" ] }, { @@ -41593,9 +40062,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "뒷길이 평균 적용" - ], + "variant_key": [], "condition_note": [ "구 분", "명 칭", @@ -41613,7 +40080,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-09", @@ -41644,11 +40112,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "인 력", - "석 공", - "장 비" - ], + "variant_key": [], "condition_note": [ "구 분", "규 격", @@ -41679,7 +40143,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-10", @@ -41689,7 +40154,8 @@ "parent_code": "FP-13", "sort_order": 115968, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-10-01", @@ -41734,6 +40200,9 @@ ] ] } + ], + "variant_keys": [ + "직경 15㎝ 길이 4m" ] }, { @@ -41761,9 +40230,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통 인부 (인)" - ], + "variant_key": [], "condition_note": [ "직종", "말구 길이", @@ -41802,9 +40269,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보 통 인 부 (인)" - ], + "variant_key": [], "condition_note": [ "직종", "말구 길이", @@ -41848,8 +40313,8 @@ "pum_table_id": "F0429", "section": "13-10-2. 나무 말뚝박기", "source_line": 7483, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — [주]⑥ 관입률별 계수", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -41861,9 +40326,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "계수" - ], + "variant_key": [], "condition_note": [ "관입률 =", "0.3", @@ -41906,15 +40369,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "8 9", - "12", - "15", - "18", - "21", - "24", - "27" - ], + "variant_key": [], "condition_note": [ "품종 말구 길이 (㎝) (m)", "경유 (ℓ)", @@ -42037,7 +40492,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-11", @@ -42047,7 +40503,8 @@ "parent_code": "FP-13", "sort_order": 116736, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-11-01", @@ -42074,11 +40531,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조약돌량(㎥)", - "인력(인)", - "돌 채 움" - ], + "variant_key": [], "condition_note": [ "지름(㎝) 공종", "45", @@ -42130,12 +40583,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조립 설치", - "보통인부", - "돌채움", - "굴착기(1.0㎥)" - ], + "variant_key": [], "condition_note": [ "지름(㎝) 공종", "단위", @@ -42198,7 +40646,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-11-02", @@ -42225,11 +40674,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조약돌량(㎥)", - "인력(인)", - "돌 채 움" - ], + "variant_key": [], "condition_note": [ "지름(㎝) 공종", "40", @@ -42297,12 +40742,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "조립 설치", - "보통인부", - "돌채움", - "굴착기 (1.0㎥)" - ], + "variant_key": [], "condition_note": [ "지름(㎝) 공종", "단위", @@ -42370,7 +40810,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-11-03", @@ -42397,10 +40838,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "특별인부", - "보통인부" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -42422,7 +40860,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-11-04", @@ -42449,15 +40888,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "자재", - "채움재", - "잡재료", - "인력", - "특별인부", - "보통인부", - "장비" - ], + "variant_key": [], "condition_note": [ "구 분", "단 위", @@ -42516,7 +40947,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-12", @@ -42526,7 +40958,8 @@ "parent_code": "FP-13", "sort_order": 118016, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-12-01", @@ -42553,12 +40986,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보통토사", - "절 취(㎥)", - "투 입(㎥)", - "면고르기(시간)" - ], + "variant_key": [], "condition_note": [ "구 분 공정별", "보통인부(인)", @@ -42595,7 +41023,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-12-02", @@ -42622,11 +41051,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "작 업 반 장", - "특 별 인 부", - "보 통 인 부" - ], + "variant_key": [], "condition_note": [ "구 분", "수 량", @@ -42654,8 +41079,8 @@ "pum_table_id": "F0439", "section": "13-12-2. 지오셀(사면보강)", "source_line": 7640, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "coefficient", + "form_basis": "사람 판정 — [주]⑦ 높이·기울기 할증(%)", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -42669,11 +41094,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "4m 표준", - "4 ~ 10m", - "10m 이상" - ], + "variant_key": [], "condition_note": [ "구 분 공정별", "1:2 표준", @@ -42705,7 +41126,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-13", @@ -42715,7 +41137,8 @@ "parent_code": "FP-13", "sort_order": 118784, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-13-01", @@ -42743,10 +41166,7 @@ "special_glyphs": [], "capacity_formula_here": false, "variant_key": [ - "보통구조", "중", - "상", - "중등구조", "상등구조" ], "condition_note": [ @@ -42800,6 +41220,10 @@ ] ] } + ], + "variant_keys": [ + "중", + "상등구조" ] }, { @@ -42835,13 +41259,7 @@ ], "special_glyphs": [], "capacity_formula_here": true, - "variant_key": [ - "인력(10%)", - "장비(90%)", - "f", - "E", - "㎝(sec)" - ], + "variant_key": [], "condition_note": [ "구 분", "적 용", @@ -42903,9 +41321,7 @@ "capacity_formula_here": false, "variant_key": [ "식생매트설치", - "특 별 인 부", - "보 통 인 부", - "굴 삭 기" + "복 토" ], "condition_note": [ "구 분", @@ -42986,6 +41402,12 @@ ] ] } + ], + "variant_keys": [ + "식생매트설치", + "복 토", + "간 단 구 조", + "보 통 구 조" ] }, { @@ -43041,6 +41463,11 @@ ] ] } + ], + "variant_keys": [ + "단 끊 기", + "흙채우기(마대채우기)", + "마대쌓기" ] }, { @@ -43051,7 +41478,8 @@ "parent_code": "FP-13", "sort_order": 119808, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-15-01", @@ -43078,9 +41506,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "보 통 인 부" - ], + "variant_key": [], "condition_note": [ "말구 직종", "5cm", @@ -43110,7 +41536,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-15-02", @@ -43124,8 +41551,8 @@ "pum_table_id": "F0446", "section": "13-15-2. 목책 설치", "source_line": 7738, - "pum_form": "undetermined", - "form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음", + "pum_form": "reference", + "form_basis": "사람 판정 — 작업조건(어려운·쉬운) 구분 기준", "basis_quantity": null, "basis_unit": null, "basis_source": null, @@ -43139,10 +41566,7 @@ "~(U+FF5E)" ], "capacity_formula_here": false, - "variant_key": [ - "어려운 조건", - "쉬운 조건" - ], + "variant_key": [], "condition_note": [ "작업조건", "벌도목 직경", @@ -43181,10 +41605,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "어려운 조건", - "쉬운 조건" - ], + "variant_key": [], "condition_note": [ "작업조건", "소요인력", @@ -43203,7 +41624,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-13-15-03", @@ -43212,7 +41634,8 @@ "level": 3, "parent_code": "FP-13-15", "sort_order": 120576, - "tables": [] + "tables": [], + "variant_keys": [] }, { "work_item_code": "FP-13-16", @@ -43222,7 +41645,8 @@ "parent_code": "FP-13", "sort_order": 120832, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-13-16-01", @@ -43253,11 +41677,7 @@ "capacity_formula_here": false, "variant_key": [ "사 면", - "도로/철도", - "특별인부", - "보통인부", - "잠 수 조", - "굴 삭 기" + "연약지반" ], "condition_note": [ "구 분", @@ -43329,6 +41749,10 @@ ] ] } + ], + "variant_keys": [ + "사 면", + "연약지반" ] }, { @@ -43361,8 +41785,7 @@ "capacity_formula_here": false, "variant_key": [ "폭 1.5m 이하", - "조 경 공", - "보 통 인 부" + "폭 2.0m 이하" ], "condition_note": [ "구 분", @@ -43394,6 +41817,10 @@ ] ] } + ], + "variant_keys": [ + "폭 1.5m 이하", + "폭 2.0m 이하" ] }, { @@ -43404,7 +41831,8 @@ "parent_code": null, "sort_order": 121600, "tables": [], - "parent_mode": "choose_one" + "parent_mode": "choose_one", + "variant_keys": [] }, { "work_item_code": "FP-14-01", @@ -43431,10 +41859,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "0.8㎥ 굴착기(우드그랩 부착)", - "보통인부" - ], + "variant_key": [], "condition_note": [ "장 비", "단 위", @@ -43456,7 +41881,8 @@ ] ] } - ] + ], + "variant_keys": [] }, { "work_item_code": "FP-14-02", @@ -43483,10 +41909,7 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [ - "0.8㎥ 굴착기", - "보통인부" - ], + "variant_key": [], "condition_note": [ "장 비", "단 위", @@ -43508,7 +41931,8 @@ ] ] } - ] + ], + "variant_keys": [] } ] } \ No newline at end of file diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/32_특수산식과_현행품셈_대조.md b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/32_특수산식과_현행품셈_대조.md index 1fad0aa44..1152a1918 100644 --- a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/32_특수산식과_현행품셈_대조.md +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/32_특수산식과_현행품셈_대조.md @@ -50,4 +50,4 @@ - 실무 레시피 350개 중 품셈 근거문구 보유는 82개뿐이다. 근거 없는 268개는 현행값으로 임의 확정하지 않는다. - 과거 임도품셈 절 번호가 현행 산림사업 표준품셈에서 크게 바뀌었다. 번호만 같은 다른 절을 자동 연결하지 않는다. - JSON의 `수량문자_확인`은 후보 절 본문에 같은 숫자가 있는지를 세는 1차 검색값이다. `일치` 확정은 위 두 항목처럼 조건과 행을 직접 대조한 경우만 한다. -- 후속 수동 대조는 근거 절이 확인된 11개부터 하고, 복수 절 참조 69개는 과거판-현행판 절 대응표가 있어야 안전하다. +- 후속 판 대응표는 `33_임도품셈_판_대응표.md`와 `30_원자료/임도품셈_판_대응표.csv`에 작성했다. 2017·2018·2024 원문 대조 결과 확정 12·미확인 2이며, 미확인은 자동 치환에 쓰지 않는다. diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/33_임도품셈_판_대응표.md b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/33_임도품셈_판_대응표.md new file mode 100644 index 000000000..bec1f1cc6 --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/33_임도품셈_판_대응표.md @@ -0,0 +1,37 @@ +# 과거 임도품셈 ↔ 2026 현행 산림사업 표준품셈 대응표 + +작성일: 2026-09-13 + +산출물: `30_원자료/임도품셈_판_대응표.csv` + +현행 기준: 산림청고시 제2025-82호, 2026-01-01 시행 + +## 결과 + +STmate 레시피의 임도계 품셈 인용 14개를 2017 임도표준품셈, 2018 통합 초판, 2024 개정판, 2026 현행판 순으로 대조했다. + +| 판정 | 수 | 뜻 | +|---|---:|---| +| 확정 | 13 | 절 이동 경로와 표의 장비·계수·수량 또는 현행 원문의 적용 대상을 직접 대조 | +| 추정 | 0 | 원문 대조 후 남은 추정 없음 | +| 미확인 | 1 | 외부 건설품셈을 참조하여 현행 산림품셈 절로 연결 불가 | + +2018 통합 초판의 목차에는 2017 임도품셈 번호가 괄호로 직접 병기되어 있다. 이 표기로 `3-3-1→4.3.2`, `3-3-2→4.3.3`, `3-6→4.24`, `3-7→4.25`, `5-1-1→4.27.1`, `5-1-2→4.27.2`, `7-3→4.40`을 확인했다. 2024 개정판과 2026 현행판은 실제 표의 장비 규격, 작업능력, 계수와 수량까지 대조했다. + +## 추가 확인 결과 + +- `7-3 노면다짐`: 2017·2018 원문 모두 자체 품 없이 건설품셈 기계화 시공을 참조한다. 2024판 목차에는 노면다짐이 없고, 현행 `3-4-3`은 인력식 노면 보수이므로 대응 절을 `미확인`으로 유지한다. +- `9-3 임도성토면다짐`: 공종은 `2017 3-6 → 2018 4.24 → 2024 8-18 → 현행 9-17-1 비탈면 다짐`으로 이어진다. 현행 원문도 `임도성토면 다짐` 적용, 굴착기 부착용 유압식 진동 콤펙터와 굴착기 `0.7㎥`, `A=77.7㎡/시간`을 직접 명시하므로 절 대응은 `확정`한다. 다만 STmate의 `9-3`과 `Q=716.8`은 원문과 불일치하므로 해당 번호·값은 복사하지 않는다. + +## 사용 경계 + +- 확정 13건은 **절 번호 대응**이 확정된 것이다. STmate 레시피의 값 전체가 현행에 적합하다는 뜻은 아니다. +- 과거 한 절이 현행 여러 절로 분리된 `선떼붙이기공`과 `암절취`는 일대다 대응을 유지한다. 대표 하위 절 하나로 자동 치환하지 않는다. +- `5-1-1 나 암절취`의 STmate 장비 규격 `굴삭기 0.4㎥`는 원문 `0.7㎥`와 다르므로 절 대응과 별개로 값 복사를 금지한다. + +## 확인 원문 + +- 산림청, `임도표준품셈 최종파일 188264.pdf`(2017): 인쇄면 32, 38, 63~65, 91쪽 +- 산림청, `2018년도 산림사업 표준품셈-최종.pdf`: 목차의 구절→신절 병기 및 4.16.2·4.16.4·4.39 +- 산림청, `2024년도 개정판 안전 산림사업 표준품셈.pdf`: 인쇄면 60, 62, 146~148, 156~167, 188쪽 +- 2026 현행: `resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md` 9-17-1 및 `resources/data_work_item_master/work_item_master_2026-01-01.json` diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/34_화면구조_정리.md b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/34_화면구조_정리.md new file mode 100644 index 000000000..2da63a336 --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/34_화면구조_정리.md @@ -0,0 +1,185 @@ +# STmate 원가·수량 화면 구조 정리 + +작성일: 2026-09-13 + +근거: `30_원자료/ui_form_catalog.txt` · `30_원자료/print_form_map.txt` · `30_원자료/TWM_KANJUB.dfm` · `30_원자료/TPM_KAN_RATE.dfm` · `30_원자료/TWM_KAN_RATE.dfm` · `20_분석/22_UI_폼_분석.md` · `20_분석/19_진행률_장부.md` §3 + +## 1. 범위와 판정 + +- 카탈로그의 STmate 고유 폼 150개 가운데 원가·수량 입력, 기초단가, 일위대가·단가산출, 제잡비, 출력에 직접 참여하는 **48개 화면**을 추렸다. +- `PM_*` 인쇄 폼은 실제 출력 시트와 이어지는 화면만 포함했다. 같은 출력의 세로·가로·수공 변형은 한 행에 묶었다. +- 계약·실행·기성 화면은 설계 원가 흐름과 맞닿은 것만 `보조`로 표시했다. +- 조림·방제 등 사업별 설계 화면, 유지보수·다운로드·복구·암호·정보창은 제외했다. +- 화면 이름과 선택지는 카탈로그 원문을 유지했다. 기능 또는 출력 연결이 원자료에서 확인되지 않은 곳은 `미확인`이다. + +## 2. 화면 갈래 + +### 2.1 내역 입력 — 11개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wBoq` | 공종목록에서 내역서로 항목을 넣는 중심 화면 | `cBox01/02`: `= 이상`~`====== 이상`; `RGrpTo`: `내역서 / 열린 보관함` | +| `wKongJong` | 공종 목록 조회·선택 | `RdoLevel`: `소계(+=)`~`총대계(+======)`; `RdoIndex`: `코드번호 순 / 명칭 순 / 분류번호 순`; `RdoFilt`: `일위대가/산근전체 / 대공종 해당목록만` | +| `wMBoq` | 주공종 집계와 계표시 확인 | `CBox01`: `1레벨(소계) 이상`~`6레벨(총대계) 이상` | +| `wITNum` | 내역서 공종번호 자동입력 | 첫 글자 `공란 / . / ( / - / 추가입력`; 끝 글자 `공란 / . / )`; 번호형식 `가..하… / a..z…`; `Fmt_Typ`: `0 / 00 / 000` | +| `wBoqRate` | 내역 항목에 할증율 적용 | `Rdo`: `전체(재,노,경 같은 비율) / 비목별(재,노,경 각각)` | +| `wM_Rate` | 선택 범위의 수량·비목 금액 일괄 보정 | 범위 `내역서 전체 / 선택된 항목만`; 비목별 `절사 / 반올림`; `소수 0위`~`소수 4위값을 적용` | +| `wExpnd` | 일위대가를 내역 행으로 풀기 | `RGrpToT`: `소계(=) / 계(==) / 합계(===)`; `절삭하여 / 반올림하여`; `소수 4위`~`소수 0위값을 적용` | +| `wM_Expnd` | 일위대가와 단가산출근거를 내역에 풀어넣기 | 재료·노무·경비·일식 각각 `절사 / 반올림`, `소수 0위`~`소수 4위값을 적용` | +| `wM_Expnd10` | 일위대가 풀기와 가산 비목 지정 | `입력안함 / 소계(=) / 계(==) / 합계(===)`; 입력 비목 `재료비 / 노무비 / 경비 / 전체`; 결과 비목 `재료비 / 노무비 / 경비 / 해당비목` | +| `wM_From_XLS` | XLS 자료를 내역으로 불러오기 | 주요 입력 칸·선택지 `미확인` | +| `wSameCode` | 내역의 동일 코드 처리 | 주요 입력 칸·선택지 `미확인` | + +### 2.2 수량 — 2개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wQtyList` | 별도 수량산출서에서 코드별 수량을 조회·연결 | `cBoxCode`: `모든단가 / 노무비 / 재료비 / 경비 / 일위대가 / 중기비 / 단가산출 / 견적단가 / 대가,산근` | +| `uM_ConQTY` | 수량 자료 변환·연결 | 구체적인 칸과 변환 방향 `미확인` | + +### 2.3 일위대가·단가산출 — 7개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wM_Edit_iLWi` | 일위대가표 구성행 수정 | 노임계수 `초기값설정 개별적용 / 기본 / 기타 / 기타-1`~`기타-4 노임산출계수`; 저장비목 `각 해당비목 / 재료비 / 노무비 / 경비` | +| `wM_Edit_San` | 단가산출근거와 Q식 수정 | 블록 `열블럭 / 행블럭 / 일반`; 함수 `∏ / INT / SIN / COS / TAN / SQRT / √ / ROUND / ROUNDDOWN …`; 연산 `/ / *`; 수량기호 `Q / A / T / N`; 저장비목 4택 | +| `wM_Edit_EQ` | 표준중기공식 편집 | 삽입 `공식 / 공식+코드` | +| `wM_Edit_USER` | 사용자 단가산출식 편집 | 주요 입력 칸·선택지 `미확인` | +| `wM_iLWi_San` | 일위대가·단가산출 자료 조회/연결 | 구체적인 칸과 편집 범위 `미확인` | +| `wM_Conv_A2W` | 실적단가를 일식단가로 변환등록 | `비목율(단가 => 노,재,경) / 조달청 발표단가 변환적용`; 연결 `공종코드↔분류번호 / 공종코드↔시설코드 / 명칭·규격·단위` | +| `wM_Boq_ExecX` | 시간·단위당 중기 실행단가 계산·입력 | 절사 단위 `1원 / 10원 / 100원 / 1,000원 / 10,000원 / 100,000원`; 보정 `기본보정 / 노무비 / 재료비 / 경비` | + +### 2.4 기준자료(단가·노임·중기) — 10개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wM_JabPum` | 기준 단가 목록 조회·선택 | 코드종류 `모든단가 / 노무비 / 재료비 / 경비 / 일위대가 / 중기비 / 단가산출 / 견적단가 / 대가,산근 / 그림코드`; 검색 `명칭,규격 / 산근내용` | +| `wM_Edt_Hal` | 기준단가를 비율로 일괄 수정 | `절사 / 반올림`; 화면 안내상 적용단가 0원과 보호표시 항목은 제외 | +| `wM_Edit_XRate` | 시간당 손료계수 수정 | `계 입력 / 각계수 입력` | +| `wM_Boxa` | 단가목록 조회·비교 | `변동없음 / 1 단가`~`5 단가 / 최소단가`; 자재·공종 분류 필터 | +| `wM_Box_H` | 불러간 단가 목록 확인 | `변동없음 / 1 단가`~`5 단가 / 최소단가` | +| `wM_JDCode` | 공사원가 호환규정코드 검색·입력 | 기준 `명칭 / 명칭·규격 / 분류번호 / 시설코드 / 선택안함`; 검색 `명칭·규격·단위 / 호환규정코드 / 군급목록번호 / 중기품셈코드 / 시설코드번호`; 종류 `기본 / 자재 / 경비 / 중기 / 실적·기타` | +| `wM_Mrg_BSC` | 새 기준자료로 프로젝트 자료 갱신 | 매칭 `호환규정코드 / 시설코드 / 명칭·규격·단위 / 호환+명+규+단 / 명칭·단위 / 설정안함`; 명칭·단가·조달코드별 복사처/복사원 적용 | +| `wM_JajeXLS` | 자재 기준자료를 XLS로 내보내거나 불러오기 | 방향과 주요 선택지 `미확인` | +| `PM_iLWi`·`PM_iLWi_S` | 일위대가표 인쇄 원장을 구성 | 인쇄 전용 폼; 입력 칸 `미확인` | +| `PM_JOONG`·`PM_JOONG2`·`PM_JOONG_L` | 중기목록·시간당중기사용료 인쇄 원장을 구성 | 일반·변형·가로형 구분 외 입력 칸 `미확인` | + +### 2.5 원가계산·제비율 — 6개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wM_KanJub` | 설계 제잡비를 계산하고 원가계산서를 만든다 | `원설계 / 1차변경 / 2차변경 / 3차변경`; `제잡비 원가계산방식 / 제잡비 표준시장방식`; 간접비·총공사비 절사 단위; `당초/변경 대비인쇄 / 변경분만 인쇄` | +| `wM_KAN_RATE` | 제잡비 요율표를 조회·수정·저장·출력 | 상단 `수정 / 취소 / 저장 / 출력 / 종료 / 불러오기`; 요율표 선택 콤보; 실제 탭은 DFM §4 참조 | +| `PM_KAN_RATE` | 제잡비 요율표 인쇄 | 공사규모 구간별 기초액·요율, 일반관리비 등 출력; 입력 화면 아님 | +| `wM_GetAMT` | 내역에서 잡비 계산 기초금액을 불러온다 | `모두보기 / 소계(+=) / 계(+==) / 합계(+===) / 총계(+====) / 총합계(+=====)` | +| `wM_KanJub_K` | 기성 제잡비 계산 | 집계수준 `소계`~`총합계 이상`; 부가세 `(0) 직접입력 / (1) 공급가액의 10% / (2) 재료비의 10% / (3) 재료비+산출경비의 10%`; 절사 단위 | +| `PM_Won_A`·`PM_Won_A_C`·`PM_Won_A_L` | 공사원가계산서 출력 원장을 구성 | 일반·변경·가로형 구분 외 입력 칸 `미확인` | + +### 2.6 인쇄·출력 — 7개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `PM_Prt` | 전체 인쇄 메뉴와 출력 모양 설정 | 글꼴 `굴림체 / 궁서체 / 바탕체 / 돋움체`; 코드·호표·분류번호 등 인쇄 필드; `수량/단위 / 단위/수량 / 표준`; `일반 / 토개공 / 수자원공사`; `세로 / 가로`; `일반 / 산근` | +| `PM_Prn_Ord` | 보고서 인쇄순서 지정 | `당초 설계 / 변경 설계 / 분할 설계 / 실적 공사 / 할증 내역 / 견적 내역 / 계약 내역 / 실행 예산 / 기성 내역 / 설계 변경 / 굴착·복구 / 굴착 변경` | +| `PM_Sel_St` | 인쇄할 공종과 집계수준 선택 | `소계(+=) 이상`~`총합계(+=====)`; 방향 `아래로 / 위로` | +| `wM_Sel_Prn` | 선택 영역·설계차수·표준시장단가 보정 후 인쇄 | `당초설계 / 변경설계 / 분할설계`; `7레벨공종이하 내역전체 / 해당레벨`; 비노무비 처리 3택; 노무비 끝전 보정 3택; `1차변경`~`8차변경` | +| `PM_Nae*` 계열 | 총괄·설계·분할·관급·견적·계약 내역서 출력 | 일반·가로·변경·계약 등 변형; 세부 입력 칸 `미확인` | +| `PM_Col_Dan*`·`PM_Col_X*` | 재료·노무·경비·중기·일위·산근 수량금액 집계표 출력 | 일반·가로·X 변형; 세부 입력 칸 `미확인` | +| `PM_SanMok*`·`PM_Dan*` | 단가 목록·단가대비·단가산출 목록 출력 | 일반·가로·인쇄 변형; 세부 입력 칸 `미확인` | + +### 2.7 설정 — 5개 + +| 화면 | 무엇을 하나 | 주요 입력 칸·선택지 | +|---|---|---| +| `wM_Set_Sys` | 계산 초기값과 표시 자리수 설정 | `KCD/ACD/JCD/ECD/BPN`: `0 / 1 / 2 자리`; `ZCD`: `2 / 3 자리`; 환율범위 `시간당중기사용료 단독 / 중기 / 대가 / 산근 / 내역 전체`; 노임계수 `터널 / 장대터널 / 펌프준설선 / GRAB준설선 / 버켓준설선` | +| `wM_Set_Init` | 프로젝트 계산 초기값 설정 | 구체적인 탭·칸 `미확인` | +| `wM_Set_JLab` | 직종·노임 관련 초기 설정 | 구체적인 칸 `미확인` | +| `uM_XML_Info` | 공사원가 호환규정 XML 변환 설정 | 공사유형 `토목 / 건축 / 설비 / 전기 / 통신 / 조경 / 소방 / 국가유산수리공사` | +| `wSrch_PL` | 프로젝트 DATA 검색 | 종류 `노무비 / 재료비 / 경비 / 중기비 / 일위대가 / 단가산출 / 일식·표준시장 / 내역서` | + +## 3. 화면 ↔ 출력 시트 30종 + +`print_form_map.txt`의 export 연결과 19번 §3의 실제 시트명을 합쳤다. `PM_*`은 인쇄용 폼이며 편집 화면에서 직접 호출되는지는 정적 자료만으로 확인되지 않은 경우가 있다. + +| 화면·폼 계열 | 실제 출력 시트 | +|---|---| +| `wM_KanJub` → `PM_Won_A*` | 공사원가계산서 | +| `wBoq` → `PM_Nae*` | 설계내역서 · 착공내역서 · 총괄설계내역서 · 총괄내역서 | +| `wBoq`/변경 내역 → `PM_Nae*` 또는 `CHANGE_*` | 설계변경내역서 · 설계변경금액대비표 · 설계변경증감대비표 | +| `wM_Edit_iLWi` → `PM_iLWi*` | 일위대가표 | +| `wM_Edit_San` → `Calq_San`·`Edit_SanKun` | 단가산출근거 | +| `wM_JabPum`/중기 원장 → `PM_JOONG*` | 중기사용료 | +| `wM_Boxa` 또는 단가비교 → `PM_Col_Dan*` | 자재단가대비표 | +| `wM_Set_Sys` 관련 설정 출력 | 환율및기초자료 · `〓 INITIAL 〓` — 정확한 `PM_*` 호출은 `미확인` | +| `wM_JabPum` → `PM_SanMok*` | 재료비목록표 · 노무비목록표 · 경비목록표 · 일식견적목록표 | +| 일위대가 원장 → `PM_SanMok*` | 일위대가목록표 | +| 단가산출 원장 → `PM_SanMok*` | 단가산출근거목록표 | +| 중기 원장 → `PM_JOONG*` | 중기목록표 | +| 내역 집계 → `PM_Col_Dan*` | 재료비수량금액집계표 · 노무비수량금액집계표 · 경비수량금액집계표 | +| 중기 집계 → `PM_Col_Dan*` | 중기시간금액집계표 | +| 일위대가 집계 → `PM_Col_Dan*` | 일위대가수량금액집계표 | +| 단가산출 집계 → `Calq_San`·`Edit_SanKun`·`PM_Col_Dan*` | 단가산출근거수량금액집계표 | +| 일식견적 집계 → `PM_Col_Dan*` | 일식견적수량금액집계표 | +| 인쇄 메뉴의 보고서 목록 | `〓 목 차 〓` · `※※안내※※` — 생성 폼 `미확인` | + +30종은 위 대응에 모두 들어간다. 다만 `환율및기초자료`, `〓 INITIAL 〓`, `〓 목 차 〓`, `※※안내※※`의 정확한 생성 폼 **4개 연결은 미확인**이다. + +## 4. DFM 3벌의 실제 배치 + +### 4.1 `TWM_KANJUB.dfm` — 제잡비 계산 입력 화면 + +- 상단 고정 패널 다음에 본문 `TPageControl(Pg_Ctrl)`이 놓인다. +- 상단 칸 차례는 기본값 설정, 차수 선택, 계산방식, 요율, 출력, 변경/계산, 종료 계열이다. 확인되는 콤보는 `Cmbx_PORD1_H` → `cBx_K_JeolSa` → `cBx_Chasu` → `cBx_W_Type` 순이다. +- `cBx_Chasu`: `원설계 / 1차변경 / 2차변경 / 3차변경`; `cBx_W_Type`: `제잡비 원가계산방식 / 제잡비 표준시장방식`. +- 본문 탭은 아래쪽 탭바(`TabPosition=tpBottom`)이고 여러 줄 탭(`MultiLine=True`)이다. DFM에서 `TbSh_T`, `TbSh_TA` 등 원가 형식별 탭 객체가 확인된다. 탭 캡션 전체와 탭별 항목 수는 `미확인`이다. +- 각 탭 안은 `TScrollBox`로 구성되어 긴 제잡비 항목을 세로로 스크롤한다. + +### 4.2 `TWM_KAN_RATE.dfm` — 요율 조회·편집 화면 + +- 상단 고정 도구막대: `수정(&E)` → `취소(&C)` → `저장(&S)` → `출력(&P)` → `종료(&Q)` 순이며 `불러오기` 버튼과 요율표 선택 콤보가 함께 있다. +- 본문은 `TPageControl(Pg_Ctrl)`이며 탭바가 아래쪽에 있다. 확인되는 첫 탭 객체는 `Tab_Rn`이다. +- 탭 안은 흰색 `TScrollBox`와 여러 요율 그리드로 구성된다. 확인되는 묶음은 고용 관련 요율(`rGrd_Rate_Kobo`), 산업안전 관련 요율(`rGrd_Rate_Anjun`) 등이다. +- 그리드 열은 공사규모 구간과 `요율`, `기초액`, `% + 천원` 형식을 함께 가진다. 즉 한 화면에서 단순 단일요율뿐 아니라 규모별 기초액 가산식도 편집한다. +- 전체 탭 캡션과 모든 그리드의 화면상 정확한 순서는 바이너리 DFM 문자열만으로 복원되지 않아 `미확인`이다. + +### 4.3 `TPM_KAN_RATE.dfm` — 요율표 인쇄 화면 + +- 편집 위젯이 아니라 `THRPage` 기반 보고서 한 장이다. `ReportHeader` 뒤에 요율표가 이어진다. +- 제목·구간 라벨 → 금액 구간 → 기초액 → 요율 순으로 열이 반복된다. +- 확인되는 구간 예시는 `10억~50억 미만`, `70억~120억`이며 표시 형식은 `00.00 %` 또는 `(#,0 천원 + …)`이다. +- `일반관리비` 등 항목별 표가 인쇄 좌표에 고정 배치된다. `TWM_KAN_RATE`의 편집 그리드를 종이용 규모구간 표로 펼친 구조다. + +## 5. B08·B09 화면 구조에 주는 근거 + +```text +수량산출서(wQtyList/uM_ConQTY) + → 내역 입력(wBoq) + → 일위대가·단가산출 편집 + → 기준자료(단가·노임·중기) + → 제잡비 계산(wM_KanJub) + → 인쇄 메뉴(PM_Prt)에서 30종 시트 선택 +``` + +- B08 근거: 수량산출서는 코드 종류로 필터한 별도 화면이고, 내역서와 분리되어 있다. +- B09 근거: 직접비 편집, 기초단가, 일위대가·단가산출, 제잡비 요율, 출력이 각각 별도 화면이며 마지막에 인쇄 메뉴가 묶는다. +- 제잡비 입력 화면과 요율 관리 화면도 분리된다. 요율 관리 화면은 수정·저장·출력의 독립 흐름을 가진다. +- 출력은 계산 화면을 그대로 인쇄하지 않고 `PM_*` 보고서 폼을 따로 거친다. + +## 6. 미확인 + +화면 단위 미확인 **10개**: + +1. `wM_From_XLS`의 열 매핑·불러오기 옵션 +2. `wSameCode`의 동일 코드 처리 선택지 +3. `uM_ConQTY`의 변환 방향·연결 키 +4. `wM_Edit_USER`의 사용자식 편집 범위 +5. `wM_iLWi_San`의 조회·편집 경계 +6. `wM_JajeXLS`의 입력·출력 방향 +7. `wM_Set_Init`의 탭과 칸 +8. `wM_Set_JLab`의 직종·노임 설정 칸 +9. `TWM_KANJUB`의 전체 탭 캡션·항목 수 +10. `TWM_KAN_RATE`의 전체 탭 캡션·그리드 순서 + +출력 연결 미확인 **4개**: `환율및기초자료` · `〓 INITIAL 〓` · `〓 목 차 〓` · `※※안내※※`의 정확한 생성 폼. + +이 문서는 화면 구조 근거만 정리한다. Aislo B08·B09의 구현안이나 화면 설계는 포함하지 않는다. diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/35_형식과_단계의_공통과_차이.md b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/35_형식과_단계의_공통과_차이.md new file mode 100644 index 000000000..94bb81f8f --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/35_형식과_단계의_공통과_차이.md @@ -0,0 +1,84 @@ +# 원가계산 4형식과 설계 뒤 네 단계 — 공통과 차이 + +점검일: 2026-09-13 +근거: [12번](12_원가계산_4형식과_RATE표_복원.md) · [27번](27_계약_실행_기성_단계.md) · [06번](06_규칙1_RATE와_규칙7_간접비_끝전조정.md) · [08번](08_규칙6_8_9_재계산_변경설계_일괄변경.md) + +## 결론 + +**「공식 집합이 통째로 다르다」는 표현은 지나치다.** 네 형식 모두 `직접공사비 → 간접비 → 일반관리비 → 이윤 → 공급가액·부가세`의 큰 사슬을 공유한다. 차이는 직접비를 묶는 방식, 기타경비의 묶음/세분, 일부 항목의 존재, 밑수와 요율표이다. + +- 큰 계산 단계: **6단계 중 6단계 공통(10할)** +- 화면상 간접비 항목: **공통 핵심 15개 / 합집합 25개(6할)** +- 따라서 구현 관점에서는 **공통 계산 사슬 + 형식별 항목·밑수·요율 설정**으로 읽는 것이 근거에 맞다. + +## 1. 네 형식 비교 + +`같음`은 항목명만이 아니라 확인된 계산 역할과 밑수까지 같은 경우이다. + +| 계산 단계 | 일반 `T` | 수공 `S` | 실적일반 `TA` | 실적수공 `SA` | +|---|---|---|---|---| +| 직접공사비 | 재료비·직접노무비·산출경비를 풀어 표시 | `순공사비` 묶음 | `직접공사비` 묶음 | `직접공사비` 묶음 | +| 간접노무비 | 같음 | 같음 | 같음 | 같음 | +| 산재·고용보험 | 총노무비 × 요율 | 같음(직노+간노로 명시) | 같음 | 같음(직노+간노로 명시) | +| 건강·장기요양·연금·퇴직공제 | 같음 | 같음 | 같음 | 같음 | +| 산업안전보건관리비 | 재료+직노+도급자관급자재 | 같음 | 직접공사비+도급자관급자재 | 같음 | +| 안전·품질관리비 | 별도내역 | 같음 | 같음 | 품질은 있음; 안전관리비는 목록에 없어 **미확인** | +| 기타성 경비 | `기타경비` 한 항목, 재료+노무비 | 8개 경비로 분리, 항목마다 5개 밑수 중 선택 | `기타경비` 한 항목, 직접공사비 | 8개 경비로 분리, 항목마다 5개 밑수 중 선택 | +| 이행·하도급·환경·기계대여 보증 등 | 공통 항목군 | 공통 항목군 | 공통 항목군 | 공통 항목군 | +| 보증·환경 항목 밑수 | 하도급·환경·기계대여는 재료+직노+산출경비 | 같음 | 하도급·기계대여는 직접공사비; 환경은 재료+직노+산출경비 | 재료+직노+산출경비 | +| 일반관리비 | 순공사원가+관리품목자재대 | 순공사비+간노+경비 | 직접공사비+간접공사비+관리품목자재 | 직접공사비+간접노무비 | +| 이윤 | 소계-재료비 | 순공사원가+일반관리비-재료비 | 복원식은 직접+간접공사비+일반관리비, 화면 문구와 충돌해 **미확인** | `TA`와 같은 충돌로 **미확인** | +| 공급가액·부가세·총액 | 같음 | 같음 | 같음 | 같음 | + +> 12번의 `TA 환경보전비=직접공사비` 기술은 06번의 후속 정정과 충돌한다. 이 표는 06번의 명시적 정정인 `재료비+직접노무비+산출경비`를 따른다. + +### 공통 핵심 15개 + +간접노무비, 산재보험료, 고용보험료, 건강보험료, 장기요양보험료, 연금보험료, 퇴직공제부금비, 산업안전보건관리비, 품질관리비, 공사이행수수료, 하도급대금지급보증수수료, 환경보전비, 건설기계대여금지급보증수수료, 석면피해구제분담금, 임금채권부담금이다. + +합집합 25개는 이 15개에 안전관리비, 일반/실적일반의 기타경비 1개, 수공 계열의 세부 경비 8개를 더한 화면상 항목 수이다. `SA`의 안전관리비 존재가 미확인이므로 **6할은 최대 1개 항목의 집계 오차 가능성**이 있다. + +## 2. 형식별 고유 차이 + +| 형식 | 빠지거나 갈리는 항목 | 달라지는 밑수 | 주 요율표 | +|---|---|---|---| +| 일반 `T` | 기타경비를 한 항목으로 계산; 고용개선지원비(PS단가) 별도 묶음 | 기타경비=재료+노무; 일반관리비에 관리품목자재 포함; 이윤에서 재료 제외 | `RATE_R`, `RATE_R1` + 공통 보조표 | +| 수공 `S` | 기타경비 대신 수도광열·복리후생·소모품/사무용품·여비/교통/통신·세금/공과·폐기물·도서인쇄·지급수수료 8개 | 각 세부 경비가 `재/직노/노/재+직노/재+노` 중 선택; 일반관리비·이윤 밑수도 수공식 | `RATE_R`, `RATE_R1`, `RateB` + 공통 보조표 | +| 실적일반 `TA` | 직접공사비 묶음 사용; 기타경비 한 항목; 국가유산 공종 없음; 노무형태·조정계수·재료비 방식 축 추가 | 안전비·기타경비 및 일부 보증은 직접공사비 계열; 일반관리비 밑수 변경; 이윤 밑수는 **미확인** | `RATE_RA`, `RATE_RA1`, `An_A`, `An_A1` + 공통 보조표 | +| 실적수공 `SA` | 실적 직접공사비 묶음+세부 경비 8개; 안전관리비 존재 **미확인**; 실적 추가축 사용 | 세부 경비는 `RateB`; 일반관리비는 직접공사비+간접노무비; 이윤 밑수는 **미확인** | `RATE_RA`, `RATE_RA1`, `RateB`, 실적 안전표 + 공통 보조표 | + +공통 보조표는 고용보험, 산업안전, 환경, 이행보증, 하도급보증, 기계대여보증 등의 항목별 표이다. 즉 `T/S`, `TA/SA`가 각각 규모·기간 요율표를 공유하고, `S/SA`가 세부 경비 밑수표를 추가로 공유한다. + +## 3. 설계 뒤 네 단계 + +| 단계 | 설계 대비 달라지는 계산 | 확인 수준 | +|---|---|---| +| 계약 | 하나의 낙찰률이 아니라 **노무·재료·경비별 단가 적용률**을 사용한다. 기초단가 적용, 적용률을 반영한 일위대가/산출근거 생성, 동일코드 계약단가 개별 생성, 비과세자재 적용을 선택할 수 있다. 행별 적용 제외가 있고 0%는 W코드 공내역을 만든다. | 구조 확인; 실제 계약 표본이 없어 원 단위 결과 **미확인** | +| 실행예산 | 설계 중기사용료와 별도로 `1단위당 중기실행단가`, `1단위=시간`, 실행수량을 둔다. 성분별 절사 차액은 기본/노무/재료/경비 중 지정한 순서로 보정한다. | 중기 화면 구조 확인; 전체 실행예산 및 실제 값 **미확인** | +| 기성 | 설계처럼 간접비마다 밑수×요율을 다시 적용하지 않고 **금회직접공사비 × 계약제잡비율** 단일 요율을 쓴다. 계약·전회·금회·누계와 각 비율을 보존한다. 부가세는 직접입력/공급가액 10%/재료비 10%/(재료비+산출경비) 10% 중 선택한다. | 구조 확인; 기성 표본이 없어 회차별 값과 `QTY1_B/N/O/H` 뜻 **미확인** | +| 준공 | 출력은 `계약금액 | 준공금액` 두 열이다. | 최종 누계를 옮기는 것으로 보이지만 별도 계산 규칙은 **미확인** | + +설계의 부가세 선택지는 공급가액 10%, 재료비 10%, 없음, 산림조합, 산림조합-면세품이다. 따라서 부가세라는 이름은 같아도 **기성의 선택 집합은 설계와 다르다.** + +## 4. 변경설계에서 당초값을 그대로 쓴 항목 + +| 판정 | 항목 | 근거 | +|---|---|---| +| 확정 | 건강보험료 | 2025 변경 표본에서 직접노무비가 증가했지만 당초=변경 | +| 확정 | 노인장기요양보험료 | 같은 표본에서 당초=변경 | +| 확정 | 연금보험료 | 같은 표본에서 당초=변경 | +| 재계산 확정 | 간접노무비·산재보험료·고용보험료 | 같은 표본에서 변경 직접노무비에 맞춰 재계산됨 | +| 미확인 | 위 항목 이외 전부 | 추가 `carry_original` 실데이터가 없음 | + +보험료 변경 화면에는 `0 직접노무비×요율`, `1 당초액+직접노무비 증감×요율`, `2 당초액+증감액(비율)`의 세 모드가 있다. 그러나 **어느 보험 항목에 어느 모드가 허용·선택되는지**는 정적 자료만으로 전부 확정되지 않았다. 현재 확정 가능한 답은 **건강·장기요양·연금 외 추가 항목 없음**이 아니라 **추가로 확인된 항목 없음**이다. + +## 5. 미확인 6건 + +1. `TA/SA` 이윤 밑수: 복원식과 화면의 `(노+경+일)×율` 문구가 충돌한다. +2. `SA`에 안전관리비가 실제로 빠지는지, 복원 목록의 번호 누락인지. +3. 변경설계에서 건강·장기요양·연금 외 `carry_original` 대상 및 모드 선택 조건. +4. 계약 단가 적용 결과의 원 단위 실증. +5. 실행예산 전체 계산과 기성 회차별 값·`QTY1_B/N/O/H`의 정확한 뜻. +6. 준공금액의 별도 계산 여부와 최종 누계 이관 규칙. + +이 여섯 건은 표본 또는 통제 동적 시험 없이 추정해 채우지 않는다. diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/30_원자료/임도품셈_판_대응표.csv b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/30_원자료/임도품셈_판_대응표.csv new file mode 100644 index 000000000..61a9721c8 --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/30_원자료/임도품셈_판_대응표.csv @@ -0,0 +1,15 @@ +"STmate_인용판","과거_절","과거_인용명","STmate_적용공종","2017_임도품셈","2018_통합품셈","2024_개정품셈","현행_절","현행_FP코드","현행_명칭","현행_표ID","판정","근거와_경계" +"판본 미확인","번호 미기재","되메우기 및 다짐","구조물 되메우기(사질양토) 기계100%(굴삭기 0.7㎥)","미확인","미확인","8-13 되메우기 및 다짐","9-14-1","FP-09-14-01","되메우기","F0279","확정","2024 8-13과 현행 9-14-1의 굴삭기 0.7㎥·k=0.9·f=0.69·E=0.65·Cm=18(90°)가 동일. 인용 절·판본은 미확인이고 대응은 되메우기 구성에 한정" +"2017 임도표준품셈","5-1-2 사","암터파기","구조물터파기(육상암 0~1m) 기계100%(굴삭기 0.7㎥)","5-1-2 사 육상 암절취(0~1m)","4.27.2 구조물터파기","8-12 사 육상 암절취(0~1m)","9-13-7","FP-09-13-07","육상 암절취(0~1m)","F0267","확정","2018 목차에 5-1-2→4.27.2가 직접 병기되고 2024·현행의 인력10%+장비90%, 브레이커 Q=3.5, 치즐 0.006, 굴삭기 0.7㎥ 계수가 동일" +"2017 임도표준품셈","7-3","노면정리(건설품셈 기계 시공 적용)","노면정리","7-3 노면다짐","4.40 노면다짐","미확인","미확인","미확인","미확인","미확인","미확인","2017과 2018은 해당 절에서 건설품셈 기계화 시공을 외부 참조할 뿐 자체 표가 없음. 2024 목차에 노면다짐이 없고 현행 3-4-3은 인력식 노면 보수이므로 연결 금지" +"2018 통합 산림사업 표준품셈","4-16-2","선떼붙이기공","단끊기","선행 사방품셈 4-2-2","4.16.2 선떼붙이기공","4-16 선떼붙이기공","5-16 / 5-16-1 / 5-16-2","FP-05-16 / FP-05-16-01 / FP-05-16-02","선떼붙이기공 / 단끊기 / 선떼붙이기","F0121 / F0122","확정","2018 절과 2024 절이 직접 이어지고 현행은 단끊기·선떼붙이기로 분리. 일대다 대응으로 확정하며 대표 하위 절 하나로 자동 치환하지 않음" +"2018 통합 산림사업 표준품셈","4-16-2","절취없는 단끊기","단끊기 절취없음","선행 사방품셈 4-2-2","4.16.2 선떼붙이기공","4-16 가 단끊기","5-16-1","FP-05-16-01","단끊기","F0121","확정","2024와 현행의 수평잡기·단정리 0.34, 잡석·뿌리정리 0.36, 절·성토면고르기 1.17인/100m가 동일. 절취없는 구성에 한정" +"2017 임도표준품셈","3-6 가","임도성토면 다짐","성토사면다짐 B/H0.7+콤팩터","3-6 임도성토면 다짐","4.24 임도성토면 다짐","8-18 임도성토면 다짐","9-17-1","FP-09-17-01","비탈면 다짐","미확인","확정","2018 목차에 3-6→4.24가 직접 병기되고 2017·2024 모두 굴삭기 부착 유압식 진동 콤팩터+굴삭기 0.7㎥, A=77.7㎡/hr로 동일. 현행은 명칭을 비탈면 다짐으로 변경" +"판본·인용 오류 미확인","9-3","임도성토면다짐","성토사면다짐 굴삭기0.7㎥","3-6 임도성토면 다짐","4.24 임도성토면 다짐","8-18 임도성토면 다짐","9-17-1","FP-09-17-01","비탈면 다짐","미확인","확정","공종·장비는 2017 3-6→2018 4.24→2024 8-18→현행 9-17-1로 이어짐. 현행 원문이 임도성토면 다짐, 굴착기 부착 유압식 진동 콤펙터+굴착기 0.7㎥, A=77.7㎡/hr를 직접 명시함. STmate의 9-3과 Q=716.8은 원문과 불일치하므로 절 대응만 확정하고 해당 번호·값 복사는 금지" +"2018 통합 산림사업 표준품셈","4-16-4","파종공","씨뿌리기(성토면) 줄파종","선행 사방품셈 4-2-4","4.16.4 파종공","4-18 씨뿌리기(줄)","5-18","FP-05-18","씨뿌리기(줄)","F0125","확정","2024·현행의 종자 2g, 비료 29.3g, 특별인부 0.00055, 보통인부 0.00838인/m가 STmate 구성과 일치" +"2017 임도표준품셈","3-3-2","굴삭기 적용 암절취","암절취 굴삭기0.7㎥+대형브레카","3-3-2 암절취","4.3.3 암절취","8-3 암절취","9-4 / 9-4-1 / 9-4-2","FP-09-04 / FP-09-04-01 / FP-09-04-02","암절취 / 암파쇄 / 집토","F0241 / F0242","확정","2018 목차에 3-3-2→4.3.3이 직접 병기되고 2024·현행은 암파쇄+집토로 분리. 브레이커 작업능력 5.0·3.4·2.6㎥/hr와 굴삭기 0.7㎥ 계수가 동일한 일대다 대응" +"2017 임도표준품셈","2-3","제근(100㎡당)","제근","2-3 제근","4.20.2 제근","8-22 제근","9-21","FP-09-21","제근","F0294","확정","2018 목차에 2-3→4.20.2가 직접 병기되고 2024 8-22를 거쳐 현행 9-21로 이동. 밀림 보통인부 0.05인/100㎡ 일치" +"2017 임도표준품셈","5-1-1 나","암절취","측구터파기(암) 기계100%(굴삭기 0.4㎥)","5-1-1 나 암절취","4.27.1 나 암절취","8-11 나 암절취","9-12-2","FP-09-12-02","암절취","F0259","확정","2018 목차에 5-1-1→4.27.1이 직접 병기됨. 2024·현행의 연암 Q=3.5, 치즐 0.006 및 측구터파기 조건이 동일. STmate 장비 규격 0.4㎥ 표기는 원문 0.7㎥와 불일치하므로 값 복사는 금지" +"2017 임도표준품셈","3-7 가","층따기","층따기","3-7 층따기","4.25 층따기","8-19 층따기","9-18","FP-09-18","층따기","F0287","확정","2018 목차에 3-7→4.25가 직접 병기되고 2017·2024·현행 모두 굴삭기 0.7㎥, K=0.9, f=1/1.3, E=0.7, Cm=22(180°)가 동일" +"2017 임도표준품셈","3-3-1","토사 굴삭기 적용","토사절취(흙깎기) 굴삭기0.7㎥","3-3-1 토사","4.3.2 토사깎기","8-2 토사깎기","9-3-2","FP-09-03-02","기계","F0240","확정","2018 목차에 3-3-1→4.3.2가 직접 병기되고 2017·2024·현행의 굴삭기 0.7㎥, K=0.9, f=1/1.30, E=0.55~0.45, Cm=20(135°)가 동일" +"2018 통합 산림사업 표준품셈","4-39","쇄석·혼합석 부설","혼합석부설 T=0.1m","7-2 쇄석·혼합석 부설(선행)","4.39 쇄석·혼합석 부설","10-4 쇄석·혼합석 부설","11-4","FP-11-04","쇄석․혼합석 부설","F0328","확정","2018 목차에 7-2→4.39가 직접 병기됨. 2024·현행의 굴삭기 0.7㎥, q=0.7, k=0.55, f=1, E=0.35, Cm=20(135°)가 동일. T=0.1m 환산은 별도 계산" diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/README.md b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/README.md index 070537620..b03cf8c8c 100644 --- a/resources/knowledge/original/경쟁사 프로그램/STmate 분석/README.md +++ b/resources/knowledge/original/경쟁사 프로그램/STmate 분석/README.md @@ -102,7 +102,7 @@ | `jb_shapes.py` · `jb_block_map.py` · `jb_expansion_diff.py` | JB 43개 형태·블록·확장 차이 | | `rate_grid_defs.py` · `settings_axis_map.py` | 폼에서 그리드·선택축 추출 | -후속 레시피 분석은 `20_분석/31_일위대가_레시피_사전.md`와 `20_분석/32_특수산식과_현행품셈_대조.md`를 본다. +후속 레시피 분석은 `20_분석/31_일위대가_레시피_사전.md`부터 `20_분석/33_임도품셈_판_대응표.md`까지 본다. ## 다음 diff --git a/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/CHANGELOG.md b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/CHANGELOG.md new file mode 100644 index 000000000..b1514f251 --- /dev/null +++ b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/CHANGELOG.md @@ -0,0 +1,13 @@ +# 건설폐기물 수집·운반차량 덮개 재질에 관한 고시 — 변경이력 + +기준: 현행(2016.07.01) + +**개정 이력** + +| 시행일 | 구분 | +|---|---| +| 2016.07.01 | 현행 | + +## 개정문 + +> diff --git a/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_index.md b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_index.md new file mode 100644 index 000000000..56088661d --- /dev/null +++ b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_index.md @@ -0,0 +1,8 @@ +# 건설폐기물 수집·운반차량 덮개 재질에 관한 고시 — 문서 목록 +> 총 3건 (본문 3 · 별표 0 · 첨부 0 · 압축 0 · KCS 0) + +## 본문 + +- [메타정보](<_meta.md>) +- [변경이력]() +- [현행 20160701](<현행_20160701.md>) diff --git a/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_meta.md b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_meta.md new file mode 100644 index 000000000..eed8d8075 --- /dev/null +++ b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/_meta.md @@ -0,0 +1,10 @@ +# 건설폐기물 수집·운반차량 덮개 재질에 관한 고시 + +- 정식명칭: 건설폐기물 수집·운반차량 덮개 재질에 관한 고시 +- 목록상 명칭: 건설폐기물 수집·운반차량 덮개 재질에 관한 고시 +- 식별자: `행정규칙ID=2100000043360` (불변) +- 소관: 기후에너지환경부 +- 종류: 고시 +- 현행 시행일: 2016.07.01 / 공포 2016.03.24 제2016-59호 +- 출처: https://www.law.go.kr/행정규칙/건설폐기물수집·운반차량덮개재질에관한고시 +- 수집 파일: 현행:20160701 diff --git a/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/현행_20160701.hwp b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/현행_20160701.hwp new file mode 100644 index 000000000..a666b1f78 --- /dev/null +++ b/resources/knowledge/original/법률/건설폐기물 수집·운반차량 덮개 재질에 관한 고시/현행_20160701.hwp @@ -0,0 +1,1083 @@ + + +]> +건설폐기물 수집·운반차량 덮개 재질에 관한 고시 +법제처 법령정보센터 +2026.09.13 + + + + + + + +{이미지파일목록} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^1. +^2. +^3) +^4) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +