From fedb5bc166f309982ba09fa8a8bfb5d08de9b06d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 12:15:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06=20=EA=B4=80=20=EA=B8=80):=20371-2=20?= =?UTF-8?q?=ED=9A=A1=EB=8B=A8=20=EC=B9=B4=EB=93=9C=20=EA=B4=80=20=EA=B8=80?= =?UTF-8?q?=20L=20=EB=92=A4=20=EA=B8=B0=EC=9A=B8=EA=B8=B0=20S=3Dx.x%=20?= =?UTF-8?q?=C2=B7=20=ED=88=B4=ED=8C=81=20=EB=A7=A4=EC=84=A4=20=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=20=EC=A7=80=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「관매설 …, L=8 m (7.5), S=0.9%」 소수 한 자리 · 역경사 0.0 · 관 숨김(세월교 · 박스 흙막이) · B07 덧그림 글 없음 - 시험 test_371_2_pipe_slope_text · helper_371_2_pipe_slope Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- B06_Section/B06_Section_UI_Cross_Culvert.ts | 4 +- resources/tester/helper_371_2_pipe_slope.cjs | 120 ++++++++++++++++++ .../tester/test_371_2_pipe_slope_text.py | 49 +++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 resources/tester/helper_371_2_pipe_slope.cjs create mode 100644 resources/tester/test_371_2_pipe_slope_text.py diff --git a/B06_Section/B06_Section_UI_Cross_Culvert.ts b/B06_Section/B06_Section_UI_Cross_Culvert.ts index 9d53cf141..ee2e95d3c 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert.ts @@ -313,12 +313,12 @@ export function appendCulvertOverlay( const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : ""; const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter); - const pipeInfo = `관매설 ${kindLabel}Φ${Math.round(diameter * 1000)}m/m, L=${pipeLengthText(pipe.lengthM)}`; + // 관 기울기 = L 뒤 소수 한 자리 · 역경사는 0.0(371-2) + const pipeInfo = `관매설 ${kindLabel}Φ${Math.round(diameter * 1000)}m/m, L=${pipeLengthText(pipe.lengthM)}, S=${Math.max(pipe.slopePct, 0).toFixed(1)}%`; // 관 정보 글 = 노폭 · 노견 라벨 위 줄(225-1 · B06 카드만 — 그 라벨 없는 덧그림엔 안 그림) if (!hidePipe) appendPipeInfoText(layer, pipeInfo, null); const tip = pipeInfo + - ` · 매설 경사 ${pipe.slopePct.toFixed(1)}%(수평 가능)` + ` · 관 두께 ${(wallThickness * 1000).toFixed(1)}㎜(외경선 기준)` + ` · 최소 토피 ${culvert.min_cover_m.toFixed(1)}m(별표2 복토 교차 참조)`; // 외경(관 두께 바깥) → 내경 순으로 2겹. 도면처럼 관벽 두께가 보이게 한다(사용자 ③). diff --git a/resources/tester/helper_371_2_pipe_slope.cjs b/resources/tester/helper_371_2_pipe_slope.cjs new file mode 100644 index 000000000..23be4ef11 --- /dev/null +++ b/resources/tester/helper_371_2_pipe_slope.cjs @@ -0,0 +1,120 @@ +/* 371-2 관 글 기울기 헬퍼 — 짝 = `test_371_2_pipe_slope_text.py`. + * B07 픽스처 배관 측점에 `appendCulvertOverlay` 를 가짜 SVG 로 그려 관 글 · 툴팁을 읽음. + */ +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 noop = new Proxy({}, { get: () => () => "" }); + +/** 아주 작은 SVG 가짜 — 글 자리 판정에 쓰는 것만(class 고르기 · 속성 · 부모). */ +class El { + constructor(tag) { + this.tag = tag; + this.attrs = {}; + this.children = []; + this.parentNode = null; + this.ownerSVGElement = null; + this.textContent = ""; + } + setAttribute(k, v) { + this.attrs[k] = String(v); + } + getAttribute(k) { + return this.attrs[k] ?? null; + } + appendChild(c) { + c.parentNode = this; + c.ownerSVGElement = this.tag === "svg" ? this : this.ownerSVGElement; + this.children.push(c); + return c; + } + append(...cs) { + cs.forEach((c) => this.appendChild(c)); + } + addEventListener() {} + all() { + return this.children.flatMap((c) => [c, ...c.all()]); + } + querySelectorAll(sel) { + return this.all().filter((e) => (e.attrs.class ?? "").split(" ").includes(sel.slice(1))); + } + querySelector(sel) { + return this.querySelectorAll(sel)[0] ?? null; + } +} +globalThis.document = { addEventListener: () => undefined, createElementNS: (_ns, t) => new El(t) }; + +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + if (full.endsWith(".json")) return JSON.parse(fs.readFileSync(full, "utf8")); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const resolve = (base) => + fs.existsSync(base) && fs.statSync(base).isFile() ? base : `${base}.ts`; + const req = (r) => { + if (r.startsWith("@ui/") || r.startsWith("@config/")) return noop; + if (r.startsWith("@util/")) return loadTs(resolve(path.join(ROOT, "common_util", r.slice(6)))); + return loadTs(resolve(path.join(path.dirname(full), r))); + }; + new Function("exports", "module", "require", js)(box.exports, box, req); + cache.set(full, box.exports); + return box.exports; +} + +const { computeCulvertLayout } = loadTs( + path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Geom.ts"), +); +const { appendCulvertOverlay } = loadTs( + path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert.ts"), +); +const sections = JSON.parse( + fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"), +).cross_sections; +const section = sections.find((s) => s.culvert && !s.culvert.hidden_pipe); + +/** 노폭 라벨이 있는 B06 카드 꼴 svg 에 그려 관 글 · 관 툴팁을 돌려줌. */ +function draw(layout, hidePipe) { + const svg = new El("svg"); + const road = new El("text"); + road.setAttribute("class", "b06-chart__carriageway-label"); + road.setAttribute("x", 200); + road.setAttribute("y", 180); + svg.appendChild(road); + const plot = new El("g"); + svg.appendChild(plot); + appendCulvertOverlay( + plot, + layout, + (o) => o, + (z) => z, + undefined, + hidePipe, + false, + ); + const titles = svg + .all() + .filter((e) => e.tag === "title") + .map((e) => e.textContent); + return { + infos: svg.querySelectorAll(".b06-chart__pipe-info").map((e) => e.textContent), + pipeTips: titles.filter((t) => t.startsWith("관매설")), + }; +} +const layout = computeCulvertLayout(section, section.samples); +const reverse = { ...layout, pipe: { ...layout.pipe, slopePct: -0.43 } }; +process.stdout.write( + JSON.stringify({ + slopePct: layout.pipe.slopePct, + lengthM: layout.pipe.lengthM, + pipe: draw(layout, false), + reverse: draw(reverse, false), + hidden: draw(layout, true), + }), +); diff --git a/resources/tester/test_371_2_pipe_slope_text.py b/resources/tester/test_371_2_pipe_slope_text.py new file mode 100644 index 000000000..5ce087c98 --- /dev/null +++ b/resources/tester/test_371_2_pipe_slope_text.py @@ -0,0 +1,49 @@ +"""PLAN 371-2 — B06 횡단 카드 관 글 L 뒤 기울기(요구 240-16). + +규칙: 「관매설 {관종}Φ…m/m, L=8 m (7.6), S=2.5%」(소수 한 자리 · 역경사 0.0) · +툴팁 「매설 경사」 지움 · 세월교 · 박스 흙막이 세트(hidden_pipe = 관 숨김)는 관 글 없음 · +B07 덧그림(노폭 라벨 없음)도 없음(225-1). +짝 = `helper_371_2_pipe_slope.cjs`. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).parent / "helper_371_2_pipe_slope.cjs" + +pytestmark = pytest.mark.skipif( + not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치" +) + + +@pytest.fixture(scope="module") +def out() -> 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_관_글_L_뒤에_기울기_소수_한_자리(out: dict) -> None: + slope = f"{out['slopePct']:.1f}" + assert out["pipe"]["infos"] == [f"관매설 파형강관 Φ1000m/m, L=8 m (7.5), S={slope}%"] + + +def test_역경사는_0_0(out: dict) -> None: + assert out["reverse"]["infos"][0].endswith(", S=0.0%") + + +def test_툴팁에_매설_경사_없음(out: dict) -> None: + tips = out["pipe"]["pipeTips"] + assert tips and all("매설 경사" not in tip for tip in tips) + + +def test_관_숨김_세트는_관_글_없음(out: dict) -> None: + assert out["hidden"] == {"infos": [], "pipeTips": []}