From 6a7c339f8a330b05156990d33bd4899cc9fd57e2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:22:58 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(B06):=20=EC=B8=A1=EA=B5=AC=ED=84=B0?= =?UTF-8?q?=ED=8C=8C=EA=B8=B0=20=EB=8B=A8=EB=A9=B4=EC=A0=81=EC=9D=84=20?= =?UTF-8?q?=ED=86=A0=EC=82=AC/=EC=95=94=EC=9C=BC=EB=A1=9C=20=EA=B0=80?= =?UTF-8?q?=EB=A6=84=20=E2=80=94=20=EC=83=88=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=95=94=EB=B0=98=20=EA=B2=BD=EA=B3=84?= =?UTF-8?q?=EC=84=A0=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 별표2 Ⅰ.1.나.(5) 「측구터파기 단면적」이 횡단도 표의 법정 칸인데 한 값뿐이라 「측구 토사 / 측구 암석」 두 칸이 반만 채워졌다. - 가르는 근거는 **절토 분리와 같은 것**(지반 유형 + 암반 경계선). 새 입력을 만들지 않았다. - 측구 상단에서 암반 경계선까지의 깊이로 공칭 도형(사다리꼴·L형)을 가로로 가른다. - ⚠ 근거가 없으면 **나누지 않는다.** 사유를 `ditch_split_basis` 로 함께 냄: rock_boundary / soil_ground / rock_ground_no_boundary / no_ditch. - 기존 `ditch_area_m2` 는 **합계로 그대로** 두고 갈래를 덧붙였다 — B08 이 순서대로 옮겨 갈 수 있게. - 파이썬·TS 짝을 함께 고침. 거울 테스트 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Engine_Areas.py | 35 +++++++++++++++ B06_Section/B06_Section_Engine_Design.py | 29 ++++++++++++ common_util/common_util_cross_design.ts | 45 ++++++++++++++++++- common_util/common_util_cross_design_areas.ts | 33 ++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/B06_Section/B06_Section_Engine_Areas.py b/B06_Section/B06_Section_Engine_Areas.py index a3df5142..62220348 100644 --- a/B06_Section/B06_Section_Engine_Areas.py +++ b/B06_Section/B06_Section_Engine_Areas.py @@ -131,3 +131,38 @@ def _bench_cut_length(offsets: list[float], grounds: list[float], diffs: list[fl continue total += ((run**2 + rise**2) ** 0.5) * share return total + + +def _split_ditch_area(ditch_spec: dict, depth_to_boundary_m: float | None) -> tuple[float, float]: + """측구 단면적을 (토사, 암반)으로 가른다 — 암반 경계선까지의 깊이 기준. + + `depth_to_boundary_m` 은 **측구 상단에서 암반 경계선까지의 깊이(m)** 다. + `None` 이면 가를 근거가 없다는 뜻이라 부르는 쪽이 처리한다(여기서는 안 부른다). + + ⚠ 측구 단면은 **공칭 도형**(사다리꼴·L형 근사)이라 지반선을 따라 적분하지 않는다. + 경계선도 그 자리 한 높이로 본다 — 폭 1m 안팎에서 지반선 기울기 차이는 도형 근사보다 + 작다. 절토 면적 분리(`_split_cut_areas`)가 균일 두께를 쓰는 것과 같은 태도다. + """ + kind = str(ditch_spec.get("type") or "none") + if kind == "l_type": + width = float(ditch_spec.get("width_m") or 0.0) + depth = float(ditch_spec.get("depth_m") or 0.0) + total = width * depth / 2.0 + if depth <= 0 or width <= 0: + return 0.0, 0.0 + d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth) + # 깊이 d 에서의 가로 폭 = W(1 − d/D). 위에서 d0 까지 적분한다. + soil = width * d0 - width * d0 * d0 / (2.0 * depth) + return soil, max(total - soil, 0.0) + if kind == "standard": + top = float(ditch_spec.get("top_width_m") or 0.0) + bottom = min(float(ditch_spec.get("bottom_width_m") or 0.0), top) + depth = float(ditch_spec.get("depth_m") or 0.0) + total = (top + bottom) / 2.0 * depth + if depth <= 0 or top <= 0: + return 0.0, 0.0 + d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth) + # 깊이 d 에서의 폭 = top − (top−bottom)·d/depth. 위에서 d0 까지 적분한다. + soil = top * d0 - (top - bottom) * d0 * d0 / (2.0 * depth) + return soil, max(total - soil, 0.0) + return 0.0, 0.0 diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index c4142964..41beaaf8 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -34,6 +34,7 @@ from typing import Any from B06_Section.B06_Section_Engine_Areas import ( _bench_cut_length, _split_cut_areas, + _split_ditch_area, _trapezoid_areas, ) from common_util.common_util_cross_berm import ( @@ -749,6 +750,28 @@ def compute_cross_design( "depth_m": group["ditch_depth_m"], } + # 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거 + # (지반 유형 + 암반 경계선)를 그대로 쓴다. 별표2 Ⅰ.1.나.(5) 「측구터파기 단면적」이 + # 횡단도 표의 법정 칸이라 반만 채워 나가면 안 된다(2026-09-09). + # 근거가 없으면 **나누지 않고** 사유를 함께 내보낸다 — 절반을 임의로 가르지 않는다. + if not geometry.has_ditch: + ditch_soil_area, ditch_rock_area = 0.0, 0.0 + ditch_split_basis = "no_ditch" + elif preset_key != "rock": + # 토사 지반 — 암반 경계선 자체가 없다. 전량 토사(절토 분리와 같은 판정). + ditch_soil_area, ditch_rock_area = ditch_area, 0.0 + ditch_split_basis = "soil_ground" + elif rock_boundary_offset_m is None or not geometry.ditch_points: + # 암 지반인데 경계선 값이 없다(구 데이터) — 가를 근거가 없으므로 전량 암. + ditch_soil_area, ditch_rock_area = 0.0, ditch_area + ditch_split_basis = "rock_ground_no_boundary" + else: + ditch_top_z = geometry.ditch_points[0][1] + mid_offset = sum(point[0] for point in geometry.ditch_points) / len(geometry.ditch_points) + boundary_z = ground_at(mid_offset) - abs(float(rock_boundary_offset_m)) + ditch_soil_area, ditch_rock_area = _split_ditch_area(ditch_spec, ditch_top_z - boundary_z) + ditch_split_basis = "rock_boundary" + # 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo한다(D-2, 표시·저장용). if geometry.left_role == "cut" and geometry.right_role == "cut": resolved_mode = "both_cut" @@ -824,6 +847,12 @@ def compute_cross_design( round(fill_ground_slope, 4) if fill_ground_slope is not None else None ), "ditch_area_m2": round(ditch_area, 4), + # 측구터파기 내역(합=ditch_area_m2). 가른 근거는 `ditch_split_basis` 로 함께 낸다: + # rock_boundary(암반 경계선으로 가름) · soil_ground(토사 지반이라 전량 토사) · + # rock_ground_no_boundary(암 지반인데 경계선 없음 — 전량 암) · no_ditch(측구 없음). + "ditch_soil_area_m2": round(ditch_soil_area, 4), + "ditch_rock_area_m2": round(ditch_rock_area, 4), + "ditch_split_basis": ditch_split_basis, "design_line": design_line, # 절토 사면을 경사 구간별로 쪼갠 목록(소단 제외). # ⚠ **지금 이 값을 읽는 곳은 없다**(2026-09-07). 원래 임자였던 별표2 법정 경사 검사는 diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 9eee140d..b011ac78 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -24,7 +24,12 @@ * ========================================================================== */ import type { BermSpec } from "./common_util_cross_berm"; -import { benchCutLength, splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas"; +import { + benchCutLength, + splitCutAreas, + splitDitchArea, + trapezoidAreas, +} from "./common_util_cross_design_areas"; // 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04). import { CURVE_WIDENING_MAX_WIDTH_M, @@ -136,6 +141,10 @@ export interface CrossDesignResult { slope_unclosed: boolean; fill_ground_slope: number | null; ditch_area_m2: number; + /** 측구터파기 내역(합=ditch_area_m2)과 가른 근거. 짝: 파이썬 `ditch_split_basis`. */ + ditch_soil_area_m2: number; + ditch_rock_area_m2: number; + ditch_split_basis: string; design_line: CrossDesignEdge[]; /** 절토 사면 경사 구간(소단 제외). 짝: `cut_slope_segments`. * ⚠ **지금 읽는 곳은 없다**(2026-09-07) — 임자였던 별표2 검사는 폐기됐고 저장분에도 @@ -395,6 +404,37 @@ export function computeCrossDesign( }; } + // 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거 + // (지반 유형 + 암반 경계선)를 그대로 쓴다. 근거가 없으면 나누지 않고 사유를 낸다. + // ⚠ 파이썬 짝: `B06_Section_Engine_Design` 의 같은 자리. + let ditchSoilArea: number; + let ditchRockArea: number; + let ditchSplitBasis: string; + if (!geometry.hasDitch) { + ditchSoilArea = 0; + ditchRockArea = 0; + ditchSplitBasis = "no_ditch"; + } else if (presetKey !== "rock") { + ditchSoilArea = ditchArea; + ditchRockArea = 0; + ditchSplitBasis = "soil_ground"; + } else if ( + rockBoundaryOffsetM === null || + rockBoundaryOffsetM === undefined || + !geometry.ditchPoints.length + ) { + ditchSoilArea = 0; + ditchRockArea = ditchArea; + ditchSplitBasis = "rock_ground_no_boundary"; + } else { + const ditchTopZ = geometry.ditchPoints[0][1]; + const midOffset = + geometry.ditchPoints.reduce((sum, point) => sum + point[0], 0) / geometry.ditchPoints.length; + const boundaryZ = groundAt(midOffset) - Math.abs(rockBoundaryOffsetM); + [ditchSoilArea, ditchRockArea] = splitDitchArea(ditchSpec, ditchTopZ - boundaryZ); + ditchSplitBasis = "rock_boundary"; + } + // 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo 한다(D-2). let resolvedMode: string; if (geometry.leftRole === "cut" && geometry.rightRole === "cut") resolvedMode = "both_cut"; @@ -457,6 +497,9 @@ export function computeCrossDesign( slope_unclosed: slopeUnclosed, fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope), ditch_area_m2: round4(ditchArea), + ditch_soil_area_m2: round4(ditchSoilArea), + ditch_rock_area_m2: round4(ditchRockArea), + ditch_split_basis: ditchSplitBasis, design_line: designLine, cut_slope_segments: geometry.cutSlopeSegments(), }; diff --git a/common_util/common_util_cross_design_areas.ts b/common_util/common_util_cross_design_areas.ts index 83e6fb89..dfb41184 100644 --- a/common_util/common_util_cross_design_areas.ts +++ b/common_util/common_util_cross_design_areas.ts @@ -126,3 +126,36 @@ export function benchCutLength(offsets: number[], grounds: number[], diffs: numb } return total; } + +/** + * 측구 단면적을 [토사, 암반]으로 가른다 — 측구 상단에서 암반 경계선까지의 깊이(m) 기준. + * ⚠ 파이썬 짝: `B06_Section_Engine_Areas._split_ditch_area`. 한 벌로 움직인다. + * 측구 단면은 공칭 도형이라 지반선을 따라 적분하지 않는다 — 경계선도 그 자리 한 높이로 본다. + */ +export function splitDitchArea( + ditchSpec: Record, + depthToBoundaryM: number | null, +): [number, number] { + const kind = String(ditchSpec.type ?? "none"); + const clamp = (depth: number): number => Math.min(Math.max(depthToBoundaryM ?? 0, 0), depth); + if (kind === "l_type") { + const width = Number(ditchSpec.width_m ?? 0); + const depth = Number(ditchSpec.depth_m ?? 0); + if (depth <= 0 || width <= 0) return [0, 0]; + const total = (width * depth) / 2; + const d0 = clamp(depth); + const soil = width * d0 - (width * d0 * d0) / (2 * depth); + return [soil, Math.max(total - soil, 0)]; + } + if (kind === "standard") { + const top = Number(ditchSpec.top_width_m ?? 0); + const bottom = Math.min(Number(ditchSpec.bottom_width_m ?? 0), top); + const depth = Number(ditchSpec.depth_m ?? 0); + if (depth <= 0 || top <= 0) return [0, 0]; + const total = ((top + bottom) / 2) * depth; + const d0 = clamp(depth); + const soil = top * d0 - ((top - bottom) * d0 * d0) / (2 * depth); + return [soil, Math.max(total - soil, 0)]; + } + return [0, 0]; +} From c39212d7b5a2943cb5a3ffb93cd339dea2a97e47 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:23:36 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(B07):=20=ED=9A=A1=EB=8B=A8=EB=8F=84=20?= =?UTF-8?q?=EC=88=98=EB=9F=89=ED=91=9C=204=EC=B9=B8=20=E2=86=92=2014?= =?UTF-8?q?=EC=B9=B8=20=E2=80=94=20=EC=A0=80=EC=9E=A5=EB=90=9C=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=EC=97=90=EC=84=9C=20=EC=B1=84=EC=9A=B0=EB=8A=94=20?= =?UTF-8?q?=ED=86=B5=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 별표2 Ⅰ.1.나.(5) 가 요구하는 여덟(지반고·계획고·절토고·성토고·단면적·지장목 제거· 측구터파기 단면적·사면보호공) 중 뒤 넷이 빈칸으로 나가던 자리. 빈칸의 까닭은 「값이 없어서」가 아니라 **통로가 없어서**였음 — 표가 `source["quantities"]` 를 보는데 그 키가 원본 파일에 아예 없음(실측: 횡단 원본 15키에 `samples`·`center_z`· `chainage_m` 은 있고 `quantities` 는 없음). 반면 저장된 설계에는 단면적이 그대로 있고 사면길이도 설계선에서 유도됨. ⚠ 계산을 새로 짜지 않음 — 단면적은 B06 저장값을 그대로 읽고, 사면 계열은 B08 이 쓰는 `station_slope` 를 그대로 부름. 계열 이름·어느 면을 쓰나도 `SlopeArea` 정의를 빌림 (거기서 밑수가 바뀌면 이 표도 같이 움직여야 함). 채운 열 칸 단면적 3 깍기 토사 · 깍기 암석 · 쌓기 사면 7 층따기 · 면고르기(성·절) · 지장목제거(성·절) · 성토파종 · 절토살포 ⚠ 사면 칸의 단위 — B08 은 측점 사이를 적분해 ㎡ 를 내나 그것은 두 측점이 있어야 나옴. 한 장짜리 횡단도에 들어가는 것은 그 측점의 **사면길이(m)** 이고 1m 폭 조각의 ㎡/m 와 수치가 같음. 표기를 m 로 볼지 ㎡/m 로 볼지는 표기 문제이고 값은 하나임. 안 채운 칸 — 근거가 없어 임의로 안 넣음 측구 토사/암석(저장값이 `ditch_area_m2` 한 값뿐) · 표토제거(두께 칸 없음) · 편책(별도 일위대가 제작 뒤 연결로 확정) · 제근(입목 본수 안 듦) · 노면다짐 자체검증 — 새 시험 8건(사면길이는 손으로 잰 √5 · √(2.4²+2²) 와 대조, `is not None` 만 보면 0 이어도 통과하므로 실수치로 맞춤), 회귀 570 통과 · 0 실패. 실프로젝트 936be972 세 측점에서 **21칸 중 14칸**에 값이 찍힘(앞서 4칸). Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Engine_Cross_Quantity.py | 91 +++++++++++++++++++ .../B07_DesignDetail_Router_Support.py | 8 ++ 2 files changed, 99 insertions(+) create mode 100644 B07_DesignDetail/B07_DesignDetail_Engine_Cross_Quantity.py diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cross_Quantity.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cross_Quantity.py new file mode 100644 index 00000000..5306f1ee --- /dev/null +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cross_Quantity.py @@ -0,0 +1,91 @@ +"""횡단도 아래 수량 산출표 — **저장된 횡단 설계에서 칸을 채운다**. + +왜 필요한가 (법정 요구) + 별표2 Ⅰ.1.나.(5) 가 횡단면도에 「지반고·계획고·절토고·성토고·**단면적·지장목 제거· + 측구터파기 단면적·사면보호공**」 여덟을 요구한다. 지금 나가는 것은 **앞 넷뿐**이고 + 뒤 넷이 빈칸으로 나갔다. 그 구멍을 메우는 자리다. + +⚠ 빈칸의 까닭은 「값이 없어서」가 아니었다 + 표가 `source["quantities"]` 를 보는데 **그 키가 원본 파일에 아예 없다**(실측: + `cross_00960m.json` 에 `samples`·`center_z`·`frame` 뿐). 반면 **저장된 횡단 설계에는 + 단면적이 그대로 있고**(`cut_soil_area_m2` 등), **사면길이도 그 설계선에서 유도된다** + (`B08_Quantity_Engine_SlopeLength.station_slope`). 즉 **통로만 없었다.** + +⚠ 계산을 새로 짜지 않는다 (CLAUDE.md 5장) + 단면적은 B06 이 낸 저장값을 **그대로 읽고**, 사면 계열은 **B08 이 쓰는 그 함수**를 부른다. + 계열 이름과 「어느 면을 쓰나」도 `B08_Quantity_Engine_SlopeArea` 의 정의를 빌려 쓴다 — + 거기서 밑수가 바뀌면 이 표도 같이 움직여야 하기 때문이다. + +⚠ 사면 계열 칸의 **단위** + B08 은 측점 사이를 평균단면적법으로 적분해 **면적(㎡)** 을 내지만, 그것은 두 측점이 있어야 + 나오는 값이라 **한 장짜리 횡단도에는 못 쓴다.** 횡단도 칸에 들어가는 것은 그 측점의 + **사면길이(m)** 이고, 이는 **1m 폭 조각의 면적(㎡/m)과 수치가 같다.** + ⇒ 표에 「m 로 볼 것인가 ㎡/m 로 볼 것인가」는 표기 문제이고 **값은 하나다.** + +아직 안 채우는 칸 — 근거가 없다(임의로 넣지 않는다) + · 측구 토사/암석 — 저장값이 `ditch_area_m2` **한 값뿐**이라 토사·암석으로 못 가른다 + · 표토제거 성토/절토 — 법은 「전량 제거」인데 **두께 칸이 없어** 물량이 안 선다 + · 편책 — 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4). 밑수는 그때 붙는다 + · 제근 — 입목 본수를 안 든다 + · 노면다짐 — 밑수(노면 폭)는 있으나 이 표의 다른 칸과 축이 달라 뒤로 미룸 +""" + +from __future__ import annotations + +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_SlopeArea import PROTECTION_SOURCE, _key, _length_of +from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slope + +#: 저장된 설계 단면적을 그대로 옮기는 칸 — `표 키 → 설계 키`. +AREA_KEYS: tuple[tuple[str, str], ...] = ( + ("cut_soil", "cut_soil_area_m2"), + ("cut_rock", "cut_rock_area_m2"), + ("embankment", "fill_area_m2"), +) + +#: 사면 계열에서 오는 칸 — `표 키 → (B08 계열, 면)`. +#: 성토파종·절토살포는 **법면보호공**이고, 그것은 B08 에서 면고르기를 참조한다 +#: (`PROTECTION_SOURCE`). 참조를 끊으면 그쪽 한 곳만 고치면 이 표도 따라온다. +SLOPE_KEYS: tuple[tuple[str, str, str], ...] = ( + ("benching", "bench_cut", "fill"), + ("grading_fill", "face_dressing", "fill"), + ("grading_cut", "face_dressing", "cut"), + ("tree_removal_fill", "tree_removal", "fill"), + ("tree_removal_cut", "tree_removal", "cut"), + ("fill_seeding", PROTECTION_SOURCE, "fill"), + ("cut_spraying", PROTECTION_SOURCE, "cut"), +) + + +def _num(value: Any) -> float | None: + return float(value) if isinstance(value, (int, float)) else None + + +def derived_cells(chainage_m: float, design: dict[str, Any] | None) -> dict[str, float | None]: + """저장된 횡단 설계 하나에서 **채울 수 있는 칸**만 낸다. + + 설계가 없거나 설계선이 없으면 **빈 dict** 를 낸다 — 0 으로 때우지 않는다. + 도면에 0 이 찍히면 「없다」와 「안 쟀다」를 구별할 수 없다. + """ + if not isinstance(design, dict) or not design: + return {} + + cells: dict[str, float | None] = {} + for table_key, design_key in AREA_KEYS: + value = _num(design.get(design_key)) + if value is not None: + cells[table_key] = value + + # 사면길이는 설계선에서 유도한다 — 설계선이 없으면 유도할 것이 없다. + if not design.get("design_line"): + return cells + + slope = station_slope(float(chainage_m), design) + lengths = { + _key(series, face): _length_of(slope, series, face) + for _table_key, series, face in SLOPE_KEYS + } + for table_key, series, face in SLOPE_KEYS: + cells[table_key] = lengths[_key(series, face)] + return cells diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index ef7bfcc3..b2b9488c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -54,6 +54,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import ( from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( QUANTITY_VALUE_KEYS, ) +from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import derived_cells from B07_DesignDetail.B07_DesignDetail_Engine_Template import add_title_fields # 유역도 배경·파일 입출력 조각은 700줄 제한으로 떼어냈다(2026-09-04). @@ -270,6 +271,11 @@ def _quantity_table( **계획고는 횡단 설계(design)에도 있다** — 장 배치 입력의 원본에는 그 값이 없어 계획고·절토고·성토고 세 칸이 통째로 비어 나갔다(2026-09-03 실측: 장 확정 시 21개 항목 중 지반고 하나만 채워짐). 원본에 없으면 설계에서 읽는다. + + ⚠ **본문 칸도 설계에서 온다**(2026-09-09) — `source["quantities"]` 키는 원본 파일에 + 아예 없어 열일곱 칸이 통째로 비어 나갔다. 채울 수 있는 것은 `_Engine_Cross_Quantity` + 가 낸다(단면적은 저장값, 사면 계열은 B08 이 쓰는 함수 그대로). 별표2 법정 요구 + 여덟 중 뒤 넷이 비던 자리다. """ def num(value: Any) -> float | None: @@ -289,6 +295,8 @@ def _quantity_table( "cut": cut, "fill": fill, } + chainage = num(source.get("chainage_m")) or 0.0 + table.update(derived_cells(chainage, design)) for key in QUANTITY_VALUE_KEYS: table.setdefault(key, num(quantities.get(key))) return table From 11394d40119dc6ca75b04bca1729b36ada2715ed Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:32:38 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(B05):=20=EB=8F=8C=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=EC=97=90=20=E3=80=8C=EC=A1=B0=EB=8B=AC(=EC=B1=84?= =?UTF-8?q?=EC=A7=91/=EA=B5=AC=EC=9E=85)=E3=80=8D=20=EC=B9=B8=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4=20=E2=80=94=20=EA=B8=B0=EB=B3=B8=20=EC=B1=84=EC=A7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 엔진(`B08_Quantity_Engine_UnitQuantity.STONE_SUPPLY_KEYS`)은 이미 `stone_supply` 를 읽는데 저장 칸이 없어 저장이 거부됐다(`정의되지 않은 옵션입니다: stone_supply`). `back_len_cm` 때와 같은 계열 — 읽는 키와 저장 칸이 어긋난 자리. - 붙인 곳: 돌쌓기(찰·메) · 큰돌쌓기 · 골막이 · 바닥막이 · 기슭막이 여섯. - 기본 「채집」 — 사용자 확정 ②(2026-09-09) 「기본은 캔다, 구조물마다 바꿀 수 있게」. 별표2 「석축 등에 필요한 야면석 등은 가급적 현장에서 채취·사용」이 근거. - 저장 왕복을 실제로 해 봄: PUT 200 → 되읽기에서 `stone_supply="구입"` 그대로 나옴. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_Structure_Types.json | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 1e898cfb..bfb217ae 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -770,6 +770,15 @@ "required": true, "phase": "detail" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -844,6 +853,15 @@ "required": true, "phase": "detail" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -983,6 +1001,15 @@ "required": true, "phase": "detail" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -1079,6 +1106,15 @@ "required": true, "phase": "detail" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "length_m", "label": "길이", @@ -1115,6 +1151,15 @@ "required": false, "phase": "b05" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "area_m2", "label": "면적", @@ -1214,6 +1259,15 @@ "required": false, "phase": "b05" }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": ["채집", "구입"], + "default": "채집", + "required": false, + "phase": "detail" + }, { "key": "height_m", "label": "높이",