From bfad293c66128257cf513ea8970a3205005d96e7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 16:22:53 +0900 Subject: [PATCH] =?UTF-8?q?fix(section):=20=EB=86=92=EC=9D=B4=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=20=ED=92=80=EA=B8=B0=20=E2=80=94=20=ED=9D=99=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=C2=B7=20=EC=A7=91=EC=88=98=EC=A0=95=20=C2=B7=20?= =?UTF-8?q?=EC=98=B9=EB=B2=BD=20=C2=B7=20=EA=B4=80=20=EB=B3=B4=ED=98=B8?= =?UTF-8?q?=EA=B3=B5=20=EB=86=92=EC=9D=B4=20=EC=9E=85=EB=A0=A5=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C(54-51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관 흙막이 관경 기준 최소(관경 + 1.0) · 벽 최저(근입 + 0.1) 하한 걷음 — 관경 기준값은 빈 칸 기본값만 - 하한은 그림이 서는 0.1 하나(WALL_MIN_PURE_HEIGHT_M) · 0 이하는 토스트 없이 0.1 에서 멈춤 - 좌측 칸 최소 높이 토스트 · 조정창 「최소 높이」 토스트 뺌 - 높이 · 지형이 벽 자리를 옮긴 것은 자리 한계 토스트 안 띄움 — 자리 버튼이 막혔을 때만 - 집수정 내공 폭 · 높이 1~2m 한도 걷음 · 옹벽 0 입력도 0.1 에서 멈춤 - 근입보다 낮은 관 벽도 그림(노출 높이 음수 허용) · 서버 번들 다시 빌드 - 시험 test_b06_height_unlock Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- .../B06_Section_UI_Cross_Culvert_Const.ts | 12 ++- .../B06_Section_UI_Cross_Culvert_Extra.ts | 8 +- .../B06_Section_UI_Cross_Culvert_Geom.ts | 4 +- .../B06_Section_UI_Cross_Culvert_Wire.ts | 23 ++++- B06_Section/B06_Section_UI_Cross_Revetment.ts | 9 +- .../B06_Section_UI_Cross_Structure_Panel.ts | 8 +- .../B06_Section_UI_Cross_View_Structure.ts | 8 +- B06_Section/B06_Section_UI_Cross_Wall.ts | 3 +- .../B06_Section_UI_Page_Pipe_Options.ts | 20 +--- .../B06_Section_UI_Page_Station_Controls.ts | 4 +- resources/tester/test_b06_height_unlock.py | 94 +++++++++++++++++++ 11 files changed, 149 insertions(+), 44 deletions(-) create mode 100644 resources/tester/test_b06_height_unlock.py diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts index ab51b6978..346b2a5dc 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts @@ -137,9 +137,11 @@ export function revetWallSpec( diameterM: number, ): { form: string; material: RevetMaterial; pureHeight: number; height: number } { const form = revetFormOf(adjust.m, spec.revet_form); - const floor = hiddenPipe ? REVET_EMBED_DEPTH_M : pipeWallMinPureHeight(diameterM); - const preferred = hiddenPipe ? (spec.revet_height_m ?? floor) : floor; - const pureHeight = Math.max(adjust.h ?? preferred, floor); + // 관경 기준 높이는 기본값만 — 하한은 그림이 서는 값뿐(54-51 · 사용자가 고름). + const preferred = hiddenPipe + ? (spec.revet_height_m ?? REVET_EMBED_DEPTH_M) + : pipeWallMinPureHeight(diameterM); + const pureHeight = Math.max(adjust.h ?? preferred, WALL_MIN_PURE_HEIGHT_M); return { form, material: materialFromForm(form), @@ -161,8 +163,8 @@ export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180); export function pipeWallMinPureHeight(diameterM: number): number { return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M; } -/** 일반(추가) 기슭막이 높이 조작 하한(m) — 프로젝트 기본값 0.5m. */ -export const EXTRA_WALL_MIN_HEIGHT_M = 0.5; +/** 벽 순수 높이 하한(m) — 0 이하는 그림이 안 섬. 관경 · 근입 하한 없음(54-51). */ +export const WALL_MIN_PURE_HEIGHT_M = 0.1; /** 일반(추가) 기슭막이 기본 높이(m). */ export const EXTRA_WALL_DEFAULT_HEIGHT_M = 1.5; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts index 5f29d0e79..e526ca91e 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts @@ -14,7 +14,6 @@ import { EXTRA_WALL_DEFAULT_HEIGHT_M, PIPE_CONNECT_GRADE, - EXTRA_WALL_MIN_HEIGHT_M, FILL_SLOPE_MAX_LENGTH_M, FILL_SLOPE_RATIO_MIN, materialFromForm, @@ -22,6 +21,7 @@ import { REVET_EMBED_DEPTH_M, REVET_LEAN_RATIO, REVET_THICKNESS_M, + WALL_MIN_PURE_HEIGHT_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { OffsetPoint, @@ -248,11 +248,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult const material = materialFromForm(form); // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함) — 배관 기준벽과 같은 기준. // 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠). - const height = Math.max( - adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M, - EXTRA_WALL_MIN_HEIGHT_M, - REVET_EMBED_DEPTH_M + 0.1, - ); + const height = Math.max(adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M, WALL_MIN_PURE_HEIGHT_M); /** 기준선(근입 위)~상단 — 도형·자리 계산은 이 값으로 한다. */ const exposed = height - REVET_EMBED_DEPTH_M; // 전면 기울기 — 형태·요청 높이로 표준경사(2026-09-14 B5). 자리 닫힌식의 분모도 따라감. diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index f97453203..f3e0a9a16 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -14,6 +14,7 @@ import { FILL_SLOPE_RATIO_MAX, FILL_SLOPE_RATIO_MIN, MIN_PIPE_COVER_M, + REVET_EMBED_DEPTH_M, REVET_LEAN_RATIO, REVET_THICKNESS_M, revetTargetHeight, @@ -324,7 +325,8 @@ export function computeCulvertLayout( // 형상 = 공용 `buildRevetWallGeometry`(추가·사면안정 흙막이·옹벽과 한 벌). 높이는 // vertical(계산용)이 들고 온다 — 형태별 높이 한계 없음, 윗면은 노면에서 멈춤. const height = vertical?.height ?? revetTargetHeight(diameter); - if (!(height > 0.05)) return null; + // 근입 포함 순수 높이가 서면 그림 — 관경 · 근입보다 낮아도 그대로(54-51). + if (!(height + REVET_EMBED_DEPTH_M > 0.05)) return null; const side = outward > 0 ? edges.left : edges.right; const wall = buildRevetWallGeometry({ anchor, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts index 3a07ae539..bed3e3864 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts @@ -154,6 +154,9 @@ export interface CulvertLink { * 키에 세워진 단 수를 넣어, 지형·조작이 바뀌어 결과가 달라지면 다시 알린다. */ const extraLimitNotified = new Set(); +/** 벽마다 직전에 적용된 자리 — 자리 조작이 한계에 막혀 벽이 그대로일 때만 알리려고 둠. */ +const lastApplied = new Map(); + /** 요청 단 수보다 적게 섰음을 알린다 — 조정창을 열지 않은 경로(좌측 폼 등)에서도 뜬다. * 종전에는 `activeRevet` 이 그 벽일 때만 떠서, 다른 길로 단 수를 바꾸면 조용히 잘렸다 * (2026-09-07). 문구는 종전 그대로 쓴다. */ @@ -539,9 +542,17 @@ export function computeCardCulvert( ]; for (const [role, applied] of roles) { const requested = revetOffset.adjustFor(section, role); + const memoKey = `${section.chainage_m.toFixed(2)}|${role}`; + const before = lastApplied.get(memoKey); + lastApplied.set(memoKey, applied); + // 높이 · 지형 때문에 자리가 따라 옮겨진 것은 알리지 않음 — 벽이 그대로일 때만(54-51). + const held = (axis: "x" | "d"): boolean => + before?.[axis] != null && + applied[axis] != null && + Math.abs(Number(before[axis]) - Number(applied[axis])) < 0.05; // 축별로 요청이 잘렸으면 왜 안 됐는지 토스트로 알린다(조작 중인 벽만). if (activeRevet === role) { - if (Math.abs(requested.x - applied.x) > 0.05) { + if (held("x") && Math.abs(requested.x - applied.x) > 0.05) { showToast( L( requested.x < applied.x @@ -551,15 +562,17 @@ export function computeCardCulvert( "info", ); } - if (requested.d != null && applied.d != null && Math.abs(requested.d - applied.d) > 0.05) { + if ( + held("d") && + requested.d != null && + applied.d != null && + Math.abs(requested.d - applied.d) > 0.05 + ) { showToast( L(requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down"), "info", ); } - if (requested.h != null && applied.h != null && requested.h < applied.h - 0.05) { - showToast(L("B06_Cross_Height_Floor"), "info"); - } } // d = null(기본 자리)은 null로 되돌려 담는다 — 적용 좌표를 그대로 저장하면 // 자동 자리가 "사용자 지정"으로 굳어 지형·높이 변경을 따라가지 못한다. diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index f64c64493..64191e0f7 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -23,6 +23,7 @@ import { PIPE_WALL_DEFAULT_RUN_M, REVET_EMBED_DEPTH_M, REVET_THICKNESS_M, + WALL_MIN_PURE_HEIGHT_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; @@ -159,8 +160,10 @@ export function computeRevetmentLayout( const spec = section.revetment; const design = section.design; if (!spec || !design) return null; - const requestedHeight = Number(adjust?.h ?? spec.height_m); - if (!Number.isFinite(requestedHeight) || requestedHeight <= 0) return null; + const rawHeight = adjust?.h ?? spec.height_m; + // 빈 칸만 안 그림 — 0 이하는 아래 하한(0.1)에서 멈춤(54-51). + if (rawHeight == null || !Number.isFinite(Number(rawHeight))) return null; + const requestedHeight = Number(rawHeight); // 설치 측이 비어 있으면 **성토가 나는 쪽**에 세운다 — 좌측 「구조물 배치」의 C군 벽 // (옹벽·돌쌓기 등)에는 설치 측 칸이 없기 때문이다(2026-09-06). 양측 절토면 벽이 설 @@ -181,7 +184,7 @@ export function computeRevetmentLayout( // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준). // 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠). - const pureHeight = Math.max(requestedHeight, REVET_EMBED_DEPTH_M + 0.1); + const pureHeight = Math.max(requestedHeight, WALL_MIN_PURE_HEIGHT_M); /** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */ const height = pureHeight - REVET_EMBED_DEPTH_M; diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index ee3683e27..c46b44c28 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -303,12 +303,12 @@ export function buildStructurePanel( parts.row.classList.add("b06-structure-panel__struct--basin-size"); const readout = document.createElement("span"); readout.className = "b06-structure-panel__hval"; - // 한계 1~2m 는 옛 9키 그대로. + // 한계 없음 — 그림이 안 서는 0 이하만 한 눈금에서 멈춤(54-51). const step = (delta: number) => basinPatch(() => ({ - [field]: Math.min( - 2, - Math.max(1, Math.round((deps.basinAdjustFor()[field] + delta) * 10) / 10), + [field]: Math.max( + BASIN_STEP_M, + Math.round((deps.basinAdjustFor()[field] + delta) * 10) / 10, ), })); parts.controls.append( diff --git a/B06_Section/B06_Section_UI_Cross_View_Structure.ts b/B06_Section/B06_Section_UI_Cross_View_Structure.ts index d1bb26b67..c65676d57 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Structure.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Structure.ts @@ -7,7 +7,11 @@ import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; -import { REVET_EMBED_DEPTH_M, REVET_FORM_DEFAULT } from "./B06_Section_UI_Cross_Culvert_Const"; +import { + REVET_EMBED_DEPTH_M, + REVET_FORM_DEFAULT, + WALL_MIN_PURE_HEIGHT_M, +} from "./B06_Section_UI_Cross_Culvert_Const"; import type { CulvertLayout } from "./B06_Section_UI_Cross_Culvert_Types"; import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types"; import type { StructurePanelDeps } from "./B06_Section_UI_Cross_Structure_Panel"; @@ -95,7 +99,7 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe const current = ctx.adjustOf(key); const base = current.h ?? ctx.heightOfWall(key); ctx.revetOffset?.update(ctx.adjustChainage(), key, { - h: Math.round((base + deltaM) * 10) / 10, + h: Math.max(WALL_MIN_PURE_HEIGHT_M, Math.round((base + deltaM) * 10) / 10), }); }, formFor: (key) => ctx.formOfWall(key), diff --git a/B06_Section/B06_Section_UI_Cross_Wall.ts b/B06_Section/B06_Section_UI_Cross_Wall.ts index b087ed153..5c7670e0b 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall.ts @@ -14,6 +14,7 @@ import { REVET_EMBED_DEPTH_M, REVET_LEAN_RATIO, REVET_THICKNESS_M, + WALL_MIN_PURE_HEIGHT_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; @@ -70,7 +71,7 @@ export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayou const road = input.roadElevation; const height = road != null && anchor.elevation + input.height > road + 1e-9 - ? Math.max(road - anchor.elevation, 0) + ? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M) : input.height; const thickness = input.thickness ?? REVET_THICKNESS_M; const lean = input.lean ?? REVET_LEAN_RATIO; diff --git a/B06_Section/B06_Section_UI_Page_Pipe_Options.ts b/B06_Section/B06_Section_UI_Page_Pipe_Options.ts index 3ee855477..d443de3c4 100644 --- a/B06_Section/B06_Section_UI_Page_Pipe_Options.ts +++ b/B06_Section/B06_Section_UI_Page_Pipe_Options.ts @@ -7,16 +7,14 @@ * ========================================================================== */ import { pipeDiameterM } from "@util/common_util_culvert_sets"; -import { showToast } from "@ui/ui_template_elements"; import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { applyBoxFormOptions, applyFordFormOptions } from "./B06_Section_UI_Page_Ford_Controls"; import { applySpanPatch, - EXTRA_WALL_MIN_HEIGHT_M, - REVET_EMBED_DEPTH_M, revetWallSpec, + WALL_MIN_PURE_HEIGHT_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { createStationControls } from "./B06_Section_UI_Page_Station_Controls"; @@ -119,7 +117,7 @@ export function applyPipeOptionsToCache( h: height ?? blank, m: typeof form === "string" ? form : blank, }); - // 높이 하한(관경 기준 최소)에 걸린 실제 높이를 폼에 되돌린다 — 형태별 상한은 없음. + // 그림이 안 서는 값(0 이하)만 하한에서 멈춰 폼에 되돌린다 — 토스트 없음(54-51). if (height === undefined) continue; const applied = revetWallSpec( role === "outlet" ? culvert.outlet : culvert.inlet, @@ -131,11 +129,6 @@ export function applyPipeOptionsToCache( ctx.overrideOptions(owner.chainage_m, { [`${prefix}_revet_height_m`]: Number(applied.toFixed(1)), }); - showToast( - `${prefix === "outlet" ? "유출" : "유입"} 기슭막이 높이는 최소 ` + - `${applied.toFixed(1)}m로 적용됩니다.`, - "error", - ); } } if (!culvert.hidden_pipe) applySoilTiers(ctx, owner, patch, whole); @@ -167,7 +160,7 @@ const soilSent = new Map(); * 흙막이 n 컨테이너(`soil_add_{n}_*`) → 유출 다단 n 단(`extra{n-1}`) 그림(54-10). 형태 · 높이 = 조정값 * (m · h · 빈 칸 = 자동) · 길이 · 전 · 후 = 단별 구간값(빈 칸 = 그대로). 지형이 세운 단까지만 — * 넘친 컨테이너 값은 파일에만 남음. 높이 = 요청 · 서는 키는 기하 규칙 그대로(지형 맞춤 · 형태 한계) · - * 하한을 넘은 요청만 폼에 되돌림(형태별 상한 없음). + * 그림이 안 서는 값(0 이하)만 폼에 되돌림(형태별 상한 없음 · 토스트 없음). */ function applySoilTiers( ctx: PipeOptionsContext, @@ -192,11 +185,8 @@ function applySoilTiers( m: typeof form === "string" && form ? form : blank, }; if (want.h != null) { - const h = Math.max(want.h, EXTRA_WALL_MIN_HEIGHT_M, REVET_EMBED_DEPTH_M + 0.1); - if (Math.abs(h - want.h) > 0.05) { - clipped[`soil_add_${n}_height`] = Number(h.toFixed(1)); - showToast(`흙막이 ${n} 높이는 최소 ${h.toFixed(1)}m로 적용됩니다.`, "error"); - } + const h = Math.max(want.h, WALL_MIN_PURE_HEIGHT_M); + if (Math.abs(h - want.h) > 0.05) clipped[`soil_add_${n}_height`] = Number(h.toFixed(1)); want.h = Number(h.toFixed(1)); } const sent = soilSent.get(key) ?? ctx.revetOffset.adjustFor(owner, role); diff --git a/B06_Section/B06_Section_UI_Page_Station_Controls.ts b/B06_Section/B06_Section_UI_Page_Station_Controls.ts index 8dcaee654..6f56403f8 100644 --- a/B06_Section/B06_Section_UI_Page_Station_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Station_Controls.ts @@ -419,8 +419,8 @@ export function createStationControls(deps: StationControlDeps): StationControls const current = basinAdjustments.get(key) ?? { ...DEFAULT_BASIN_ADJUST }; const next = { ...current, ...patch }; basinAdjustments.set(key, { - innerWidthM: Math.min(2, Math.max(1, round1(next.innerWidthM))), - innerHeightM: Math.min(2, Math.max(1, round1(next.innerHeightM))), + innerWidthM: Math.max(0.1, round1(next.innerWidthM)), + innerHeightM: Math.max(0.1, round1(next.innerHeightM)), lateralM: Math.max(0, clampMove(next.lateralM)), slopeM: Math.max(0, clampMove(next.slopeM)), }); diff --git a/resources/tester/test_b06_height_unlock.py b/resources/tester/test_b06_height_unlock.py new file mode 100644 index 000000000..476ccc7d4 --- /dev/null +++ b/resources/tester/test_b06_height_unlock.py @@ -0,0 +1,94 @@ +"""높이 제약 풀기(54-51) — 관 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽 · 집수정 높이를 사용자 값 그대로. + +남는 규칙: 그림이 안 서는 값(순수 높이 0 이하)만 0.1 에서 멈춤 · 토스트 없음. +서버 면적은 같은 TS 를 Node 로 돌림(`B06_Section_Server_Calc_Node.ts`) — 번들도 같은 판인지 봄. +""" + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +B06 = PROJECT_ROOT / "B06_Section" +TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" +BUNDLE = PROJECT_ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js" + +_RUNNER = """ +import { writeFileSync } from "node:fs"; +import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const.js"; +import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js"; +const spec = { revet_form: "찰쌓기", revet_height_m: 1.5 }; +const pipe = (h) => revetWallSpec(spec, { h, m: null }, false, 0.8).pureHeight; +const own = (h) => revetWallSpec(spec, { h, m: null }, true, 0).pureHeight; +const extras = buildOutletExtras({ + start: { offset: 10, elevation: 95 }, startBottomElevation: 94.5, outward: 1, + groundAt: () => 80, limitOffset: 40, + adjusts: [{ x: 0, d: 0, h: 0.3, m: null }], +}); +writeFileSync(process.argv[2], JSON.stringify({ + pipeLow: pipe(0.7), pipeAuto: pipe(null), pipeZero: pipe(0), + ownLow: own(0.3), ownNeg: own(-1), + extraCount: extras.walls.length, + extraPure: extras.walls.map((w) => w.topBack.elevation - w.bottomBack.elevation), +})); +""" + + +@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") +def test_높이는_입력_그대로_0이하만_멈춤(tmp_path: Path) -> None: + out = tmp_path / "js" + for name in ("Culvert_Const", "Culvert_Extra"): + subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(TSC), + str(B06 / f"B06_Section_UI_Cross_{name}.ts"), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--moduleResolution", + "bundler", + "--skipLibCheck", + "--ignoreConfig", + ], + cwd=str(PROJECT_ROOT), + capture_output=True, + ) + for emitted in out.rglob("*.js"): + emitted.write_text( + re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")), + encoding="utf-8", + ) + base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Culvert_Const.js")) + (base / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + result = tmp_path / "out.json" + subprocess.run( # noqa: S603 + ["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True + ) + got = json.loads(result.read_text(encoding="utf-8")) + + assert got["pipeLow"] == pytest.approx(0.7), "관 흙막이가 관경 기준 최소로 되돌아감" + assert got["pipeAuto"] == pytest.approx(1.8), "빈 칸 기본값(관경 + 1.0) 틀림" + assert got["pipeZero"] == pytest.approx(0.1), "0 이하가 그림 하한에서 안 멈춤" + assert got["ownLow"] == pytest.approx(0.3), "흙막이 0.3 이 근입 기준 최소로 되돌아감" + assert got["ownNeg"] == pytest.approx(0.1), "음수 높이가 그림 하한에서 안 멈춤" + assert got["extraCount"] == 1, "낮은 추가 흙막이가 안 섬" + assert got["extraPure"][0] == pytest.approx(0.3), "추가 흙막이 0.3 이 되돌아감" + + +def test_높이_토스트_없음_서버_같은_판() -> None: + for name in ("UI_Cross_Culvert_Wire", "UI_Page_Pipe_Options"): + source = (B06 / f"B06_Section_{name}.ts").read_text(encoding="utf-8") + assert "B06_Cross_Height_Floor" not in source, f"{name} 에 최소 높이 토스트가 남음" + assert "높이는 최소" not in source, f"{name} 에 최소 높이 토스트가 남음" + panel = (B06 / "B06_Section_UI_Cross_Structure_Panel.ts").read_text(encoding="utf-8") + assert "Math.min(\n 2," not in panel, "집수정 내공 2m 상한이 남음" + bundle = BUNDLE.read_text(encoding="utf-8") + assert "EXTRA_WALL_MIN_HEIGHT_M" not in bundle, "서버 번들이 옛 판 — npm run build:server-calc" + assert "WALL_MIN_PURE_HEIGHT_M" in bundle, "서버 번들이 옛 판 — npm run build:server-calc"