From 9cd2920afcf8ca5beb27ce84a85a68001c21731a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 09:01:37 +0900 Subject: [PATCH 1/3] =?UTF-8?q?knowledge(=EC=B6=95C=20=EB=AA=85=EC=84=B8):?= =?UTF-8?q?=20=EC=B4=9D=EC=95=A1=201,000=EC=9B=90=20=EB=AF=B8=EB=A7=8C?= =?UTF-8?q?=EB=B2=84=EB=A6=BC=20=EC=8B=A4=EC=B8=A1=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고시 다섯 줄 중 총액 줄만 우리가 안 따르고 있었음(기본값 꺼짐) - 실무 6건 끝 세 자리 000 ↔ 우리 373·695·578·654·341·639 - 곁 버그 — 절사 자동보정이 둘째 걸음부터 ÷1.1 누락으로 진동 - 집계표 ROUND 는 설계서 소계와 다른 자리로 봄 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0154EMdwPYNPuKc9eZv8SLEX --- docs/raw/verification/2026-09-13_축C_명세.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/raw/verification/2026-09-13_축C_명세.md b/docs/raw/verification/2026-09-13_축C_명세.md index bcb21055..a9e4d7bd 100644 --- a/docs/raw/verification/2026-09-13_축C_명세.md +++ b/docs/raw/verification/2026-09-13_축C_명세.md @@ -358,7 +358,7 @@ B08 인계본 `ground_class_aliases` — **B09 가 안 읽음**)을 한 벌로 | 종목 | 단위 | 지위 | 비고 | 우리 실측과 | | --- | --- | --- | --- | --- | -| 설계서의 **총액** | 원 | **1,000** | 미만버림 | ⚠ **아직 확인 안 됨** — 데스크탑 서브가 재는 중 | +| 설계서의 **총액** | 원 | **1,000** | 미만버림 | ⚠ **우리는 안 버리고 있었음** — 실무 6건은 여섯 다 `000`, 우리는 `373·695·578·654·341·639`. 기본을 켜는 것으로 판정(2026-09-14) | | 설계서의 소계 | 원 | 1 | 미만버림 | ✅ | | 설계서의 **금액란** | 원 | 1 | 미만버림 | ✅ 「내역에 실릴 때 정수 절사」와 같음 | | **일위대가표의 계금** | 원 | 1 | 미만버림 | ✅ 「호표 안 성분 소계 절사」와 같음 | @@ -366,6 +366,15 @@ B08 인계본 `ground_class_aliases` — **B09 가 안 읽음**)을 한 벌로 ⇒ **STmate 를 재서 얻은 규칙이 법 조문이었음.** 경쟁사를 베낀 것이 아니라 둘 다 같은 고시를 따른 것임. +⚠ **총액 줄만 우리가 안 따르고 있었음** (2026-09-14 데스크탑 서브 실측). 기본값이 꺼져 있어(`cut_basis=""`) +실무 6건의 총공사비 끝 세 자리가 우리 엔진에서만 남음. **기본을 켜되 화면에 근거를 보일 것** — +2026-09-08 「조용히 깎지 않음」 합의는 근거가 없을 때 정한 것이고, 지금은 고시가 근거임. +곁에 버그 하나 — 절사 자동보정이 둘째 걸음부터 ÷1.1 을 안 해 경계를 지나쳐 **진동**함 +(울진 신설 `654→999→900→910→909…`). 다섯 건은 첫 걸음에 앉아 안 드러났음. + +⚠ 「설계서의 소계 1원 미만버림」과 우리 「집계표 행 금액 `ROUND`(279/279)」는 **다른 자리**로 봄 — +집계표는 설계서 소계가 아님. 같은 자리로 읽을 근거가 나오면 다시 잴 것. + **같은 고시 1-2-2 「1. 수량의 단위표준」 [주]**: ``` From d5bdd2ee17c401122427a53689b0b171c08d0f37 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 09:02:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs(b09):=20=EC=B6=95C=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=EC=97=90=20=ED=92=88=EC=85=88=20=ED=8C=90=20=EA=B7=BC?= =?UTF-8?q?=EA=B1=B0=EC=99=80=20=EA=B8=88=EC=95=A1=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=ED=91=9C=EC=A4=80=20=EC=A0=88=20=EB=B3=B4=ED=83=AC=20=E2=80=94?= =?UTF-8?q?=20=EC=82=B0=EB=A6=BC=EC=B2=AD=EA=B3=A0=EC=8B=9C=202025-82?= =?UTF-8?q?=ED=98=B8?= 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_01Prk9BCHG1EMAywk9k8wegA --- docs/raw/verification/2026-09-13_축C_명세.md | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/raw/verification/2026-09-13_축C_명세.md b/docs/raw/verification/2026-09-13_축C_명세.md index 2b3a1862..a9e4d7bd 100644 --- a/docs/raw/verification/2026-09-13_축C_명세.md +++ b/docs/raw/verification/2026-09-13_축C_명세.md @@ -90,6 +90,24 @@ FP-<절>-<절>-<절> 예) FP-04-02-02 (벌목, 5m 미만) - 정본은 **품셈 절 번호**. 표의 행은 그 절의 **조건별 변형**(`variant` 칸)임 - **이름은 표시용, 잇기는 코드로.** 이름으로 맞추는 코드가 남아 있으면 그것이 곧 결함임 + +### ⭐ 어느 품셈 판인가 — 2026-09-14 확인 (조문 근거 있음) + +**`FP-` 는 산림사업 표준품셈 한 판임.** 건설공사 표준품셈은 **공종 축이 아니라 기계 층에만** 들어옴. + +``` +공종 축 477/477 FP- · 원천 pum_forest_2026.json (산림청고시 2025-82호, 2026-01-01) +기계 층 pum_const_2026.json · mach_base_2026.json (건설품셈 제8장 — 취득가·손료계수·연료·운전원) +빌려 온 몇 곳 「교차 참조」 딱지를 붙임 (예: 뿌리뽑기 밑수 3-9-2) +``` + +**근거 — 같은 고시 총칙 7**: 「본 품셈에 명시되지 않은 품은 타 부문(토목, 건축, 전기, 기계 등)의 +표준품셈에 명시된 부분의 품을 적용하고, **타 부문과 유사한 공종의 품은 본 품셈을 우선하여 적용**한다.」 + +⇒ 지금 얼개가 이 조문 그대로임. **건설품셈 절번호로 선 공종 줄은 0 건**(소스에 「8-2-8」 참조 0건). +⚠ 자료를 찾을 때 건설품셈만 보면 「값이 없다」고 잘못 결론 내기 쉬움 — 덤프 운반이 그랬음 +(건설 8-2-8 에는 단위중량·적재용량 표가 없으나 **산림 10-12 [주]에 토사 1.9 · 암 2.4 ton/㎥ · 덤프 15 ton 이 있었음**). +**막히면 산림품셈 쪽을 먼저 볼 것.** - 부모·잎이 있음. ⚠ **「부모에 수량 금지」는 반쪽만 맞았음** — 2026-09-13 정정. **부모가 두 모양이고 규칙이 다름**(랩탑 메인 실측). @@ -334,6 +352,40 @@ B08 인계본 `ground_class_aliases` — **B09 가 안 읽음**)을 한 벌로 | **단가산출 성분단가** | 소수 1자리 절사 | 211건 중 137건 설명 | | 내부 계산 | 은행가(최근접-짝수) | STmate 내부. ⚠ 뽑은 엑셀은 사사오입이라 20개 경계 중 10개가 갈림 | +### ⭐⭐ 2026-09-14 — 위 실측이 **고시 조문과 그대로 맞았음** + +**산림청고시 제2025-82호 「산림사업 표준품셈」 1-2-2 「2. 금액의 단위표준」**: + +| 종목 | 단위 | 지위 | 비고 | 우리 실측과 | +| --- | --- | --- | --- | --- | +| 설계서의 **총액** | 원 | **1,000** | 미만버림 | ⚠ **우리는 안 버리고 있었음** — 실무 6건은 여섯 다 `000`, 우리는 `373·695·578·654·341·639`. 기본을 켜는 것으로 판정(2026-09-14) | +| 설계서의 소계 | 원 | 1 | 미만버림 | ✅ | +| 설계서의 **금액란** | 원 | 1 | 미만버림 | ✅ 「내역에 실릴 때 정수 절사」와 같음 | +| **일위대가표의 계금** | 원 | 1 | 미만버림 | ✅ 「호표 안 성분 소계 절사」와 같음 | +| **일위대가표의 금액란** | 원 | **0.1** | 미만버림 | ✅ 「단가산출 성분단가 소수 1자리 절사」와 같음 | + +⇒ **STmate 를 재서 얻은 규칙이 법 조문이었음.** 경쟁사를 베낀 것이 아니라 둘 다 같은 고시를 따른 것임. + +⚠ **총액 줄만 우리가 안 따르고 있었음** (2026-09-14 데스크탑 서브 실측). 기본값이 꺼져 있어(`cut_basis=""`) +실무 6건의 총공사비 끝 세 자리가 우리 엔진에서만 남음. **기본을 켜되 화면에 근거를 보일 것** — +2026-09-08 「조용히 깎지 않음」 합의는 근거가 없을 때 정한 것이고, 지금은 고시가 근거임. +곁에 버그 하나 — 절사 자동보정이 둘째 걸음부터 ÷1.1 을 안 해 경계를 지나쳐 **진동**함 +(울진 신설 `654→999→900→910→909…`). 다섯 건은 첫 걸음에 앉아 안 드러났음. + +⚠ 「설계서의 소계 1원 미만버림」과 우리 「집계표 행 금액 `ROUND`(279/279)」는 **다른 자리**로 봄 — +집계표는 설계서 소계가 아님. 같은 자리로 읽을 근거가 나오면 다시 잴 것. + +**같은 고시 1-2-2 「1. 수량의 단위표준」 [주]**: + +``` +① 설계서 수량의 단위와 소수자리 표시는 본 표에 따르며, 반올림하여 적용 +② 품목이 제시한 소숫자리가 본 표와 다르면 항목 쪽을 우선 +③ 본 표에 없는 품은 유사품의 규격·단위수량을 참고, C.G.S 단위 원칙 +``` + +⇒ 아래 「규정이 없는 자리 = 소수 2자리」는 **이 표에도 없는 자리에만** 적용할 것. +표에 실린 품목(관류 길이 m 소수 2 · 수로연장 m 소수 1 · 옹벽 ㎡ 소수 1 …)은 **표가 규정임.** + ### ⭐ 규정이 없는 자리 — 소수점 2자리가 기본값 (2026-09-13 사용자 확정) **단수처리에 법적 근거가 없음이 확인됐음** — 기재부 **예정가격작성기준 전 조문에 From 8eb1189b4fc01c5da9e227d48c68d801fb26d2a0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 09:10:47 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(b05,b06,b08):=20=EA=B8=B0=EC=8A=AD?= =?UTF-8?q?=EB=A7=89=EC=9D=B4=20=EB=B2=BD=20=EB=86=92=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=EA=B4=80=EA=B2=BD=20=EA=B8=B0=EC=A4=80=20=ED=95=9C=20=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=C2=B7=20=ED=8F=BC=EC=9D=80=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=EC=9D=84=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=95=88=20=EB=B3=B4=EB=83=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한 벽을 두고 값이 셋(등록부·표 H2.5 · 구조물도 H2.5 · 횡단도 관경 기준)이던 결함 — 근거 있는 관경 기준으로 맞춤 - 관 벽 높이 안 적으면 관경 + 여유 0.5 를 0.1m 로 올리고 근입 0.5 더함(Ø800 1.8 · Ø1000 2.0 · Ø1500 2.5) - 파이썬(B06 엔진)·TS(세트 스펙·횡단 상수) 거울 시험 · 등록부 기본 2.5 걷고 empty_means - 최소값이라 미확정·금액 밖은 그대로 - B05 벽 칸(관 유입·유출 · 독립 기슭막이)은 비워 두고 회색 제안값 · [제안값 넣기] 누른 때만 값 — 저장 한 번에 미확정이 확정으로 바뀌던 길을 막음 - 전체 시험: 받은 덤프 적재 커밋 몫 2개만 빨강(보고함) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 10 +-- .../B05_Profile_UI_Drainage_Facility.ts | 7 ++- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 63 +++++++++++++------ B06_Section/B06_Section_Engine_Culvert.py | 34 ++++++++-- B08_Quantity/B08_Quantity_Engine_Pipe.py | 39 +++++++++--- common_util/common_util_culvert_sets.ts | 22 +++++-- .../test_b05_structures_registry_policy.py | 6 +- resources/tester/test_b06_culvert_set.py | 14 +++-- .../test_b06_pipe_wall_height_mirror.py | 43 +++++++++++++ .../tester/test_b08_facility_structures.py | 4 +- 10 files changed, 192 insertions(+), 50 deletions(-) create mode 100644 resources/tester/test_b06_pipe_wall_height_mirror.py diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 6362f9c3..9d7772ef 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -134,9 +134,10 @@ "label": "유입 기슭막이 높이", "input": "number", "unit": "m", - "default": 2.5, + "default": null, "required": false, - "phase": "detail" + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" }, { "key": "outlet_type", @@ -186,9 +187,10 @@ "label": "유출 기슭막이 높이", "input": "number", "unit": "m", - "default": 2.5, + "default": null, "required": false, - "phase": "detail" + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" }, { "key": "revet_foundation", diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 409ed2f1..bd9acc19 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -59,9 +59,9 @@ import { WING_IN_KEYS, WING_OUT_KEYS, } from "./B05_Profile_UI_Drainage_Facility_Fields"; +import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets"; -// 유입구 형식 목록은 700줄 한계로 `_Fields` 로 옮김(2026-09-14) — 부르던 곳은 그대로 여기서. -export type { InletStructureKind }; +export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로 /** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을 * 가지면 저장한다. */ @@ -235,9 +235,11 @@ export function createFacilityOptionsForm( const basinRows = [ grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))), ]; + const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value) / 1000)); const inletRevet = createRevetmentFields(INLET_REVET_KEYS, { ...REVET_COMMON_DEFAULTS, form: "돌쌓기(찰)", + height, }); // 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다 // (2026-08-17 사용자 지시 3). @@ -260,6 +262,7 @@ export function createFacilityOptionsForm( const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, { ...REVET_COMMON_DEFAULTS, form: "돌쌓기(메)", + height, }); outletGroup.body.append( grid(labeled("구조", outletType), outletRevet.formField), diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 1b3eb9ce..36d3a3a6 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -201,16 +201,20 @@ export interface RevetmentKeys { after?: string; } -/** 부위별 기본 구성 — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m·높이 2.5m. */ +/** 부위별 **제안값** — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m. 관 벽 높이는 관경 기준 + * (함수로 받아 볼 때마다 셈). + * ⚠ 제안값은 **칸에 채우지 않고 회색 글씨로만** 보임 — 사용자가 적거나 [제안값 넣기]를 누른 때만 + * 값이 됨(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」). 칸에 채워 두면 저장이 + * 그 값을 적어 **미확정이 저장 한 번에 확정으로 바뀜** — 사용자가 한 일이 없는데 금액이 섬. */ export interface RevetmentDefaults { form: string; length: string; - height: string; + height: string | (() => string); before: string; after: string; } -/** 부위와 무관한 공통 기본 치수 — 길이 10m(전 5·후 5)·높이 2.5m. */ +/** 부위와 무관한 공통 제안 치수 — 길이 10m(전 5·후 5)·높이 2.5m(독립 기슭막이 · 관 벽은 관경 기준). */ export const REVET_COMMON_DEFAULTS = { length: "10", height: "2.5", @@ -284,26 +288,30 @@ export function createRevetmentFields( keys: RevetmentKeys, defaults: RevetmentDefaults, ): RevetmentFields { + // ⚠ 제안값은 **회색 글씨(placeholder)로만** — 칸은 비워 둠(`RevetmentDefaults` 주석). + const suggestHeight = (): string => + typeof defaults.height === "function" ? defaults.height() : defaults.height; const form = document.createElement("select"); - form.replaceChildren(...REVET_FORMS.map((value) => new Option(value, value))); - form.value = defaults.form; - const length = keys.length ? numberInput("0.1") : null; - if (length) length.value = defaults.length; - const height = numberInput("0.1"); - height.value = defaults.height; + form.replaceChildren( + new Option(`제안 ${defaults.form}`, ""), + ...REVET_FORMS.map((value) => new Option(value, value)), + ); + const length = keys.length ? numberInput("0.1", "0", `제안 ${defaults.length}`) : null; + const height = numberInput("0.1", "0", `제안 ${suggestHeight()}`); + // 관 벽 높이 제안은 관경을 따라감 — 칸을 볼 때마다 다시 셈. + height.addEventListener("focus", () => (height.placeholder = `제안 ${suggestHeight()}`)); // 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동: // 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다. - const before = keys.before && length ? numberInput("0.1") : null; - const after = keys.after && length ? numberInput("0.1") : null; - if (before) before.value = defaults.before; - if (after) after.value = defaults.after; + const before = keys.before && length ? numberInput("0.1", "0", `제안 ${defaults.before}`) : null; + const after = keys.after && length ? numberInput("0.1", "0", `제안 ${defaults.after}`) : null; let lastSplitEdit: "before" | "after" = "before"; const readNumber = (input: HTMLInputElement | null): number => { const value = input ? Number.parseFloat(input.value) : Number.NaN; return Number.isFinite(value) && value >= 0 ? value : 0; }; const syncSplit = (source: "length" | "before" | "after"): void => { - if (!length || !before || !after) return; + // 길이가 비었으면 전/후를 0 으로 채우지 않음 — 빈 칸이 「0 으로 적음」이 되면 안 됨. + if (!length || !before || !after || length.value === "") return; if (source !== "length") lastSplitEdit = source; const total = readNumber(length); const keep = lastSplitEdit === "after" ? after : before; @@ -319,10 +327,25 @@ export function createRevetmentFields( const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null; const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null; + // [제안값 넣기] — **누른 때만** 빈 칸에 제안값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양. + const fill = document.createElement("button"); + fill.type = "button"; + fill.className = "b05-structure__suggest"; + fill.textContent = "제안값 넣기"; + fill.addEventListener("click", () => { + if (!form.value) form.value = defaults.form; + if (length && !length.value) length.value = defaults.length; + if (!height.value) height.value = suggestHeight(); + if (before && !before.value) before.value = defaults.before; + if (after && !after.value) after.value = defaults.after; + form.dispatchEvent(new Event("change")); + }); + // 형태는 그룹 첫 행(구조 옆)에 따로 놓이므로 여기 행에서는 뺀다. const rows = [ grid(...[lengthField, heightField].filter((item): item is HTMLLabelElement => item !== null)), ...(beforeField && afterField ? [grid(beforeField, afterField)] : []), + grid(fill), ]; return { @@ -330,13 +353,15 @@ export function createRevetmentFields( rows, lengthInput: length, write(options) { + // 저장된 값만 칸에 — 없으면 비워 두고 제안값은 회색 글씨로(몰래 채우지 않음). const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; - form.value = text(keys.form) || defaults.form; - if (length && keys.length) length.value = text(keys.length) || defaults.length; - height.value = text(keys.height) || defaults.height; - if (before && keys.before) before.value = text(keys.before) || defaults.before; - if (after && keys.after) after.value = text(keys.after) || defaults.after; + form.value = text(keys.form); + if (length && keys.length) length.value = text(keys.length); + height.value = text(keys.height); + height.placeholder = `제안 ${suggestHeight()}`; + if (before && keys.before) before.value = text(keys.before); + if (after && keys.after) after.value = text(keys.after); syncSplit("length"); }, read(target) { diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 17f00ca6..f1a8bcd4 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -74,6 +74,26 @@ APRON_THICKNESS_M = 0.45 # 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다. REVET_FACE_SLOPE = 0.3 +# ── 관 벽 최소 높이 — ⚠ TS 짝: `B06_Section_UI_Cross_Culvert_Const.ts` +# (`REVET_FREEBOARD_M`·`REVET_HEIGHT_STEP_M`·`REVET_EMBED_DEPTH_M`·`pipeWallMinPureHeight`). +# 횡단도가 관 벽을 그리는 그 높이 — 구조물 표·구조물도 그림도 이 값을 씀(2026-09-14 브레인 판정 +# 「그림 둘이 같은 벽을 다르게 그리면 결함 · 근거 있는 관경 기준으로 맞춤」). 거울 시험 +# `test_b06_pipe_wall_height_mirror`. +REVET_FREEBOARD_M = 0.5 # 관 상단 ~ 벽 상단 여유고(잠정 · 토피 0.5 와 같은 값) +REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금 +REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이 + + +def pipe_wall_min_height_m(diameter_m: float) -> float: + """관 벽 **순수 높이**(근입 포함) 최소값 — 관경 + 여유고를 0.1m 로 올린 뒤 근입을 더함. + + ⚠ 「최소」다 — 실제 설계값은 더 클 수 있어 이 값으로 선 수량은 미확정으로 둠(금액 밖). + """ + raw = float(diameter_m) + REVET_FREEBOARD_M + steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) + return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3) + + # (2026-08-20 사용자 확정) 배관은 **수평도 가능** — 그림에서 경사를 강제로 만들지 # 않는다. config `DRAINAGE_PIPE_SLOPE_DEG`(10도)는 유효직경 수리계산 전용으로 남는다. @@ -139,7 +159,9 @@ def _number(value: Any, fallback: float | None) -> float | None: return fallback -def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]: +def _side_spec( + options: dict[str, Any], defaults: dict[str, Any], side: str, diameter_m: float = 1.0 +) -> dict[str, Any]: """유입("inlet")·유출("outlet") 한쪽의 부속 제원. 구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 집수정 단면은 후속 작업이라 @@ -164,9 +186,10 @@ def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> ) return spec + # 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음). height = _number( options.get(f"{side}_revet_height_m"), - _number(defaults.get(f"{side}_revet_height_m"), None), + _number(defaults.get(f"{side}_revet_height_m"), pipe_wall_min_height_m(diameter_m)), ) length = _number( options.get(f"{side}_revet_length_m"), @@ -208,10 +231,11 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: values.get("pipe_diameter_mm"), _number(defaults.get("pipe_diameter_mm"), 1000.0) ) kind = values.get("pipe_kind") or defaults.get("pipe_kind") + diameter_m = round((diameter_mm or 1000.0) / 1000.0, 3) return { "type": "pipe", "pipe_kind": str(kind) if kind else None, - "diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3), + "diameter_m": diameter_m, "min_cover_m": MIN_PIPE_COVER_M, # 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다(2026-09-09). 한쪽만 기초유로 # 하는 일이 드물어 폼을 둘로 늘리지 않았다. 실무가 갈라 쓰면 그때 나눌 것. @@ -219,8 +243,8 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: str(values.get("revet_foundation") or defaults.get("revet_foundation") or "").strip() or None ), - "inlet": _side_spec(values, defaults, "inlet"), - "outlet": _side_spec(values, defaults, "outlet"), + "inlet": _side_spec(values, defaults, "inlet", diameter_m), + "outlet": _side_spec(values, defaults, "outlet", diameter_m), } diff --git a/B08_Quantity/B08_Quantity_Engine_Pipe.py b/B08_Quantity/B08_Quantity_Engine_Pipe.py index 4c4e1722..ddbde4aa 100644 --- a/B08_Quantity/B08_Quantity_Engine_Pipe.py +++ b/B08_Quantity/B08_Quantity_Engine_Pipe.py @@ -208,12 +208,15 @@ REVET_ROLES = ( ("inlet", "유입부 기슭막이", "유입 칸 벽"), ("outlet", "유출부 기슭막이", "유출 칸 벽"), ) -#: ⚠ 「횡단도와 같은 값」이 아님 — B06 은 관 벽 높이를 **관경 기준 최소 높이**로 따로 그림 -#: (`revetWallSpec`), 독립 기슭막이도 저장 높이가 없으면 근입 깊이로 그림. 형태·길이만 같은 기본값. +#: 관 벽 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(B06 `pipe_wall_min_height_m`, +#: TS 짝 `pipeWallMinPureHeight`)로 섬 — 등록부·구조물도·횡단도가 한 값(2026-09-14 브레인 판정). +#: ⚠ 「최소」라 실제 설계값이 더 클 수 있어 **미확정·금액 밖은 그대로**. NOTE_DEFAULT_WALL = ( - "⚠ 시설 지점에 {filled} 을 안 적어 등록부 기본값으로 섰음 — 미확정이라 금액에 안 들어감" - " · 횡단도 벽 높이는 관경 기준으로 따로 그려져 다를 수 있음" + "⚠ 시설 지점에 {filled} 을 안 적어 기본값으로 섰음 — 미확정이라 금액에 안 들어감" ) +#: 꼬리 — 관 벽은 횡단도와 같은 높이 · 독립 기슭막이는 아직 갈림(관경이 없음). +NOTE_WALL_HEIGHT_PIPE = " · 높이는 횡단도와 같은 관경 기준 최소 높이(실제 설계값은 더 클 수 있음)" +NOTE_WALL_HEIGHT_OWN = " · ⚠ 횡단도는 저장 높이가 없으면 근입 0.5m 만 그림 — 표 높이와 다름" #: 금액에서 뺄 까닭(2026-09-14 브레인 판정) — 줄·표·그림은 서되 내역 합에는 안 듦. UNCONFIRMED_WALL = "벽 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬" NOTE_SIDE_UNKNOWN = ( @@ -234,9 +237,13 @@ def _num_or_zero(value: Any) -> float: def _revet_values( - options: dict[str, Any], role: str, defaults: dict[str, Any], legacy: bool + options: dict[str, Any], + role: str, + defaults: dict[str, Any], + legacy: bool, + min_height_m: float | None = None, ) -> tuple[dict[str, Any], list[str]]: - """벽 한쪽 제원과 등록부 기본값으로 채운 칸 — B06 세트 스펙(`_side_spec`)과 같은 채움.""" + """벽 한쪽 제원과 기본값으로 채운 칸 — 등록부 기본값 · 관 벽 높이는 관경 기준 최소 높이.""" values: dict[str, Any] = {} filled: list[str] = [] for name, label in REVET_FIELDS: @@ -245,12 +252,23 @@ def _revet_values( value = options.get(name) if _blank(value): value = defaults.get(name if legacy else f"{role}_revet_{name}") + if value is None and name == "height_m" and min_height_m is not None: + value = min_height_m + label = f"{label}(관경 기준)" if label and value is not None: filled.append(f"{label} {value}") values[name] = value return values, filled +def _pipe_diameter_m(options: dict[str, Any], defaults: dict[str, Any]) -> float: + """관경(m) — 저장값, 없으면 등록부 기본(B06 세트 스펙과 같은 채움).""" + raw = options.get("pipe_diameter_mm") + if _blank(raw): + raw = defaults.get("pipe_diameter_mm") + return _num_or_zero(raw) / 1000.0 or 1.0 + + def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: """계곡 통과 시설(`pipe_points.json`) → 원단위 전개가 읽는 구조물 줄 (A1, 2026-09-14). @@ -260,6 +278,7 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: ⚠ 집수정·기슭막이 터파기·되메우기는 전개 성분(`destination: earthwork`)이라 토공집계로만 감. """ from B05_Profile.B05_Profile_Structures_Schema import structure_type_map + from B06_Section.B06_Section_Engine_Culvert import pipe_wall_min_height_m from B08_Quantity.B08_Quantity_Engine_StructureSummary import pipe_row_id from B08_Quantity.B08_Quantity_Engine_UnitQuantity import attachments_of, child_row @@ -304,11 +323,15 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: ) rows.extend(children) walls = [] + min_height = ( + None if legacy else pipe_wall_min_height_m(_pipe_diameter_m(options, wall_defaults)) + ) for role, pipe_label, own_label in roles: - values, filled = _revet_values(options, role, wall_defaults, legacy) + values, filled = _revet_values(options, role, wall_defaults, legacy, min_height) notes = list(inlet_notes) if role == "inlet" else [] if filled: - notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled))) + tail = NOTE_WALL_HEIGHT_OWN if legacy else NOTE_WALL_HEIGHT_PIPE + notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled)) + tail) walls.append([role, own_label if legacy else pipe_label, values, notes, False, filled]) side = str(options.get("side") or "") if legacy else "" if side in ("좌", "우"): diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 83a726e9..9144fa4d 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -46,6 +46,18 @@ export const FORD_WALL_THICKNESS_M = 0.2; export const FORD_DEFAULT_WIDTH_M = 10.0; export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0; export const BOX_COVER_M = 0.5; +/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조). + * 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */ +export const REVET_FREEBOARD_M = 0.5; +export const REVET_HEIGHT_STEP_M = 0.1; +export const REVET_EMBED_DEPTH_M = 0.5; + +/** 관 벽 순수 높이(근입 포함) 최소값 — 파이썬 `pipe_wall_min_height_m`. */ +export function pipeWallMinHeightM(diameterM: number): number { + const raw = diameterM + REVET_FREEBOARD_M; + const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9); + return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3); +} /** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */ const FACILITY_PIPE = "pipe"; @@ -82,6 +94,7 @@ function sideSpec( options: Record, defaults: Record, side: string, + diameterM: number, ): CulvertSetSpec { const structure = text(options[`${side}_type`]) ?? text(defaults[`${side}_type`]) ?? "기슭막이"; const spec: CulvertSetSpec = { role: side, structure }; @@ -100,7 +113,7 @@ function sideSpec( const height = num( options[`${side}_revet_height_m`], - num(defaults[`${side}_revet_height_m`], null), + num(defaults[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)), ); const length = num( options[`${side}_revet_length_m`], @@ -133,17 +146,18 @@ function culvertSet( ): CulvertSetSpec { const defaults = registry.pipe ?? {}; const diameterMm = num(options.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0)); + const diameterM = pipeDiameterM(diameterMm); return { type: "pipe", pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind), - diameter_m: pipeDiameterM(diameterMm), + diameter_m: diameterM, min_cover_m: MIN_PIPE_COVER_M, // 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로 // 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이 // 확인되면 그때 나눌 것. foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation), - inlet: sideSpec(options, defaults, "inlet"), - outlet: sideSpec(options, defaults, "outlet"), + inlet: sideSpec(options, defaults, "inlet", diameterM), + outlet: sideSpec(options, defaults, "outlet", diameterM), }; } diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index 0f5c2879..7b5ff97e 100644 --- a/resources/tester/test_b05_structures_registry_policy.py +++ b/resources/tester/test_b05_structures_registry_policy.py @@ -451,10 +451,12 @@ def test_pipe_accessory_options_defined(): "outlet_pitching_area_m2", ): assert gone not in options, f"pipe.{gone}는 보호공으로 합쳐졌다" - # 기본 치수 — 길이 10m·높이 2.5m. + # 기본 치수 — 길이 10m. 높이는 **기본값 없음**(2026-09-14 브레인 판정): 비우면 관경 기준 최소 + # 높이로 횡단도·구조물도·표가 같은 값을 씀(옛 2.5 는 횡단도가 안 쓰던 근거 없는 값이라 걷음). for side in ("inlet", "outlet"): assert options[f"{side}_revet_length_m"].default == 10 - assert options[f"{side}_revet_height_m"].default == 2.5 + assert options[f"{side}_revet_height_m"].default is None + assert "관경 기준" in options[f"{side}_revet_height_m"].empty_means # 유무·길이·관경·유입 구조 = b05, 형태·치수 제원 = detail. for key in ("inlet_type", "inlet_revet_length_m", "outlet_revet_length_m"): assert options[key].phase == "b05", f"pipe.{key}는 b05 phase" diff --git a/resources/tester/test_b06_culvert_set.py b/resources/tester/test_b06_culvert_set.py index d5bf7c25..4a35b241 100644 --- a/resources/tester/test_b06_culvert_set.py +++ b/resources/tester/test_b06_culvert_set.py @@ -34,7 +34,11 @@ def test_cross_reference_constants_match_plan(): def test_empty_options_fall_back_to_registry_defaults(): - """옵션이 빈 저장분은 레지스트리 기본값(Ø1000·기슭막이 H2.5/L10)으로 채운다.""" + """옵션이 빈 저장분은 레지스트리 기본값(Ø1000·L10)으로 채운다. + + 높이는 2026-09-14 브레인 판정으로 **관경 기준 최소 높이**(Ø1000 → 1.5 + 근입 0.5 = 2.0) — + 횡단도가 그리는 그 높이. 옛 기본 2.5 는 횡단도가 안 쓰던 값이라 걷음. + """ spec = _culvert_set(None) assert spec["type"] == "pipe" assert spec["diameter_m"] == 1.0 @@ -42,11 +46,11 @@ def test_empty_options_fall_back_to_registry_defaults(): for side in ("inlet", "outlet"): part = spec[side] assert part["structure"] == "기슭막이" - assert part["revet_height_m"] == 2.5 + assert part["revet_height_m"] == 2.0 assert part["revet_length_m"] == 10 assert part["face_slope"] == REVET_FACE_SLOPE - # 보호공 = 낙차고(2.5) × 2 = 5.0m, 두께 0.45m(사용자 확정). - assert part["apron_length_m"] == 5.0 + # 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정). + assert part["apron_length_m"] == 4.0 assert part["apron_thickness_m"] == 0.45 # 레지스트리 기본 형태: 유입 찰 / 유출 메. assert spec["inlet"]["revet_form"] == "돌쌓기(찰)" @@ -61,7 +65,7 @@ def test_basin_inlet_skips_revet_and_apron(): assert "apron_length_m" not in spec["inlet"] # 유출측은 그대로 세트를 갖춘다. assert spec["outlet"]["structure"] == "기슭막이" - assert spec["outlet"]["apron_length_m"] == 5.0 + assert spec["outlet"]["apron_length_m"] == 4.0 # 관경 기준 높이 2.0 × 2 def test_string_diameter_and_custom_height(): diff --git a/resources/tester/test_b06_pipe_wall_height_mirror.py b/resources/tester/test_b06_pipe_wall_height_mirror.py new file mode 100644 index 00000000..a41970e0 --- /dev/null +++ b/resources/tester/test_b06_pipe_wall_height_mirror.py @@ -0,0 +1,43 @@ +"""관 벽 최소 높이 거울 — 파이썬(`B06_Section_Engine_Culvert`) ↔ TS(`_Cross_Culvert_Const.ts`). + +⚠ 횡단도(TS)가 그리는 높이와 구조물 표·구조물도 그림(파이썬)이 같은 벽을 다르게 그리면 결함 + (2026-09-14 브레인 판정). 상수 셋이 같고 식이 같으면 같은 값. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B06_Section import B06_Section_Engine_Culvert as culvert # noqa: E402 + +TS = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts").read_text(encoding="utf-8") +#: 세트 스펙 TS 짝(`common_util_culvert_sets.ts`)도 같은 상수를 들고 있음 — 셋을 함께 대조. +SETS = (ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8") + + +def _ts_const(source: str, name: str) -> float: + return float(re.search(rf"export const {name} = ([\d.]+);", source).group(1)) + + +def test_상수_셋이_TS_와_같다() -> None: + for name in ("REVET_FREEBOARD_M", "REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"): + assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name + + +def test_TS_식_모양이_그대로다() -> None: + """식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것.""" + assert "const raw = diameterM + REVET_FREEBOARD_M;" in TS + assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS + assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS + + +def test_관경별_최소_높이() -> None: + assert culvert.pipe_wall_min_height_m(1.0) == 2.0 + assert culvert.pipe_wall_min_height_m(0.8) == 1.8 + assert culvert.pipe_wall_min_height_m(1.2) == 2.2 diff --git a/resources/tester/test_b08_facility_structures.py b/resources/tester/test_b08_facility_structures.py index 6ad5f803..dec80ba1 100644 --- a/resources/tester/test_b08_facility_structures.py +++ b/resources/tester/test_b08_facility_structures.py @@ -34,7 +34,9 @@ def test_관_하나에_기슭막이_둘이_기본값_사유와_함께_서고_관 inlet, outlet = table["structures"] assert inlet["options"]["form"] == "돌쌓기(찰)" and outlet["options"]["form"] == "돌쌓기(메)" assert all(s["components"] and s["length_m"] == 10 for s in table["structures"]) - assert "등록부 기본값으로 섰음" in inlet["notes"][0] + assert "기본값으로 섰음" in inlet["notes"][0] + # 높이는 횡단도와 같은 관경 기준 최소 높이(Ø1000 → 2.0) — 그림 둘·표가 한 값(브레인 판정) + assert inlet["height_m"] == outlet["height_m"] == 2.0 and "관경 기준" in inlet["notes"][0] # 관 줄은 관 연장만 — 기슭막이가 거기 섞이지 않음(두 번 안 셈). pipe = build_rows([point], [{"chainage_m": 100.0, "design": {"pipe_length_m": 8}}]) assert [row["quantity"] for row in pipe["rows"]] == [8.0]