diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 9a246491..6685edf0 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -240,6 +240,55 @@ "default": 2.0, "required": false }, + { + "key": "length_m", + "label": "연장(계류 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 수량이 안 섬 — 구체 길이는 설계자 입력(기본값 없음 · 2026-09-15 브레인 판정 ③)" + }, + { + "key": "wall_thickness_m", + "label": "측벽 두께", + "input": "number", + "unit": "m", + "default": 0.3, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 벽 0.3 · KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "slab_thickness_m", + "label": "상·저판 두께", + "input": "number", + "unit": "m", + "default": 0.25, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 판 0.25 — ⚠ KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ 에 못 미침(임도 예제 0.25 · KDS 도로 최소 300㎜) · 설계자가 고름", + "required": false, + "phase": "detail" + }, + { + "key": "haunch_m", + "label": "헌치(모따기) 크기", + "input": "number", + "unit": "m", + "default": 0.2, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 0.2×0.2 · 실무 울진 기번3 구조도 2×2 도 0.2 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "blinding_thickness_m", + "label": "기초(버림) 두께", + "input": "number", + "unit": "m", + "default": 0.1, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 기초 0.1(양쪽 0.1 여유) · 실무 울진 기번3 구조도 2×2 버림 0.1 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, { "key": "wing_in", "label": "날개벽(유입)", diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 5619c0fb..ce97262f 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -110,8 +110,9 @@ FORD_DEFAULT_WIDTH_M = 10.0 # 지식DB 폭 수치 근거 없음). FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0 -# BOX암거 부재 두께(m) — 지식DB에 암거 부재 두께 기준이 없어 세월교 값을 승계한다 -# (2026-08-25 사용자 확정). 측벽·상판·저판은 각각 벽 두께·바닥판 두께를 그대로 쓴다. +# BOX암거 부재 두께 — **등록부 칸**(`wall_thickness_m`·`slab_thickness_m`)이 정본이다 +# (2026-09-15 브레인 판정 ① · 제안값 = 산림과임업기술(임도) 예제 벽 0.3·판 0.25 · KDS 300㎜ 병기). +# 종전 「세월교 값 승계」(2026-08-25 사용자 확정)는 근거 없는 값이라 걷음 — 수량·그림이 한 칸을 읽음. # ⚠ 교차 참조 ④ 암거 위 복토(m) — 별표2 교량·암거 "복토 시 흙 두께 50㎝ 이상". BOX_COVER_M = 0.5 @@ -451,8 +452,10 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]: values = dict(options or {}) inner_width = _number(values.get("body_width_m"), _number(defaults.get("body_width_m"), 2.0)) inner_height = _number(values.get("body_height_m"), _number(defaults.get("body_height_m"), 2.0)) - wall = FORD_WALL_THICKNESS_M - slab = FORD_SLAB_THICKNESS_M + wall = _number(values.get("wall_thickness_m"), _number(defaults.get("wall_thickness_m"), None)) + slab = _number(values.get("slab_thickness_m"), _number(defaults.get("slab_thickness_m"), None)) + wall = wall if wall and wall > 0 else FORD_WALL_THICKNESS_M # 등록부가 비었을 때만(옛 판) + slab = slab if slab and slab > 0 else FORD_SLAB_THICKNESS_M wing_in = _wing_spec(values, defaults, "in") wing_out = _wing_spec(values, defaults, "out") return { diff --git a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts index e399baa2..e8c45dd4 100644 --- a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts @@ -379,7 +379,7 @@ export function createBoxControls(deps: FordControlDeps): { // 캐시 먼저 — 구체 길이(`span_m`)는 백엔드 `_box_set`과 같은 식으로 다시 잡는다. if (patch.body_width_m) { spec.inner_width_m = patch.body_width_m; - spec.span_m = boxSpanM(patch.body_width_m); + spec.span_m = boxSpanM(patch.body_width_m, spec.wall_thickness_m); } if (patch.body_height_m) spec.inner_height_m = patch.body_height_m; } diff --git a/B08_Quantity/B08_Quantity_Engine_Pipe.py b/B08_Quantity/B08_Quantity_Engine_Pipe.py index ded2e7b5..023b4128 100644 --- a/B08_Quantity/B08_Quantity_Engine_Pipe.py +++ b/B08_Quantity/B08_Quantity_Engine_Pipe.py @@ -369,6 +369,36 @@ def _extra_walls_at(extra_walls: dict[float, list[dict]] | None, chainage: float return extra_walls[best] if abs(best - chainage) <= SECTION_MATCH_TOLERANCE_M else [] +#: BOX암거 치수 칸 — 안 적으면 등록부 기본값(제안값)으로 서되 **미확정**(2026-09-14 브레인 판정 · +#: 2026-09-15 12장 D). 연장은 기본값이 없어 여기 없음 — 비면 전개식이 사유를 냄. +BOX_DIMENSIONS = ( + ("body_width_m", "본체 폭"), + ("body_height_m", "본체 높이"), + ("wall_thickness_m", "측벽 두께"), + ("slab_thickness_m", "상·저판 두께"), + ("haunch_m", "헌치"), + ("blinding_thickness_m", "기초(버림) 두께"), +) +UNCONFIRMED_BOX = "BOX암거 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬" + + +def _box_row(base: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]: + """BOX암거 줄 — 빈 치수 칸에 등록부 제안값을 채우고 그 사실을 미확정 사유로 붙임.""" + options = dict(base["options"]) + filled = [] + for key, label in BOX_DIMENSIONS: + if _blank(options.get(key)) and registry.get(key) is not None: + options[key] = registry[key] + filled.append(label) + reason = UNCONFIRMED_BOX.format(filled=" · ".join(filled)) if filled else "" + return { + **base, + "options": options, + "unconfirmed": reason, + "notes": [f"⚠ {reason}"] if reason else [], + } + + def facility_structures( points: list[dict[str, Any]], extra_walls: dict[float, list[dict]] | None = None, @@ -405,8 +435,11 @@ def facility_structures( "end_m": point.get("end_m") if point.get("end_m") is not None else chainage, "options": options, } + if facility == "box_culvert": + rows.append(_box_row(base, defaults(facility))) + continue if facility not in (FACILITY_PIPE, "revetment"): - rows.append(base) # BOX암거·물넘이·세월교 — 전개식·관측값이 없으면 사유로 섬 + rows.append(base) # 물넘이·세월교 — 전개식·관측값이 없으면 사유로 섬 continue legacy = facility == "revetment" # 독립 기슭막이 — 벽 칸 밖의 제원(뒷길이 등)도 벽이 씀 wall_defaults = defaults(facility) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index fda877bf..5dfce46a 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -116,6 +116,8 @@ EXPANDERS = { "soil_guard": lambda h, l, o, f=None, r="": _soil_guard()(o), # 바닥막이는 **돌붙임 ㎡당**이라 높이·연장이 아니라 **면적**이 밑수다(정본 「돌붙임L3=…」). "bed_sill": lambda h, l, o, f=None, r="": _bed_sill()(_num(o.get("area_m2"), 0.0), o), + # BOX암거 — m당 × 연장(산림과임업기술 예제 · 울진 2×2 · 2026-09-15 브레인 12장 D). + "box_culvert": lambda h, l, o, f=None, r="": _box_culvert()(o), # ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견). # 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.** # 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝). @@ -126,6 +128,13 @@ EXPANDERS = { } +def _box_culvert(): + """BOX암거 전개 함수를 늦게 가져온다.""" + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Box import box_culvert + + return box_culvert + + def _soil_guard(): """흙막이 사유 함수를 늦게 가져온다.""" from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import soil_guard @@ -162,7 +171,13 @@ def _revetment(): #: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다. -EXPANDER_WITHHELD: dict[str, str] = {} +EXPANDER_WITHHELD: dict[str, str] = { + # 2026-09-15 브레인 판정 ⑥ — 찾아봤으나 없음(「아직 안 만듦」과 다름). + "ford_bridge": ( + "세월교 수량 근거 없음 — 법령·임도기술교본은 설치 조건만(그림 3-18 은 배치 모식도)" + " · 실무 원본에 세월교 시트·도면 없음 · 관은 배수관 줄로 따로 섬" + ), +} #: 전개식이 **개소당·㎡당**으로 값을 내는 종류의 내역 단위와 수량(2026-09-14 구조물도 그림에서 잡음). #: ⚠ 안 적으면 「m · 연장」으로 읽혀 — 골막이·바닥막이(점 배치라 연장 0)는 **수량 0 이라 내역에 안 서고** @@ -171,6 +186,7 @@ EXPANDER_BILLING: dict[str, Any] = { "erosion_check": lambda options: ("개소", 1.0), # 정본 「골막이(찰)(치수조서연결)」 개소당 "soil_guard": lambda options: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이) "bed_sill": lambda options: ("㎡", _num(options.get("area_m2"), 0.0)), # 정본 돌붙임 ㎡당 + "box_culvert": lambda options: ("m", _num(options.get("length_m"), 0.0)), # 점 배치라 연장 칸 } @@ -328,6 +344,15 @@ def expand( return result +def _shoring_of(quantities: Iterable[StructureQuantity]) -> dict[str, Any]: + """동바리 상태 — 전개식이 동바리 줄을 낸 구조물이 있으면 대상(2026-09-15 BOX암거).""" + status = shoring_status() + found = sorted({q.name for q in quantities if any(c.name == "동바리" for c in q.components)}) + if found: + status.update(applicable=True, reason=f"슬래브 받침 동바리(12-20) — {' · '.join(found)}") + return status + + def verify_no_mix_components(quantities: Iterable[StructureQuantity]) -> list[str]: """⚠ 배합 성분이 산출물에 섞이면 알린다 (㉢ 이중계상 방어). @@ -612,8 +637,8 @@ def build_table( "structures": payload_structures, "formwork_notes": formwork_notes, "formwork_reuse_missing": formwork_missing, - # 동바리 — 대상이 없으면 0 이 아니라 「없음」이라고 말한다. - "shoring": shoring_status(), + # 동바리 — 대상이 없으면 0 이 아니라 「없음」이라고 말한다(BOX암거 동바리 줄이 서면 대상). + "shoring": _shoring_of(quantities), # ⚠ 값을 바꾸는 설계 조건인데 우리 제원에 칸이 없는 것 — 화면에 드러낸다. # 「무엇을 정해야 하는지」만으로는 부족하고 **「정하면 얼마나 달라지는지」**까지. "pending_choices": (observed.pending_choices or {}).get("items") or [], diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index 0fbf4bac..a6692956 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -58,6 +58,10 @@ DESTINATION = { "유로폼": "unit_price", # 거푸집 계열(품셈 12-38) — 설치·해체 품 "면목": "material", "떼": "material", # 사면 떼(자재총괄 extra)와 같은 자리 · 할증 10%(1-3-1) + # ⭐ 2026-09-15 BOX암거 — 버림 옆면 합판 · 동바리(12-20)·비계(12-19)는 설치 품이라 묶음 조각으로. + "합판거푸집": "unit_price", + "동바리": "unit_price", + "비계": "unit_price", } #: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄. diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py new file mode 100644 index 00000000..0657300b --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py @@ -0,0 +1,125 @@ +"""BOX암거 전개식 — **m당 × 연장** (2026-09-15 브레인 12장 D 판정 ①~⑤). + +근거 둘 — 구조가 같다 + ① 산림과임업기술(임도) 5장 「나) 구조물도에 의한 수량산출(예)」 · 그림 5-2-70 + Box(2.0×1.5×2련×6.03m) — 구체 = (외곽 − 유수구 + 헌치) × L · 기초 = 폭 × 0.1 × L · + 거푸집 = 외벽·유수구·받침판·귀면 · 동바리 = 유수구 단면 × L · 철근 「상세도에서」(값 없음) + ② 실무 울진 기번3 `5. 구조도(기번3).xlsx` 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 · + 버림 0.28 · 유로폼 11.132 · 합판6회 0.2 · 철근 SD40 H13 0.179 + H16 0.157t · 동바리 3.92 · + 비계 18.72 + 두께 제안값은 등록부 칸(예제 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1 · KDS 300㎜ 병기). +⚠ 1련만 — 등록부에 련수 칸이 없음(예제 식은 련수로 늘어나나 그림·칸이 1련). +⚠ 철근은 **크기별 식이 없다** — 울진 2×2 관측값만 선다(판정 ②). +⚠ 비계 시종점 두 면은 **개소당 한 번** — 원문은 m당 칸에 넣어 연장을 곱하면 부풀어 오름. +""" + +from __future__ import annotations + +import math +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed + +#: 버림 폭 여유(m, 한쪽) — 예제 5.1 = 외곽 4.9 + 0.1×2 · 울진 2.8 = 외곽 2.6 + 0.1×2 (둘이 같음). +BLINDING_EXTRA_M = 0.1 +#: 철근 관측값(kg/m) — 울진 기번3 「암거수량집계표(2×2)」 SD40 · 내공 2.0×2.0 · 벽·판 0.3. +REBAR_OBSERVED_2X2 = (("D13", 179.0), ("D16", 157.0)) +REBAR_SIZE_ONLY = ( + "철근 — 크기별 식 없음 · 관측값은 2×2 뿐(울진 기번3 구조도 SD40 H13 0.179 + H16 0.157t/m ·" + " 벽·판 0.3) — 이 크기는 철근이 안 섬(2026-09-15 브레인 판정 ②)" +) +SOURCES = ( + "산식 = 산림과임업기술(임도) 5장 수량산출 예제(그림 5-2-70) · 실무 울진 기번3 구조도" + " 「암거수량집계표(2×2)」" +) +NOTE_FORMWORK = ( + "거푸집 — 본체 유로폼(외벽 · 내벽 헌치 뺀 높이 · 상판 밑면 · 귀면 넷) · 버림 옆면 합판" + " (울진 2×2 모양 · 예제는 합판 3회)" +) +NOTE_SHORING_SCAFFOLD = ( + "동바리(유수구 단면 − 헌치)·비계(외벽 양면 + 시종점 두 면)는 **실무 관측 모양**" + "(울진 2×2 「강재 3개월」) — 품셈 12-20 강관동바리 · 12-19 강관비계로 이음 ·" + " 시종점 두 면은 개소당 한 번(원문은 m당 칸)" +) +NOTE_WING_EARTH = ( + "날개벽 수량 근거 없음(교본·실무 모두 · 판정 ⑤) — 날개벽 줄 안 섬" + " · 구체 터파기·되메우기는 아직 안 셈" +) + + +def box_culvert(options: dict[str, Any]) -> tuple[list[Component], list[str]]: + """BOX암거 한 개소 — (성분, 사유). 연장이 없으면 성분 없이 사유만.""" + length = _num(options.get("length_m")) + if length <= 0: + return [], ["BOX암거 연장(계류 방향)이 없어 수량이 안 섬 — 연장 칸을 적을 것(기본값 없음)"] + width, height = _num(options.get("body_width_m")), _num(options.get("body_height_m")) + wall, slab = _num(options.get("wall_thickness_m")), _num(options.get("slab_thickness_m")) + haunch, blinding = _num(options.get("haunch_m")), _num(options.get("blinding_thickness_m")) + if min(width, height, wall, slab) <= 0: + return [], ["BOX암거 본체 폭·높이·두께 중 빈 칸이 있어 수량이 안 섬"] + + outer_w, outer_h = width + 2 * wall, height + 2 * slab + chamfer = math.sqrt(2) * haunch + per_m: list[tuple[str, str, float, str, str]] = [ + ( + "콘크리트", + "㎥", + outer_w * outer_h - width * height + haunch**2 / 2 * 4, + f"{outer_w:g}×{outer_h:g} − {width:g}×{height:g} + {haunch:g}²÷2×4", + "", + ), + ( + "버림콘크리트", + "㎥", + (outer_w + 2 * BLINDING_EXTRA_M) * blinding, + f"({outer_w:g} + {BLINDING_EXTRA_M:g}×2) × {blinding:g}", + "", + ), + ( + "유로폼", + "㎡", + 2 * outer_h + 2 * (height - 2 * haunch) + (width - 2 * haunch) + 4 * chamfer, + f"외벽 {outer_h:g}×2 + 내벽 ({height:g} − {haunch:g}×2)×2" + f" + 상판 밑 ({width:g} − {haunch:g}×2) + 귀면 √({haunch:g}²+{haunch:g}²)×4", + "", + ), + ("합판거푸집", "㎡", 2 * blinding, f"버림 옆면 {blinding:g}×2", ""), + ( + "동바리", + "㎥", + width * height - 2 * haunch**2, + f"{width:g}×{height:g} − {haunch:g}²÷2×4", + "", + ), + ("비계", "㎡", 2 * outer_h, f"외벽 양면 {outer_h:g}×2", ""), + ] + notes: list[str] = [SOURCES, NOTE_FORMWORK, NOTE_SHORING_SCAFFOLD, NOTE_WING_EARTH] + if math.isclose(width, 2.0) and math.isclose(height, 2.0): + per_m += [ + ("이형철근", "kg", kg, "울진 기번3 구조도 2×2 관측(SD40)", size) + for size, kg in REBAR_OBSERVED_2X2 + ] + notes.append("철근은 관측값(울진 2×2 · 벽·판 0.3) — 두께를 바꿔도 따라 안 바뀜") + else: + notes.append(REBAR_SIZE_ONLY) + + components = [ + Component( + name, + unit, + amount * length, + destination, + f"{basis} = m당 {amount:.4g} × 연장 {length:g}m", + spec=spec, + ) + for name, unit, amount, basis, spec in per_m + if (destination := routed(name, notes)) + ] + ends = 2 * outer_w * outer_h + for component in components: + if component.name == "비계": + component.amount += ends + component.basis += ( + f" + 시종점 두 면 {outer_w:g}×{outer_h:g}×2 = {ends:.4g}(개소당 한 번)" + ) + return components, notes diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 92be3434..f9055b0a 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -211,9 +211,9 @@ export function pipeDiameterM(diameterMm: number | null | undefined): number { return round((num(diameterMm, 1000.0) ?? 1000.0) / 1000.0, 3); } -/** BOX암거 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장. */ -export function boxSpanM(innerWidthM: number): number { - return innerWidthM + 2 * FORD_WALL_THICKNESS_M; +/** BOX암거 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장(두께는 등록부 칸 · 2026-09-15). */ +export function boxSpanM(innerWidthM: number, wallThicknessM: number): number { + return innerWidthM + 2 * wallThicknessM; } /** 날개벽이 만드는 바닥판 연장량 = 길이 × cos(벌어짐각). 안 세우면 0. */ @@ -351,8 +351,11 @@ function boxSet( const defaults = registry.box_culvert ?? {}; const innerWidth = num(values.body_width_m, num(defaults.body_width_m, 2.0)); const innerHeight = num(values.body_height_m, num(defaults.body_height_m, 2.0)); - const wall = FORD_WALL_THICKNESS_M; - const slab = FORD_SLAB_THICKNESS_M; + // 두께는 등록부 칸이 정본(2026-09-15 브레인 12장 D ①) — 파이썬 `_box_set` 과 같은 폴백. + const wallValue = num(values.wall_thickness_m, num(defaults.wall_thickness_m, null)); + const slabValue = num(values.slab_thickness_m, num(defaults.slab_thickness_m, null)); + const wall = wallValue && wallValue > 0 ? wallValue : FORD_WALL_THICKNESS_M; + const slab = slabValue && slabValue > 0 ? slabValue : FORD_SLAB_THICKNESS_M; const wingIn = wingSpec(values, defaults, "in"); const wingOut = wingSpec(values, defaults, "out"); return { @@ -363,7 +366,7 @@ function boxSet( slab_thickness_m: slab, top_thickness_m: slab, cover_m: BOX_COVER_M, - span_m: boxSpanM(innerWidth || 2.0), + span_m: boxSpanM(innerWidth || 2.0, wall), wing_in: wingIn, wing_out: wingOut, defaulted: [ diff --git a/resources/data_formwork/formwork_reuse_2026-01-01.json b/resources/data_formwork/formwork_reuse_2026-01-01.json index ebc76622..0dc86468 100644 --- a/resources/data_formwork/formwork_reuse_2026-01-01.json +++ b/resources/data_formwork/formwork_reuse_2026-01-01.json @@ -76,6 +76,12 @@ "type_id": "ford_pavement", "reuse_count": null, "note": "물넘이포장은 거푸집이 서지 않는 구조(면 포장) — 대상 아님" + }, + { + "type_id": "box_culvert", + "reuse_count": 6, + "matched_example": "관의 기초", + "note": "BOX암거 합판은 **버림 옆면뿐**(본체는 유로폼 — 12-38 유형 「박스」 보통) — 원문 6회 줄 「관의 기초 등 극히 간단한 구조」 · 실무 울진 2×2 「합판6회」 와 같음(2026-09-15)" } ], "reuse_ratio_pct": { @@ -99,12 +105,11 @@ } }, "shoring": { - "note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 지금 서는 구조물(옹벽·집수정)은 벽체 거푸집만이라 대상이 아니다.", + "note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 옹벽·집수정은 벽체 거푸집만이라 대상이 아니고, BOX암거는 전개식이 동바리 줄을 낸다(2026-09-15).", "targets_pending": [ - "box_culvert", "ford_bridge" ], - "why": "그 둘은 원단위·치수가 미확보라 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보" + "why": "세월교는 수량 근거가 없어 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보" }, "euroform_type": { "note": "유로폼 설치·해체 유형(간단/보통/복잡) — **품셈 12-38-3 [주]④ 원문**이 시설 예시로 갈라 둔다. 거푸집 사용횟수(1-7-1)·철근 갈래(12-3 [주]①)와 같은 자리로, 사람이 고르는 값이 아니다.", diff --git a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json index 85f1bfda..999e47d1 100644 --- a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json +++ b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json @@ -560,15 +560,10 @@ "not_found": { "note": "규격은 우리 모델에 있으나 **관측 원단위가 어디에도 없는** 것. 지어내지 않는다.", "items": [ - { - "type_id": "box_culvert", - "why": "울진 2공구에 BOX암거가 실재하나 원단위 라이브러리에 탭이 없음. 게다가 structures.json 의 BOX 제원은 body_width_m·body_height_m 와 날개벽뿐이라 **벽·저판·상판 두께가 없어 전개식도 못 세움**.", - "needs": "표준 단면(벽·저판·상판 두께) 확보 — 사용자 확정 대기" - }, { "type_id": "ford_bridge", - "why": "세월교 본체(날개벽 포함) 원단위 없음. 관 부분은 pipe 로 따로 섬.", - "needs": "표준도 물량 또는 실무 관측" + "why": "세월교 수량 근거 없음 — 법령·임도기술교본은 설치 조건만(그림 3-18 은 배치 모식도) · 실무 원본(울진 4공구·오솔길 전수)에 세월교 시트·도면 없음. 관 부분은 pipe 로 따로 섬(2026-09-15 브레인 판정 ⑥ — 사유로 닫음).", + "needs": "세월교 표준도 물량 또는 실무 관측 — 나오면 다시 엶" }, { "type_id": "retaining_wall", 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 627aa37a..695b3b45 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 @@ -435,6 +435,62 @@ "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김", "placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.", "parts_note": "조각마다 **어느 성분에서 나오는지**(`from_components`)를 적는다. 이름을 바꾸면 물량이 조용히 0 이 되므로 원단위 성분 이름과 정확히 같아야 하고, 못 찾으면 드러낸다. `unit_from` 이 있으면 그 단위에서 목표 단위로 환산한다 — **철근은 ㎏ → ton (÷1000)**. ⚠ 단위를 안 맞추면 1000배 틀린다." + }, + { + "type_id": "box_culvert", + "parts": [ + { + "code": "FP-12-01-01", + "name": "콘크리트 타설", + "unit": "㎥", + "from_components": ["콘크리트", "버림콘크리트"], + "kind_suffix": "concrete_placing" + }, + { + "code": "FP-12-38", + "name": "유로폼", + "unit": "㎡", + "from_components": ["유로폼"], + "kind_suffix": "euroform_type" + }, + { + "code": "FP-12-04", + "name": "합판거푸집", + "unit": "㎡", + "from_components": ["합판거푸집"], + "kind_suffix": "formwork_reuse" + }, + { + "code": "FP-12-03", + "name": "철근 현장가공 및 조립", + "unit": "ton", + "from_components": ["이형철근 D13", "이형철근 D16"], + "unit_from": "kg", + "kind_suffix": "rebar_complexity", + "why": "철근은 크기별 식이 없어 울진 기번3 구조도 2×2 관측값만 섬 — 다른 크기는 이 조각이 비어 묶음이 막힘(2026-09-15 브레인 판정 ②)" + }, + { + "code": "FP-12-20", + "name": "강관동바리", + "unit": "㎥", + "from_components": ["동바리"] + }, + { + "code": "FP-12-19", + "name": "강관비계", + "unit": "㎡", + "from_components": ["비계"] + }, + { + "code": "FP-12-25", + "name": "기초잡석", + "unit": "㎥", + "from_components": ["기초잡석"], + "why": "BOX 근거 둘(예제·울진)에는 잡석 줄이 없으나 **버림이 선 구조물엔 기초잡석이 함께 섬**(사용자 확정 3차 ② · 옹벽과 같은 규칙) — 원단위에 선 줄을 묶음에서 빼면 수량만 보이고 금액이 빠짐" + } + ], + "why": "품셈 12장에 「BOX암거」 공종이 없음 — 산림과임업기술(임도) 5장 수량산출 예제가 구체콘크리트·기초·거푸집·동바리·철근으로 가르고, 실무 울진 기번3 「암거수량집계표(2×2)」가 레미콘·버림·유로폼·합판·철근·동바리·비계로 셈(2026-09-15 브레인 12장 D).", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" } ], "unit_conversion": { diff --git a/resources/tester/test_b06_box_set.py b/resources/tester/test_b06_box_set.py index 7a07deca..c88753e6 100644 --- a/resources/tester/test_b06_box_set.py +++ b/resources/tester/test_b06_box_set.py @@ -19,31 +19,29 @@ if str(ROOT) not in sys.path: from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 BOX_COVER_M, - FORD_SLAB_THICKNESS_M, - FORD_WALL_THICKNESS_M, _box_set, attach_culvert_sets, ) def test_box_set_defaults() -> None: - """저장 옵션이 없으면 레지스트리 기본 2.0×2.0에 세월교 두께를 얹는다.""" + """옵션이 없으면 등록부 기본 2.0×2.0 · 두께도 등록부 제안값(2026-09-15 · 세월교 승계 걷음).""" spec = _box_set(None) assert spec["type"] == "box" assert spec["inner_width_m"] == 2.0 assert spec["inner_height_m"] == 2.0 - assert spec["wall_thickness_m"] == FORD_WALL_THICKNESS_M - assert spec["slab_thickness_m"] == FORD_SLAB_THICKNESS_M - assert spec["top_thickness_m"] == FORD_SLAB_THICKNESS_M + assert spec["wall_thickness_m"] == 0.3 + assert spec["slab_thickness_m"] == 0.25 + assert spec["top_thickness_m"] == 0.25 assert spec["cover_m"] == BOX_COVER_M # 도로 방향 길이 = 내공 폭 + 측벽 두 장. - assert spec["span_m"] == 2.0 + 2 * FORD_WALL_THICKNESS_M + assert spec["span_m"] == 2.0 + 2 * 0.3 def test_box_wing_extends_slab() -> None: """저판 편측 연장 = 날개벽 길이 × cos(각도). 세월교와 같은 산식이다.""" spec = _box_set({"body_width_m": 3, "body_height_m": 3, "wing_in_length_m": 2}) - assert spec["span_m"] == 3 + 2 * FORD_WALL_THICKNESS_M + assert spec["span_m"] == 3 + 2 * 0.3 assert spec["wing_in"]["slab_extend_m"] == round(2 * math.cos(math.radians(45)), 3) diff --git a/resources/tester/test_b06_box_thickness_registry.py b/resources/tester/test_b06_box_thickness_registry.py new file mode 100644 index 00000000..dfabe774 --- /dev/null +++ b/resources/tester/test_b06_box_thickness_registry.py @@ -0,0 +1,61 @@ +"""BOX암거 두께·연장 칸 — 등록부가 정본, B06 그림이 그 칸을 읽음 (2026-09-15 브레인 12장 D ①③). + +① 제안값 = 산림과임업기술(임도) 5장 수량산출 예제 — 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1 + 근거에 KDS 44 90 00 4.5.2 「부재 최소두께 300㎜」(도로 기준)를 나란히 — 판 0.25 는 못 미침 +② 종전 B06 상수(벽 0.2 · 판 0.3 — 세월교 값 승계)는 근거 없는 값이라 걷음 +③ 연장 칸 — 기본값 없음 +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from B06_Section.B06_Section_Engine_Culvert import _box_set + +ROOT = Path(__file__).resolve().parents[2] + + +def _box_options() -> dict[str, dict]: + registry = json.loads( + (ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8") + ) + types = registry if isinstance(registry, list) else registry.get("types") or [] + box = next(t for t in types if t["type_id"] == "box_culvert") + return {option["key"]: option for option in box["options"]} + + +def test_등록부에_두께_헌치_버림_연장_칸이_선다() -> None: + options = _box_options() + expected = { + "wall_thickness_m": 0.3, + "slab_thickness_m": 0.25, + "haunch_m": 0.2, + "blinding_thickness_m": 0.1, + } + for key, value in expected.items(): + assert options[key]["default"] == value, key + assert "산림과임업기술" in options[key]["default_basis"], key + # 판 0.25 는 KDS 도로 최소 300㎜ 에 못 미침 — 나란히 적어 설계자가 고르게. + assert "KDS" in options["slab_thickness_m"]["default_basis"] + assert "300" in options["slab_thickness_m"]["default_basis"] + assert "KDS" in options["wall_thickness_m"]["default_basis"] + # 연장은 기본값 없음. + assert options["length_m"]["default"] is None + + +def test_B06_세트가_등록부_칸을_읽는다() -> None: + spec = _box_set(None) + assert (spec["wall_thickness_m"], spec["slab_thickness_m"], spec["top_thickness_m"]) == ( + 0.3, + 0.25, + 0.25, + ) + assert spec["span_m"] == 2.0 + 2 * 0.3 + stored = _box_set({"wall_thickness_m": 0.35, "slab_thickness_m": 0.3}) + assert (stored["wall_thickness_m"], stored["slab_thickness_m"], stored["top_thickness_m"]) == ( + 0.35, + 0.3, + 0.3, + ) + assert stored["span_m"] == 2.0 + 2 * 0.35 diff --git a/resources/tester/test_b08_box_culvert_unit.py b/resources/tester/test_b08_box_culvert_unit.py new file mode 100644 index 00000000..5f98a5fe --- /dev/null +++ b/resources/tester/test_b08_box_culvert_unit.py @@ -0,0 +1,102 @@ +"""BOX암거 전개식 — m당 × 연장 (2026-09-15 브레인 12장 D 판정 ①~⑤). + +근거 둘(구조 같음) + 산림과임업기술(임도) 5장 「구조물도에 의한 수량산출(예)」 — 구체 = (외곽 − 유수구 + 헌치) · + 기초 폭×두께 · 거푸집 외벽·유수구·받침판·귀면 · 동바리 유수구 단면 + 실무 울진 기번3 구조도 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 · 버림 0.28 · 유로폼 11.132 · + 합판 0.2 · 철근 0.336t(H13 0.179 · H16 0.157) · 동바리 3.92 · 비계 벽체 5.2 + 시종점 13.52 +⚠ 철근은 크기별 식이 없어 **2×2 관측값만** · 날개벽·토공은 사유. +""" + +from __future__ import annotations + +import pytest + +from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff +from B08_Quantity.B08_Quantity_Engine_Pipe import facility_structures +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table, expand + +ULJIN_2X2 = { + "body_width_m": 2.0, + "body_height_m": 2.0, + "wall_thickness_m": 0.3, + "slab_thickness_m": 0.3, + "haunch_m": 0.2, + "blinding_thickness_m": 0.1, + "length_m": 10.0, +} + + +def _amounts(options: dict) -> tuple[dict, list[str], object]: + result = expand({"type_id": "box_culvert", "options": options}, {"box_culvert": "BOX암거"}) + return ( + {(c.name, c.spec): c.amount for c in result.components}, + result.notes, + result, + ) + + +def test_울진_2x2_m당_값을_연장에_곱한다() -> None: + got, notes, result = _amounts(ULJIN_2X2) + assert got[("콘크리트", "")] == pytest.approx(2.84 * 10) + assert got[("버림콘크리트", "")] == pytest.approx(0.28 * 10) + assert got[("유로폼", "")] == pytest.approx( + 11.1314 * 10, abs=0.01 + ) # 원문 11.132(귀면 0.566 반올림) + assert got[("합판거푸집", "")] == pytest.approx(0.2 * 10) + assert got[("이형철근", "D13")] == pytest.approx(179 * 10) + assert got[("이형철근", "D16")] == pytest.approx(157 * 10) + assert got[("동바리", "")] == pytest.approx(3.92 * 10) + # 비계 — 벽체 m당 5.2 × 연장 + 시종점 두 면 13.52 는 한 번(원문은 m당에 넣었음 · 사유). + assert got[("비계", "")] == pytest.approx(5.2 * 10 + 13.52) + assert (result.billing_unit, result.billing_quantity) == ("m", 10.0) + text = " ".join(notes) + assert "관측" in text and "날개벽" in text + + +def test_예제_두께_기본값으로_구체가_줄어든다() -> None: + got, _notes, _r = _amounts({**ULJIN_2X2, "slab_thickness_m": 0.25}) + # (2.6 × 2.5 − 2 × 2 + 0.2² ÷ 2 × 4) = 2.58㎥/m + assert got[("콘크리트", "")] == pytest.approx(2.58 * 10) + + +def test_2x2_가_아니면_철근은_사유() -> None: + got, notes, _r = _amounts({**ULJIN_2X2, "body_width_m": 3.0}) + assert not [key for key in got if key[0] == "이형철근"] + assert any("크기별 식 없음" in note and "2×2" in note for note in notes) + assert got[("콘크리트", "")] > 0 # 나머지는 섬 + + +def test_연장이_없으면_안_선다() -> None: + got, notes, _r = _amounts({k: v for k, v in ULJIN_2X2.items() if k != "length_m"}) + assert got == {} + assert any("연장" in note for note in notes) + + +def test_치수를_안_적으면_기본값으로_서되_미확정() -> None: + rows = facility_structures( + [{"chainage_m": 50.0, "facility": "box_culvert", "options": {"length_m": 8.0}}] + ) + box = next(row for row in rows if row["type_id"] == "box_culvert") + assert box["unconfirmed"] and "측벽 두께" in box["unconfirmed"] + assert box["options"]["wall_thickness_m"] == 0.3 and box["options"]["slab_thickness_m"] == 0.25 + stored = facility_structures( + [{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}] + ) + assert not next(r for r in stored if r["type_id"] == "box_culvert").get("unconfirmed") + + +def test_인계_묶음이_조각을_세운다() -> None: + table = build_table( + facility_structures( + [{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}] + ), + {"box_culvert": "BOX암거"}, + ) + row = next( + r for r in build_handoff(unit_quantity_table=table)["work_items"] if "BOX" in r["name"] + ) + parts = {part["code"].split("#")[0]: part for part in row["composite_parts"] or []} + assert {"FP-12-01-01", "FP-12-04", "FP-12-38", "FP-12-03", "FP-12-20", "FP-12-19"} <= set(parts) + assert parts["FP-12-03"]["quantity"] == pytest.approx((179 + 157) * 10 / 1000) + assert parts["FP-12-20"]["quantity"] == pytest.approx(3.92 * 10) diff --git a/resources/tester/test_b08_facility_structures.py b/resources/tester/test_b08_facility_structures.py index d0cf477c..5c3c3163 100644 --- a/resources/tester/test_b08_facility_structures.py +++ b/resources/tester/test_b08_facility_structures.py @@ -116,7 +116,8 @@ def test_독립_기슭막이_높이는_설계자_입력_없으면_미확정과_ def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None: table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}]) - assert "산출식이 아직 없습니다" in table["structures"][0]["notes"][0] + # 2026-09-15 브레인 판정 ⑥ — 「아직 없음」이 아니라 「근거 없음」으로 닫음. + assert "세월교 수량 근거 없음" in table["structures"][0]["notes"][0] rows = build_handoff(unit_quantity_table=table)["work_items"] assert not any("터파기가 안 선 구조물" in str(row.get("spec_detail")) for row in rows) diff --git a/resources/tester/test_b08_formwork.py b/resources/tester/test_b08_formwork.py index 52d7cb82..d7369440 100644 --- a/resources/tester/test_b08_formwork.py +++ b/resources/tester/test_b08_formwork.py @@ -114,8 +114,22 @@ def test_동바리는_대상이_없으면_없다고_말할것() -> None: status = shoring_status() assert status["applicable"] is False assert status["reason"] - assert set(status["pending_types"]) == {"box_culvert", "ford_bridge"} + # BOX암거는 2026-09-15 전개식이 서서 대상에서 빠짐 — 세월교만 남음(근거 없음 사유). + assert set(status["pending_types"]) == {"ford_bridge"} def test_표에도_동바리_상태가_실림() -> None: assert build_table([옹벽()])["shoring"]["applicable"] is False + box = { + "type_id": "box_culvert", + "options": { + "body_width_m": 2.0, + "body_height_m": 2.0, + "wall_thickness_m": 0.3, + "slab_thickness_m": 0.3, + "haunch_m": 0.2, + "blinding_thickness_m": 0.1, + "length_m": 5.0, + }, + } + assert build_table([box])["shoring"]["applicable"] is True diff --git a/resources/tester/test_b08_observed_unit.py b/resources/tester/test_b08_observed_unit.py index ee911672..c185da70 100644 --- a/resources/tester/test_b08_observed_unit.py +++ b/resources/tester/test_b08_observed_unit.py @@ -169,13 +169,16 @@ def test_숫자와_글자_규격을_같이_본다() -> None: assert table.find("retaining_wall", {"form": "반중력식", "height_m": 1.9999}) is None -def test_BOX암거는_미확보로_남음() -> None: - """⚠ 두께가 저장돼 있지 않아 식도 못 세우고 관측값도 없다 — 지어내면 콘크리트·거푸집· - 철근으로 번져 나간다. 사용자 확정 대기.""" +def test_BOX암거는_관측표가_아니라_전개식으로_섬() -> None: + """2026-09-15 — 두께 칸이 등록부에 서고 전개식(`_UnitQuantity_Box`)이 생겨 관측표 몫이 아님. + + 관측값은 여전히 없음(울진 2×2 철근만 전개식 안에 관측값으로 듦) · 세월교는 사유로 닫힘. + """ table = load_observed_table() assert table.find("box_culvert", {"body_width_m": 3.0, "body_height_m": 1.2}) is None - reasons = [item["type_id"] for item in table.not_found["items"]] - assert "box_culvert" in reasons + reasons = {item["type_id"]: item for item in table.not_found["items"]} + assert "box_culvert" not in reasons + assert "세월교 수량 근거 없음" in reasons["ford_bridge"]["why"] def test_파일이_없으면_전부_미확보() -> None: