From 8c04d520f4832134ae5ef3a8cc77c1a1b6a77e4a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 12:03:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90=20?= =?UTF-8?q?=EB=B0=94=EB=8B=A5=20=EC=86=90=20=EC=A1=B0=EC=A0=95=20=EC=B9=B8?= =?UTF-8?q?=20=E2=80=94=20=EB=82=A0=EA=B0=9C=EB=B2=BD=20=EC=B9=B8=EC=97=90?= =?UTF-8?q?=20=EC=B8=A1=EB=B2=BD=20=EB=86=92=EC=9D=B4=20=EC=A4=84=20=C2=B7?= =?UTF-8?q?=20=EC=86=90=20=EA=B0=92=EC=9D=80=20=EC=9B=90=EC=A7=80=EB=B0=98?= =?UTF-8?q?=20=EB=B0=94=EB=80=8C=EC=96=B4=EB=8F=84=20=EA=B7=B8=EB=8C=80?= =?UTF-8?q?=EB=A1=9C(91-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- B06_Section/B06_Section_UI_Adjust_Dock.ts | 7 +- .../B06_Section_UI_Cross_Ford_Panel.ts | 4 +- resources/tester/helper_91_ground_free.cjs | 100 ++++++++++++++++++ resources/tester/test_65_ford_defaults.py | 8 +- resources/tester/test_91_ground_free.py | 46 ++++++++ 5 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 resources/tester/helper_91_ground_free.cjs create mode 100644 resources/tester/test_91_ground_free.py diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index d1979183c..80bf80461 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -64,11 +64,12 @@ const PIPE_INLET_ROWS = [ ".b06-structure-panel__struct--basin-size", ].join(", "); const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size"; +const WALL_HEIGHT_ROW = ".b06-structure-panel__struct--wall-height"; -/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은 안 받음 - * (높이 · 측벽 좌우 · 상하 · 바닥판 읽기는 칸에 안 둠 · 강조만 · 65-1). */ +/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은 + * 세월교 측벽 높이(바닥 손 조정 · 91-2)만 받음(좌우 · 상하 · 바닥판 읽기는 칸에 안 둠 · 65-1). */ function rowsFor(slot: HTMLElement): string | null { - if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null; + if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return WALL_HEIGHT_ROW; if (slot === sideSlots?.outlet) return PIPE_OUTLET_ROWS; return slot === sideSlots?.inlet ? PIPE_INLET_ROWS : SIDE_ROWS; } diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts index 09e1b9a52..22842406e 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts @@ -152,7 +152,9 @@ export function buildFordPanel(deps: FordPanelDeps, opts?: { dock?: boolean }): `높이 ${deps.heightFor(current).toFixed(2)}m` + (adjust.heightM === null ? " (자동)" : ""); moveLine.textContent = `좌우 ${adjust.lateralM.toFixed(1)}m · 상하 ${adjust.slopeM.toFixed(1)}m`; slabLine.textContent = `바닥판 ${deps.slabLengthM().toFixed(2)}m`; - heightValue.textContent = `${deps.heightFor(current).toFixed(1)}m`; + // 자동 = 바닥판이 계류 하상을 따라감 · 손 값 = 원지반이 바뀌어도 그대로(91-2) + heightValue.textContent = + `${deps.heightFor(current).toFixed(1)}m` + (adjust.heightM === null ? " 자동" : ""); } return { diff --git a/resources/tester/helper_91_ground_free.cjs b/resources/tester/helper_91_ground_free.cjs new file mode 100644 index 000000000..fe598aba9 --- /dev/null +++ b/resources/tester/helper_91_ground_free.cjs @@ -0,0 +1,100 @@ +/* 91 원지반 · 암 제약 삭제 · 유출측 복합경사 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. + * 입력 = 73-4 픽스처(세월교 173.09 · 박스 352.14 · 관 180) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_91_ground_free.py`. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +// 화면 전용 모듈(토스트 · 글) — 기하 계산엔 안 쓰임 +const STUBS = { + "@ui/ui_template_toast": { showToast: () => undefined }, +}; + +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + if (full.endsWith(".json")) { + const data = JSON.parse(fs.readFileSync(full, "utf8")); + cache.set(full, data); + return data; + } + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true, + }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; + for (const [prefix, dir] of Object.entries(alias)) + if (request.startsWith(prefix)) + return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); + return loadTs(resolve(path.join(path.dirname(full), request))); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + cache.set(full, box.exports); + return box.exports; +} +const resolve = (base) => + [base, `${base}.ts`, path.join(base, "index.ts")].find( + (p) => fs.existsSync(p) && fs.statSync(p).isFile(), + ) ?? `${base}.ts`; + +const B06 = (f) => path.join(ROOT, "B06_Section", f); +const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); + +const fixture = JSON.parse( + fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), +); +const sections = fixture.cross_sections; +const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6); +const clone = (v) => JSON.parse(JSON.stringify(v)); +const r3 = (v) => Math.round(v * 1000) / 1000; +const out = {}; +/** 원지반 사본 — [from, to] 안 샘플을 dz 만큼 옮김. */ +const shiftGround = (s, dz, from = -Infinity, to = Infinity) => { + for (const p of s.samples) + if (p.offset_m >= from && p.offset_m <= to && p.elevation_m != null) p.elevation_m += dz; +}; +const layoutsOf = (s, base) => + computeStoredLayouts( + s, + sections.map((x) => (x === base ? s : x)), + ); + +// ① 91-2 세월교 바닥 — 자동은 하상을 따라감 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도 +const FORD = at(173.087); +function fordFloor(edit) { + const s = clone(FORD); + for (const role of ["inlet", "outlet"]) s.culvert[role].revet_height_m = null; + edit(s); + const { ford, culvert } = layoutsOf(s, FORD); + return ford.sides.map((side) => { + const wall = culvert.walls.find((w) => w.role === side.role); + return { + role: side.role, + slabTop: r3(side.slabTopElevation), + wallBottom: wall ? r3(Math.min(...wall.points.map((p) => p.elevation))) : null, + }; + }); +} +const hand = (s) => { + s.design.ford_adjust = { + inlet: { heightM: 2.0, lateralM: 0, slopeM: 0 }, + outlet: { heightM: 2.4, lateralM: 0, slopeM: 0 }, + }; +}; +out.fordAuto = fordFloor(() => undefined); +out.fordAutoBedDown = fordFloor((s) => shiftGround(s, -2, -2, 2)); +out.fordHand = fordFloor(hand); +out.fordHandBedDown = fordFloor((s) => { + hand(s); + shiftGround(s, -2, -2, 2); +}); +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_65_ford_defaults.py b/resources/tester/test_65_ford_defaults.py index 052826723..9bb6ad637 100644 --- a/resources/tester/test_65_ford_defaults.py +++ b/resources/tester/test_65_ford_defaults.py @@ -147,8 +147,12 @@ def test_집계표_세월교_흙막이_상자_건(): ] -def test_B06_측벽_읽기_줄은_세월교_박스_칸에_안_옮김(): +def test_B06_세월교_박스_칸은_측벽_높이_줄만_받음(): dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8") - assert "slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null" in dock + assert ( + "slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return WALL_HEIGHT_ROW" + in dock + ) + assert 'WALL_HEIGHT_ROW = ".b06-structure-panel__struct--wall-height"' in dock lists = (ROOT / "B05_Profile/B05_Profile_UI_Structures_List.ts").read_text(encoding="utf-8") assert "return isNone(c, state[c.id][0])" in lists diff --git a/resources/tester/test_91_ground_free.py b/resources/tester/test_91_ground_free.py new file mode 100644 index 000000000..c33e96f8e --- /dev/null +++ b/resources/tester/test_91_ground_free.py @@ -0,0 +1,46 @@ +"""91 원지반 · 암 제약 삭제 · 유출측 복합경사. + +화면 TS 를 그 자리에서 돌린 값(`helper_91_ground_free.cjs`) · 픽스처 = 73-4 세월교 173.09 · 박스 352.14 · 관 180. +""" + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +HELPER = Path(__file__).parent / "helper_91_ground_free.cjs" +NODE_MODULES = ROOT / "config" / "node_modules" + +pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치") + + +@pytest.fixture(scope="module") +def fit() -> dict: + done = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8" + ) + assert done.returncode == 0, done.stderr + return json.loads(done.stdout) + + +def test_세월교_바닥_자동은_하상을_따라감(fit: dict) -> None: + for before, after in zip(fit["fordAuto"], fit["fordAutoBedDown"]): + assert after["slabTop"] < before["slabTop"] - 1.0 + + +def test_세월교_바닥_손_값은_원지반을_바꿔도_그대로(fit: dict) -> None: + assert fit["fordHand"] == fit["fordHandBedDown"] + # 손 값 = 측벽 높이 · 흙막이 바닥 = 바닥판 밑(두께 0.3) + for side in fit["fordHand"]: + assert side["wallBottom"] == pytest.approx(side["slabTop"] - 0.3, abs=1e-3) + + +def test_세월교_박스_칸은_측벽_높이_줄을_받음() -> None: + dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8") + assert "sideSlots?.wingOutlet) return WALL_HEIGHT_ROW" in dock