From 0d4f46ecbede57b9f5e53a432c744cfbb6a43bfc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 05:30:32 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(b05=C2=B7b08):=20=EB=B9=84=ED=83=88?= =?UTF-8?q?=EB=A9=B4=20=EB=85=B9=ED=99=94=20=E3=8E=A1=20=EB=84=B7(?= =?UTF-8?q?=EC=83=88=EC=8B=AC=EA=B8=B0=C2=B7=ED=8F=89=EB=96=BC=C2=B7?= =?UTF-8?q?=EC=A4=84=EB=96=BC=C2=B7=EB=B9=84=ED=83=88=EB=8D=AE=EA=B8=B0)?= =?UTF-8?q?=20=E2=80=94=20=EA=B5=AC=EA=B0=84=20=EC=82=AC=EB=A9=B4=20?= =?UTF-8?q?=EB=A9=B4=EC=A0=81=20=C3=97=20=EB=8C=80=EC=83=81=20=EB=A9=B4=20?= =?UTF-8?q?=EC=B9=B8=20=C2=B7=20=EB=96=BC=2011=EB=A7=A4/3.33=EB=A7=A4(?= =?UTF-8?q?=ED=92=88=EC=85=88=205-22-1=C2=B75-23-1)=20=C2=B7=20=EC=B4=88?= =?UTF-8?q?=EB=A5=98=EC=A2=85=EC=9E=90=20=EC=82=B4=ED=8F=AC=EB=8A=94=20?= =?UTF-8?q?=EA=B3=B5=EB=B2=95=EC=97=90=EC=84=9C=20=EB=B9=BC=EA=B3=A0=20?= =?UTF-8?q?=EC=B9=B8=20=EB=B0=91=20=EA=B9=8C=EB=8B=AD=20=C2=B7=20=EB=8B=A8?= =?UTF-8?q?=20=EC=A7=81=EA=B3=A0=C2=B7=EC=84=A0=EB=96=BC=20=EA=B8=89?= =?UTF-8?q?=EC=88=98=20=EC=B9=B8=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C(12=EC=9E=A5=20D=20=EB=85=B9=ED=99=94=20=C2=B7=20?= =?UTF-8?q?=EB=B8=8C=EB=A0=88=EC=9D=B8=20=E2=93=90~=E2=93=93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 겹친 사면(초류종자살포·면고르기)은 자동으로 안 빼고 맨 앞 사유 · 평떼 떼꽂이 5-22-3 [주] 읽기 사유 - 혼합석 사유 정정: 토사 = B06 측점 토사 토글(구성비는 암 몫만) · 사전터파기 「산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음」 - UnitQuantity 700줄 — 늦은 import 를 한 벌(_lazy)로 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 43 +++++- .../B08_Quantity_Engine_GravelSurfacing.py | 4 +- .../B08_Quantity_Engine_UnitQuantity.py | 18 ++- .../B08_Quantity_Engine_UnitQuantity_Base.py | 5 + ...antity_Engine_UnitQuantity_Revegetation.py | 142 ++++++++++++++++++ B08_Quantity/B08_Quantity_Router_Material.py | 7 + .../work_item_mapping_2026-01-01.json | 64 +++++++- .../tester/test_b05_not_in_table_marks.py | 5 + resources/tester/test_b08_gravel_surfacing.py | 6 + .../tester/test_b08_revegetation_unit.py | 115 ++++++++++++++ 10 files changed, 399 insertions(+), 10 deletions(-) create mode 100644 B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py create mode 100644 resources/tester/test_b08_revegetation_unit.py diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 776801fe..fd87b6a9 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -2477,7 +2477,6 @@ "input": "select", "choices": [ "씨뿌리기(줄파종)", - "초류종자 살포", "새심기", "선떼붙이기", "줄떼", @@ -2489,8 +2488,50 @@ "나무심기" ], "default": null, + "default_basis": "「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)", "required": true, "phase": "detail" + }, + { + "key": "face", + "label": "대상 면", + "input": "select", + "choices": ["절토면", "성토면", "양쪽"], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "cover_kind", + "label": "덮개(비탈덮기)", + "input": "select", + "choices": ["거적", "짚망", "방초·야자섬유매트"], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비탈덮기일 때만 고름 — 비면 비탈덮기 줄이 안 섬(품셈 5-25 거적덮기 ㎡ · 5-28-1 짚망 · 5-28-2 매트 · 재료가 공종을 가름)" + }, + { + "key": "step_height_m", + "label": "단 직고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음", + "not_in_table": "아직 표에 안 쓰임 — 길이형(선떼·조공) 산출식이 아직 없음(선떼는 사방기술교본 표 2-2-6 으로 다음 차례)" + }, + { + "key": "sod_grade", + "label": "선떼 급수", + "input": "select", + "choices": ["1급", "2급", "3급", "4급", "5급", "6급", "7급", "8급", "9급"], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음", + "not_in_table": "아직 표에 안 쓰임 — 선떼붙이기 산출식이 아직 없음(사방기술교본 표 2-2-6 급별 m당 매수로 다음 차례)" } ] }, diff --git a/B08_Quantity/B08_Quantity_Engine_GravelSurfacing.py b/B08_Quantity/B08_Quantity_Engine_GravelSurfacing.py index 50097a86..96992cf0 100644 --- a/B08_Quantity/B08_Quantity_Engine_GravelSurfacing.py +++ b/B08_Quantity/B08_Quantity_Engine_GravelSurfacing.py @@ -31,6 +31,7 @@ NOTE_ALL = ( ) NOTE_SOIL = ( "법령은 사질·점토를 가르는데 우리 프리셋은 토사 하나 — 토사 전부를 해당으로 봄(암은 비해당)" + " · 토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)" ) NOTE_GRADE = "종단 경사 = 종단 계획선에서 측점을 감싼 구간 경사 중 큰 값(B05 포장 제안과 같은 식)" NOTE_WIDTH = "폭 = 차도 표준 폭 + 확폭(노견 제외 · 실무 소광 「혼합석부설 B=3.0」)" @@ -40,7 +41,8 @@ NOTE_COMPACTION = ( ) NOTE_EXCAVATION = ( "사전 터파기 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 적용범위 「노면의 토사구간」이라" - " 토사 측점만 · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈" + " 토사 측점만 · 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음" + " · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈" ) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 01e7a0e4..dda73b2c 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -30,6 +30,7 @@ from __future__ import annotations +import importlib from typing import Any, Iterable from B08_Quantity.B08_Quantity_Engine_Formwork import annotate as annotate_formwork @@ -119,7 +120,9 @@ EXPANDERS = { # BOX암거 — m당 × 연장(산림과임업기술 예제 · 울진 2×2 · 2026-09-15 브레인 12장 D). "box_culvert": lambda h, l, o, f=None, r="": _box_culvert()(o), # 콘크리트 포장 — 면적 × 두께 · 거푸집 2L · 수축줄눈(교본 3-2 · 부록 4-7 · 소광 · 2026-09-15). - "pavement_concrete": lambda h, l, o, f=None, r="": _pavement().concrete_pavement(l, o), + "pavement_concrete": lambda h, l, o, f=None, r="": _lazy("Pavement").concrete_pavement(l, o), + # 비탈면 녹화 ㎡ 넷 — 구간 사면 면적 × 고른 면(품셈 5-14·5-22·5-23·5-25·5-28 · 2026-09-15). + "revegetation": lambda h, l, o, f=None, r="": _lazy("Revegetation").revegetation(o), # ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견). # 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.** # 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝). @@ -137,11 +140,9 @@ def _box_culvert(): return box_culvert -def _pavement(): - """콘크리트 포장 모듈을 늦게 가져온다.""" - from B08_Quantity import B08_Quantity_Engine_UnitQuantity_Pavement - - return B08_Quantity_Engine_UnitQuantity_Pavement +def _lazy(part: str): + """`_UnitQuantity_` 전개 모듈을 늦게 가져온다(포장·녹화 · 700줄이라 한 벌로).""" + return importlib.import_module(f"B08_Quantity.B08_Quantity_Engine_UnitQuantity_{part}") def _soil_guard(): @@ -196,7 +197,10 @@ EXPANDER_BILLING: dict[str, Any] = { "soil_guard": lambda options, length: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이) "bed_sill": lambda options, length: ("㎡", _num(options.get("area_m2"), 0.0)), # 돌붙임 ㎡당 "box_culvert": lambda options, length: ("m", _num(options.get("length_m"), 0.0)), # 점 배치 - "pavement_concrete": lambda options, length: _pavement().billing(length, options), # 면적 ㎡ + "pavement_concrete": lambda options, length: _lazy("Pavement").billing( + length, options + ), # 면적 ㎡ + "revegetation": lambda options, length: _lazy("Revegetation").billing(options), # 사면 면적 ㎡ } #: 연장을 **구간(끝 − 시작)** 에서 읽는 종류 — 포장은 물넘이로 나뉘면 길이 칸(10)이 조각마다 diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index 90f72755..d15677b5 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -67,6 +67,11 @@ DESTINATION = { "수축줄눈": "unit_price", "비닐": "material", "철망": "material", + # ⭐ 2026-09-15 비탈면 녹화 ㎡ 넷 — 공종 품(5-14·5-22·5-23·5-25·5-28) · 떼 재료는 위 「떼」(자재). + "새심기": "unit_price", + "평떼": "unit_price", + "줄떼": "unit_price", + "비탈덮기": "unit_price", } #: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄. diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py new file mode 100644 index 00000000..ec05911e --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py @@ -0,0 +1,142 @@ +"""비탈면 녹화 전개식 — **㎡ 넷(새심기·평떼·줄떼·비탈덮기)** (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ). + +근거 + 산림품셈 5-14 새심기(㎡당 · 1㎡당 10주) · 5-22 평떼(5-22-1 떼 0.3×0.3 11매/㎡ · 5-22-2 붙임 ㎡ · + 5-22-3 떼꽂이 ㎡) · 5-23 줄떼(5-23-1 떼 3.33매/㎡ · 5-23-2 붙임 · 5-23-3 떼꽂이) · + 5-25 거적덮기(㎡당) · 5-28 비탈덮기(5-28-1 짚망 · 5-28-2 방초·야자섬유매트) + 교본 7-1 그림 7-8~7-12 는 사진·개념도(치수 없음) — 표준도가 아님. +⭐ 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽). +⚠ 길이형(선떼·조공)은 다음 차례(선떼 = 사방기술교본 표 2-2-6 급별 m당 매수) · 나머지 공법은 사유. +""" + +from __future__ import annotations + +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed + +CUT, FILL = "b06_cut_area_m2", "b06_fill_area_m2" +FACES = {"절토면": (CUT,), "성토면": (FILL,), "양쪽": (CUT, FILL)} +#: 공법 → (성분 이름, 떼 매수/㎡ 또는 None, 근거). 떼 매수 = 품셈 5-22-1·5-23-1 「소요 매수」. +AREA_METHODS: dict[str, tuple[str, float | None, str]] = { + "새심기": ("새심기", None, "품셈 5-14 새심기(㎡당 · 1㎡당 10주 · 요소·인산은 표 안)"), + "평떼": ("평떼", 11.0, "품셈 5-22 평떼 — 떼 0.3×0.3 11매/㎡ · 붙임 5-22-2 · 떼꽂이 5-22-3"), + "줄떼": ( + "줄떼", + 3.33, + "품셈 5-23 줄떼 — 떼 0.3×0.3(30㎝ 간격) 3.33매/㎡ · 붙임 5-23-2 · 떼꽂이 5-23-3", + ), + "비탈덮기": ( + "비탈덮기", + None, + "품셈 5-25 거적덮기 · 5-28 비탈덮기(짚망·매트) — 덮개가 공종을 가름", + ), +} +SOD_SPEC = "0.3×0.3" +LENGTH_METHODS = ("선떼붙이기", "조공") +NOTE_LENGTH = ( + "{method}은(는) 길이형(10m·100m당) — 산출식 아직 없음 · 떼 매수는 품셈이" + " 「사방기술교본 기준표」라 함(선떼 = 표 2-2-6 급별 m당 매수 · 다음 차례)" + " · 단 직고·급수 칸은 기본값 없음(브레인 ⓒ)" +) +NOTE_LATER = ( + "{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)" +) +NOTE_FACE = "대상 면을 안 고름 — 절토면/성토면/양쪽 중 고를 것(공법마다 달라 기본값 없음)" +NOTE_COVER = ( + "비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것" +) +NOTE_OVERLAP = ( + "⚠ 같은 사면이 초류종자살포·면고르기에도 들어 있음 — 겹친 몫을 자동으로 빼지 않음" + "(파종 구간을 줄일지 설계자가 고름 · 2026-09-15 브레인 ⓑ)" +) +NOTE_STAKE = ( + "평떼 5-22-3 [주] 「평떼(0.06인) 품에는 떼꽂이 포함」은 5-12 품에 걸린 것으로 읽음" + " — 5-22-3 떼꽂이는 따로 셈" +) +NOTE_FIGURES = "교본 그림 7-8·7-10·7-11 은 사진 · 7-9·7-12 는 개념도(치수 없음) — 표준도가 아님" + + +def _length_at(points: list[tuple[float, float]], at: float) -> float: + """그 자리의 사면길이 — 앞뒤 측점 사이 직선 보간 · 끝 밖이면 가장 가까운 측점.""" + if at <= points[0][0]: + return points[0][1] + for (s0, v0), (s1, v1) in zip(points, points[1:]): + if s0 <= at <= s1: + return v0 if s1 == s0 else v0 + (v1 - v0) * (at - s0) / (s1 - s0) + return points[-1][1] + + +def _interval_area(points: list[tuple[float, float]], start: float, end: float) -> float: + if len(points) < 1 or end <= start: + return 0.0 + cuts = [start, *[s for s, _v in points if start < s < end], end] + return sum( + (_length_at(points, a) + _length_at(points, b)) / 2 * (b - a) + for a, b in zip(cuts, cuts[1:]) + ) + + +def revegetation_from_designs( + structure: dict[str, Any], + designs: list[dict[str, Any]] | None, + slopes: list[Any] | None = None, +) -> dict[str, Any]: + """구간 안 절토·성토 사면 면적을 B06 측점에서 셈해 실은 **새** 구조물(칸 값은 안 건드림).""" + if slopes is None: + from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes + + slopes = station_slopes(designs or []) + ordered = sorted(slopes, key=lambda s: s.chainage_m) + options = dict(structure.get("options") or {}) + notes = list(structure.get("notes") or []) + start, end = sorted((_num(structure.get("start_m")), _num(structure.get("end_m")))) + if not ordered: + notes.append("B06 횡단 설계가 없어 구간 사면 면적을 못 읽음") + return {**structure, "options": options, "notes": notes} + for key, attr in ((CUT, "cut_length_m"), (FILL, "fill_length_m")): + points = [(s.chainage_m, float(getattr(s, attr))) for s in ordered] + options[key] = round(_interval_area(points, start, end), 4) + notes.append( + f"구간 {start:g}~{end:g}m 사면 면적 — 절토면 {options[CUT]:,.2f}㎡ · 성토면" + f" {options[FILL]:,.2f}㎡ (B06 측점 사면길이 · 평균단면적법)" + ) + return {**structure, "options": options, "notes": notes} + + +def _area(options: dict[str, Any]) -> float | None: + keys = FACES.get(str(options.get("face") or "")) + return None if keys is None else sum(_num(options.get(key)) for key in keys) + + +def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]: + """녹화 한 구간 — (성분, 사유).""" + method = str(options.get("method") or "") + if not method: + return [], ["녹화 공법을 안 고름 — 공법을 고르면 섬"] + if method not in AREA_METHODS: + template = NOTE_LENGTH if method in LENGTH_METHODS else NOTE_LATER + return [], [template.format(method=method), NOTE_FIGURES] + area = _area(options) + if area is None: + return [], [NOTE_FACE] + if method == "비탈덮기" and not options.get("cover_kind"): + return [], [NOTE_COVER] + name, sheets, basis = AREA_METHODS[method] + notes = [NOTE_OVERLAP, basis, NOTE_FIGURES] + ([NOTE_STAKE] if method == "평떼" else []) + rows: list[tuple[str, str, float, str, str]] = [ + (name, "㎡", area, f"{options.get('face')} 사면 면적 {area:,.2f}㎡", "") + ] + if sheets is not None: + rows.append(("떼", "매", area * sheets, f"면적 {area:,.2f} × {sheets:g}매/㎡", SOD_SPEC)) + components = [ + Component(item, unit, amount, destination, text, spec=spec) + for item, unit, amount, text, spec in rows + if (destination := routed(item, notes)) + ] + return components, notes + + +def billing(options: dict[str, Any]) -> tuple[str, float]: + """내역 줄 — 녹화 면적 ㎡.""" + return "㎡", _area(options) or 0.0 diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 9f961fe1..b3da9e31 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -103,6 +103,13 @@ def _collect_structures( from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Pavement import pavement_from_designs payload = pavement_from_designs(payload, designs) + if type_id == "revegetation": + # 구간 안 절토·성토 사면 면적은 B06 측점 사면길이에서(2026-09-15 녹화 ⓑ). + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import ( + revegetation_from_designs, + ) + + payload = revegetation_from_designs(payload, designs) if definition.group == "B": # ⚠ B군(종단배수)은 **연장표**로 간다 — `common_util_structure_lengths` 가 # 겹친 구간을 합쳐 주기 때문이다. 구조물별로 세면 겹친 구간을 두 번 센다. 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 e3742874..7a272c70 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 @@ -195,7 +195,7 @@ "work_item_code": "FP-09-03-02", "basis_unit": "㎥", "master_name": "토사깍기 > 기계", - "note": "2026-09-15 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 「노면의 토사구간」이라 토사 측점만 · 브레인 ① 토공 축. ⚠ 노면 10㎝ 걷기를 토사깍기(기계)로 이음 — 산림품셈에 노면 사전 터파기 공종이 따로 없음(판정 확인 대기)." + "note": "2026-09-15 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 「노면의 토사구간」이라 토사 측점만 · 브레인 ① 토공 축. 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음(같은 일에 맞는 공종을 고름 · 2026-09-15 브레인 ② 승인)." } ], "haul": [ @@ -534,6 +534,68 @@ ], "why": "실무 내역은 「콘크리트포장 T=20 ㎡」 한 줄이고 품셈은 포설(12-6 ㎥ · 비닐·철망깔기·양생 포함) · 거푸집(12-8 연장 m) · 줄눈(12-7) 을 따로 둠 — 거푸집·줄눈 제외라 묶음으로 셈(2026-09-15 브레인 12장 D 포장 둘). 콘크리트·비닐·철망 재료는 자재 축.", "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "새심기"}, + "parts": [ + {"code": "FP-05-14", "name": "새심기", "unit": "㎡", "from_components": ["새심기"]} + ], + "why": "비탈면 녹화 한 줄(등록부 공법 고르기)이 공법마다 다른 품셈 공종으로 감 — 새심기 5-14 ㎡당(2026-09-15 브레인 녹화 ⓓ).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "평떼"}, + "parts": [ + {"code": "FP-05-22-02", "name": "평떼 붙임", "unit": "㎡", "from_components": ["평떼"]}, + { + "code": "FP-05-22-03", + "name": "떼꽂이 제작 및 설치", + "unit": "㎡", + "from_components": ["평떼"], + "why": "5-22-3 [주] 「평떼(보통인부 0.06인)품에는 떼꽂이가 포함」은 5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈(5-22-2 붙임을 씀 · 2026-09-15 브레인 ⓐ 승인 · 뒤집을 수 있음)" + } + ], + "why": "평떼 5-22 — 떼(0.3×0.3 11매/㎡)는 자재 축 · 붙임·떼꽂이 ㎡ 묶음(2026-09-15 브레인 녹화 ⓓ).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "줄떼"}, + "parts": [ + {"code": "FP-05-23-02", "name": "떼붙임", "unit": "㎡", "from_components": ["줄떼"]}, + {"code": "FP-05-23-03", "name": "떼꽂이 제작 및 설치", "unit": "㎡", "from_components": ["줄떼"]} + ], + "why": "줄떼 5-23 — 떼(0.3×0.3 · 30㎝ 간격 3.33매/㎡)는 자재 축 · 붙임·떼꽂이 ㎡ 묶음(2026-09-15 브레인 녹화 ⓓ).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "비탈덮기", "cover_kind": "거적"}, + "parts": [ + {"code": "FP-05-25", "name": "거적덮기", "unit": "㎡", "from_components": ["비탈덮기"]} + ], + "why": "비탈덮기(거적) — 품셈 5-25 거적덮기 ㎡당(거적 1.1㎡ 표 안 · 앙카핀 등 부속은 별도).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "비탈덮기", "cover_kind": "짚망"}, + "parts": [ + {"code": "FP-05-28-01", "name": "짚망", "unit": "㎡", "from_components": ["비탈덮기"]} + ], + "why": "비탈덮기(짚망) — 품셈 5-28-1 · ⚠ 원문 표에 밑수(단위) 표기가 없음 — B09 가 밑수로 막으면 그 사유가 섬 · 재료량은 설계수량 별도([주]②).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, + { + "type_id": "revegetation", + "when": {"method": "비탈덮기", "cover_kind": "방초·야자섬유매트"}, + "parts": [ + {"code": "FP-05-28-02", "name": "방초매트 및 야자섬유매트 포장", "unit": "㎡", "from_components": ["비탈덮기"]} + ], + "why": "비탈덮기(매트) — 품셈 5-28-2 일당 시공량(폭 1.5m 이하 90㎡ · 2.0m 이하 130㎡) · 폭 갈래는 B09 몫.", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" } ], "unit_conversion": { diff --git a/resources/tester/test_b05_not_in_table_marks.py b/resources/tester/test_b05_not_in_table_marks.py index 358c0dd9..a46bb910 100644 --- a/resources/tester/test_b05_not_in_table_marks.py +++ b/resources/tester/test_b05_not_in_table_marks.py @@ -42,6 +42,11 @@ MARKED: dict[str, tuple[dict, list[str]]] = { "weep_hole_area_m2", ], ), + # 2026-09-15 녹화 ㎡ 넷 — 단 직고·선떼 급수는 길이형(선떼·조공) 산출식이 설 때 읽힘. + "revegetation": ( + {"method": "평떼", "face": "양쪽", "b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0}, + ["step_height_m", "sod_grade"], + ), "soil_guard": ( {"height_m": 1.0, "length_m": 10.0, "form": "떼"}, [ diff --git a/resources/tester/test_b08_gravel_surfacing.py b/resources/tester/test_b08_gravel_surfacing.py index ca01dda3..9acb7e73 100644 --- a/resources/tester/test_b08_gravel_surfacing.py +++ b/resources/tester/test_b08_gravel_surfacing.py @@ -47,6 +47,8 @@ def test_법령_조건만_자동_토사_8퍼센트_초과() -> None: assert result["excavation_soil_m3"] == pytest.approx(9.0) text = " ".join(result["notes"]) assert "별표2" in text and "토사 전부를 해당으로 봄" in text + # 토사/암은 B06 토사 토글이 정본 — 리핑암은 「암」의 자리표시(브레인 2026-09-15 ① 정정). + assert "토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)" in text assert "원문 표에 혼합석 줄 없음" in text and "방향이 반대" in text assert "다짐" in text @@ -66,6 +68,10 @@ def test_비포장_전_구간_고르개는_포장만_뺀다_암은_사전터파 assert result["excavation_soil_m3"] == pytest.approx(170.0 * 0.10) assert any("교본 3-2" in note for note in result["notes"]) assert any("토사구간" in note for note in result["notes"]) + assert any( + "산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음" in note + for note in result["notes"] + ) def test_칸이_두께_C_L_을_덮어쓴다() -> None: diff --git a/resources/tester/test_b08_revegetation_unit.py b/resources/tester/test_b08_revegetation_unit.py new file mode 100644 index 00000000..039dc7dd --- /dev/null +++ b/resources/tester/test_b08_revegetation_unit.py @@ -0,0 +1,115 @@ +"""비탈면 녹화 ㎡ 넷 — 새심기·평떼·줄떼·비탈덮기 (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ). + +근거 + 산림품셈 5-14 새심기(㎡당) · 5-22 평떼(떼 0.3×0.3 11매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) · + 5-23 줄떼(떼 3.33매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) · 5-25 거적덮기(㎡당) · 5-28 비탈덮기(짚망 · 매트) +판정 — 면적 = 구간 안 사면 면적(칸: 절토면/성토면/양쪽 · 기본값 없음) · 초류종자 살포는 공법에서 뺌 + (토공집계가 5-24 로 사면 전체를 셈) · 단 직고·선떼 급수 칸은 기본값 없음 · 길이형은 다음 차례. +""" + +from __future__ import annotations + +import pytest + +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table +from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import ( + revegetation_from_designs, +) + +AREAS = {"b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0} + + +def _row(**options) -> dict: + structure = { + "structure_id": "g1", + "type_id": "revegetation", + "start_m": 0.0, + "end_m": 20.0, + "options": {**AREAS, **options}, + } + table = build_table([structure], {"revegetation": "비탈면 녹화"}) + return table + + +def _parts(table: dict) -> dict: + row = next( + r for r in build_handoff(unit_quantity_table=table)["work_items"] if "녹화" in r["name"] + ) + return {part["code"]: part for part in row["composite_parts"] or []} + + +def test_평떼_양쪽은_면적과_떼_11매() -> None: + table = _row(method="평떼", face="양쪽") + row = table["structures"][0] + got = {(c["name"], c["unit"]): c["amount"] for c in row["components"]} + assert (row["billing_unit"], row["billing_quantity"]) == ("㎡", pytest.approx(100.0)) + assert got[("평떼", "㎡")] == pytest.approx(100.0) + assert got[("떼", "매")] == pytest.approx(1100.0) + text = " ".join(row["notes"]) + assert "겹친 몫을 자동으로 빼지 않음" in text and row["notes"][0].startswith("⚠") + assert "5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈" in text + parts = _parts(table) + assert parts["FP-05-22-02"]["quantity"] == pytest.approx(100.0) + assert parts["FP-05-22-03"]["quantity"] == pytest.approx(100.0) + + +def test_줄떼_절토면은_3_33매() -> None: + table = _row(method="줄떼", face="절토면") + got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]} + assert got[("줄떼", "㎡")] == pytest.approx(40.0) + assert got[("떼", "매")] == pytest.approx(40.0 * 3.33) + assert set(_parts(table)) == {"FP-05-23-02", "FP-05-23-03"} + + +def test_새심기_성토면() -> None: + table = _row(method="새심기", face="성토면") + assert _parts(table)["FP-05-14"]["quantity"] == pytest.approx(60.0) + + +def test_비탈덮기는_덮개를_골라야_선다() -> None: + table = _row(method="비탈덮기", face="양쪽", cover_kind="거적") + assert _parts(table)["FP-05-25"]["quantity"] == pytest.approx(100.0) + unset = _row(method="비탈덮기", face="양쪽")["structures"][0] + assert not unset["components"] and any("덮개" in n for n in unset["notes"]) + + +def test_면을_안_고르면_사유() -> None: + row = _row(method="평떼")["structures"][0] + assert not row["components"] and any("절토면" in n and "성토면" in n for n in row["notes"]) + + +def test_길이형은_떼_매수표를_기다린다() -> None: + row = _row(method="선떼붙이기", face="양쪽")["structures"][0] + assert not row["components"] + assert any("사방기술교본" in n for n in row["notes"]) + + +def test_구간_안_사면_면적은_B06_측점에서_보간() -> None: + designs = [ + {"chainage_m": 0.0, "design": {}}, + {"chainage_m": 20.0, "design": {}}, + ] + from B08_Quantity.B08_Quantity_Engine_SlopeLength import StationSlope + + slopes = [ + StationSlope(chainage_m=0.0, cut_length_m=2.0, fill_length_m=4.0), + StationSlope(chainage_m=20.0, cut_length_m=4.0, fill_length_m=0.0), + ] + structure = {"type_id": "revegetation", "start_m": 5.0, "end_m": 20.0, "options": {}} + filled = revegetation_from_designs(structure, designs, slopes) + # 절토 5→2.5, 20→4 : (2.5+4)/2×15 · 성토 5→3, 20→0 : (3+0)/2×15 + assert filled["options"]["b06_cut_area_m2"] == pytest.approx(48.75) + assert filled["options"]["b06_fill_area_m2"] == pytest.approx(22.5) + + +def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인다() -> None: + options = {o.key: o for o in structure_type_map()["revegetation"].options} + assert "초류종자 살포" not in options["method"].choices + assert "토공집계" in (options["method"].default_basis or "") + assert ( + options["face"].choices == ["절토면", "성토면", "양쪽"] and options["face"].default is None + ) + for key in ("step_height_m", "sod_grade"): + assert options[key].default is None and "아직 표에 안 쓰임" in options[key].not_in_table From 4f7e87121b5abde3a34e865e7908eed550652708 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 05:30:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore(plan):=20=EB=9E=A9=ED=83=91=20?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=20=EA=B3=84=ED=9A=8D=EC=84=9C=20=E2=80=94=20?= =?UTF-8?q?=EB=85=B9=ED=99=94=20=E3=8E=A1=20=EB=84=B7=20=C2=B7=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=BC=20=ED=99=95=EC=9D=B8=20=C2=B7=20B09=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EB=B6=80=ED=92=82=20=EC=95=8C=EB=A6=BC=20=C2=B7=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=8C=90=EB=8B=A8=20=EA=B1=B0?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- PLAN_랩탑서브.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/PLAN_랩탑서브.md b/PLAN_랩탑서브.md index 6972ef2a..8e38c27e 100644 --- a/PLAN_랩탑서브.md +++ b/PLAN_랩탑서브.md @@ -47,9 +47,19 @@ ④ 제안값 소광 관측 C 0.85 · L 1.25 · 원문 1-2-3 역(礫) L 1.10~1.20 · C 1.05~1.10 병기 · ⚠ 소광 C(줄어듦)와 역 C(늘어남)는 방향이 반대 — 사유에 적음 ⑤ 폭 = 차도 표준 폭 + 확폭(노견 제외 · 소광 「B=3.0」) - [X] 토공집계 두 줄(혼합석부설 11-4 ㎥ = 다짐 후 ÷ C × L · 사전터파기 토사 9-3-2) · 자재총괄 혼합석 ㎥(사급) · 산출 조건 칸(범위 · 연약·습윤 구간 · 두께·C·L 비면 제안값) · 종단 경사는 계획선에서 B05 포장 제안과 같은 한 벌(`local_grade_pct` 로 뺌 — 종단 파일 측점 경사에 비정규 측점 8곳이 없었음) · 산출 조건 받는 모양은 `_Router_Earthwork_Settings` 로 가름 196bd73a. 검증완료(시험 · 936be972 화면 조작 후 되돌림) - - 936be972 — 법령 조건 0㎡(62측점 모두 리핑암 자리표시) · 전 구간 3,533.18㎡ · 519.58㎥ · 법령 + 연약·습윤 100~200 = 112.42㎡ · 되돌림(키는 false·[]·null 로 남음) - - ⚠ 물음 — B06 토사 토글 끔이면 측점 지반이 「리핑암」 자리표시라 법령 자동이 영영 0 · 사전터파기 공종 9-3-2 로 이은 것 확인 대기 + - 936be972 — 법령 조건 0㎡(토사 토글 켠 측점 0 — 데이터대로) · 전 구간 3,533.18㎡ · 519.58㎥ · 법령 + 연약·습윤 100~200 = 112.42㎡ · 되돌림(키는 false·[]·null 로 남음) + - 브레인 답 — ① 토사/암 = B06 토사 토글이 정본(리핑암은 「암」 자리표시 · 구성비는 암 몫만) · 사유 「토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)」 ② 사전터파기 9-3-2 승인 · 사유 「산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음」 - [ ] 녹화 — 교본 7-1 · 그림 7-8~7-12 「표준도」가 모식도인지 먼저 + - 확인 — 7-8·7-10·7-11 사진 · 7-9·7-12 개념도(치수 없음) ⇒ 표준도 아님 · 치수는 교본 본문 · 품셈 5장 공종·밑수(5-14 새심기 ㎡ · 5-16 단끊기 100m·선떼 10m · 5-17 조공 · 5-18 씨뿌리기 m · 5-22 평떼 ㎡ · 5-23 줄떼 ㎡ · 5-25 거적 · 5-28 비탈덮기) · 선떼 떼 매수는 사방기술교본 표 2-2-6(급별 1m당 매수 · 2.5매/m·6매/m 두 규격) + - 12장 D 표(PLAN.md) 고칠 것 — 녹화 「표준도 그림 7-8~7-12」 → 「그림 7-8·7-10·7-11 사진 · 7-9·7-12 개념도(치수 없음) — 표준도 아님」(PLAN.md 읽기만이라 여기 적음) + - 판정(브레인) — ⓐ 초류종자 살포는 공법에서 빼고 칸 밑에 까닭 한 줄 ⓑ 대상 면 칸(절토면/성토면/양쪽) ⓒ 선떼 떼 매수 = 사방기술교본 표 2-2-6(급별) · 단 직고·급수 칸 기본값 없음 ⓓ ㎡ 넷(새심기·평떼·줄떼·비탈덮기)부터 · 길이형은 선떼가 조공·씨뿌리기보다 앞 + - [X] ㎡ 넷 — 등록부 칸(공법 · 대상 면 · 덮개 · 단 직고·급수 「아직 표에 안 쓰임」) · 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 끝 보간) · 떼 11매/㎡(5-22-1)·3.33매/㎡(5-23-1) 자재 · 묶음 5-14 · 5-22-02+03 · 5-23-02+03 · 5-25 · 5-28-01 · 5-28-02. 검증완료(시험 · 936be972 전 구간 면적 = 사면표 합계) + - 남은 막힘(B09) — 5-28-01 밑수 없음 · 5-28-02 호표 없음 + - ⚠ B09 묶음 줄 단가가 줄 수량만큼 부풂(조각 수량 = 합계인데 1단위로 봄) — 브레인이 데스크탑 서브에 넘김(B09 가 줄 수량으로 나눔 · 시험을 수량 ≠ 1 로) + - 브레인 답 — ⓐ 떼꽂이 읽기 승인 · 사유 「[주]는 5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈」 ⓑ 겹친 사면은 사유만(맨 앞 ⚠) · 자동으로 안 뺌 + - [ ] 사용자 판단 거리 — 「녹화 구간은 초류종자살포(파종)에서 뺄까요」 칸을 둘지 + - [ ] 선떼붙이기(길이형) — 표 2-2-6 급별 m당 매수 · 단끊기 연장 = 직고 ÷ 단 직고 × 연장(5-16-1 [주]②) + - [ ] 조공·씨뿌리기·편책·녹생토·나무심기 - [ ] 소단 — 임도 조문(사면길이 2~3m 마다 폭 50~100㎝) · 사방 기준 아님 - [ ] 개소형 — 대피소·작업장·차돌림곳·노출형 횡단수로 - [ ] 부대 — 가드레일·반사경·표지(그림 11-6~11-8 표준도 확인 · 번호판 500m 제안)