feat(b06): 연동 흙막이 높이 = 본 흙막이 그린 높이 · 풀린 카드 「이 측점 높이」 줄(152-2)
- 소유 측점 적용값의 빈 높이 = 그린 순수 높이 — 옆 카드 · B07 · 서버 면적 · 연동 풂 굳힘이 같은 값(박스 옆 측점 0.5 → 2.0 m) - 연동 푼 카드의 유입 · 유출 흙막이 = 좌측 칸에 「이 측점 높이」 ±0.1 줄(날개벽 칸 포함) - 새 시험 test_152_2_link_height · 받는 줄 문자열 시험 셋 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -49,7 +49,10 @@ const EXTRA_ROWS = [
|
||||
|
||||
/** 유입구·유출구 칸은 폼에 없는 줄만 받는다 — 읽기(좌우 · 상하 · 구체 길이 · 표고) ·
|
||||
* 단 수 · 옵션 · 집수정 내공(24-4a). */
|
||||
/** 연동을 푼 옆 카드의 「이 측점 높이」 줄 — 어느 칸이든 받음(152-2). */
|
||||
const OWN_HEIGHT_ROW = ".b06-structure-panel__struct--own-height";
|
||||
const SIDE_ROWS = [
|
||||
OWN_HEIGHT_ROW,
|
||||
".b06-structure-panel__struct--readout",
|
||||
".b06-structure-panel__struct--tier",
|
||||
".b06-structure-panel__struct--options",
|
||||
@@ -59,15 +62,16 @@ const SIDE_ROWS = [
|
||||
/** 배관 칸 = 폼에 자리 없는 집수정 내공 · 유입 단 수만 — 목록에서 골라도 같은 글(54-29).
|
||||
* 유출 단 수는 폼 「추가 흙막이(단)」 칸(54-27) · 읽기 · 옵션 줄은 안 받음. */
|
||||
const PIPE_INLET_ROWS = [
|
||||
OWN_HEIGHT_ROW,
|
||||
".b06-structure-panel__struct--tier",
|
||||
".b06-structure-panel__struct--basin-size",
|
||||
].join(", ");
|
||||
const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";
|
||||
const PIPE_OUTLET_ROWS = `${OWN_HEIGHT_ROW}, .b06-structure-panel__struct--basin-size`;
|
||||
|
||||
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은
|
||||
* 받는 줄 없음(측벽 높이 줄 지움 · 107-3 · 읽기는 칸에 안 둠 · 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 OWN_HEIGHT_ROW;
|
||||
if (slot === sideSlots?.outlet) return PIPE_OUTLET_ROWS;
|
||||
return slot === sideSlots?.inlet ? PIPE_INLET_ROWS : SIDE_ROWS;
|
||||
}
|
||||
|
||||
@@ -680,6 +680,7 @@ export function computeCulvertLayout(
|
||||
},
|
||||
bodyX:
|
||||
inletBody || outletBody ? { inlet: inletBody?.x ?? 0, outlet: outletBody?.x ?? 0 } : null,
|
||||
pureHeights: { inlet: inletWallSpec.pureHeight, outlet: outletWallSpec.pureHeight },
|
||||
designTrim: body && ownTrim ? mergeGuardTrim(ownTrim, body.trim, walls) : ownTrim,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,6 +221,8 @@ export interface CulvertLayout {
|
||||
inletOptions: { revetAllowed: boolean; basinLUAllowed: boolean };
|
||||
/** 세월교 · 박스 흙막이(73-4) — 본체 끝 자리의 배관 4축 x(적용 x 의 0점). 본체 없으면 없음. */
|
||||
bodyX?: { inlet: number; outlet: number } | null;
|
||||
/** 유입 · 유출 벽의 순수 높이(m) — 높이 빈 칸인 연동 옆 카드가 물려받음(152-2). */
|
||||
pureHeights?: { inlet: number; outlet: number };
|
||||
}
|
||||
/** 유입측 구조물 사용자 선택 — auto = 규칙 기반 자동 선택. */
|
||||
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
@@ -606,7 +606,16 @@ export function ownerAppliedAdjusts(
|
||||
owner.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(owner),
|
||||
inletStructure?.adjustFor(owner),
|
||||
);
|
||||
return ownerLayout ? bodyAbsolute(ownerLayout) : ownerInput;
|
||||
if (!ownerLayout) return ownerInput;
|
||||
// 높이 빈 칸 = 소유 측점이 그린 순수 높이 — 옆 측점엔 본체가 없어 근입 0.5 만 서던 것(152-2).
|
||||
const shift = bodyAbsolute(ownerLayout);
|
||||
const heights = ownerLayout.pureHeights;
|
||||
if (!heights) return shift;
|
||||
return {
|
||||
...shift,
|
||||
inlet: { ...shift.inlet, h: shift.inlet.h ?? heights.inlet },
|
||||
outlet: { ...shift.outlet, h: shift.outlet.h ?? heights.outlet },
|
||||
};
|
||||
}
|
||||
|
||||
function adjustsInput(
|
||||
|
||||
@@ -248,6 +248,7 @@ export function buildStructurePanel(
|
||||
const heightRow = heightParts.row;
|
||||
// 다단 벽에서는 이 행이 [추가 기슭막이] 그룹으로 옮겨 간다(2026-08-30 지시 4).
|
||||
heightRow.classList.add("b06-structure-panel__struct--height");
|
||||
const heightLabel = heightRow.querySelector?.<HTMLElement>(".b06-structure-panel__label");
|
||||
const heightValue = document.createElement("span");
|
||||
heightValue.className = "b06-structure-panel__hval";
|
||||
const heightMinus = makeButton(
|
||||
@@ -550,11 +551,14 @@ export function buildStructurePanel(
|
||||
// 형태 · 높이는 좌측 폼 칸이 정본이다(유입구 · 유출구 · 독립 좌우 · 구조물 옵션) — 폼에
|
||||
// 자기 칸이 없는 **다단 벽**만 이 줄을 「추가 기슭막이」 칸에 세운다(24-4a).
|
||||
materialRow.classList.toggle("is-hidden", !isExtra);
|
||||
heightRow.classList.toggle("is-hidden", !isExtra);
|
||||
if (isExtra) {
|
||||
heightValue.textContent = `${deps.heightFor(key).toFixed(1)}m`;
|
||||
materialSelect.value = deps.formFor(key);
|
||||
}
|
||||
// 연동을 푼 옆 카드의 흙막이 = 「이 측점 높이」 줄(폼 칸은 본 흙막이 · 152-2)
|
||||
const ownHeight = deps.linkState()?.linked === false && guardKey && !isBasin;
|
||||
heightRow.classList.toggle("is-hidden", !isExtra && !ownHeight);
|
||||
heightRow.classList.toggle("b06-structure-panel__struct--own-height", ownHeight);
|
||||
if (heightLabel)
|
||||
heightLabel.textContent = ownHeight ? "이 측점 높이" : L("B06_Cross_Height_Label");
|
||||
if (isExtra || ownHeight) heightValue.textContent = `${deps.heightFor(key).toFixed(1)}m`;
|
||||
if (isExtra) materialSelect.value = deps.formFor(key);
|
||||
// 단 수 행 — 벽 종류를 가리지 않고 **높이 행 바로 아래** 한 자리에 온다.
|
||||
// 유출 벽은 자기 채널(extra), 유입 벽·집수정 계류측은 basin 채널(bextra)을 쓴다.
|
||||
// 유출 벽과 독립 기슭막이 유입 벽에서는 **항상** 보인다(2026-08-22 사용자 ⑤ —
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/* 152-2 연동 흙막이 높이 헬퍼 — 높이 칸 빈 박스 흙막이(73-4 픽스처 352.14)의 연동 옆 측점(340 · 360)이
|
||||
* 소유 측점이 그린 순수 높이를 물려받는지 · 연동을 푼 카드만 자기 높이로 서는지. 결과 = 표준출력 JSON.
|
||||
* 짝 = `test_152_2_link_height.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"),
|
||||
);
|
||||
// 높이 칸 비움(= 자동 · 세월교 · 박스 흙막이 기본) — 원본은 3.5 를 넣은 상태
|
||||
const sections = fixture.cross_sections.map((s) => {
|
||||
if (!s.culvert?.body) return s;
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
copy.culvert.inlet.revet_height_m = null;
|
||||
copy.culvert.outlet.revet_height_m = null;
|
||||
return copy;
|
||||
});
|
||||
const at = (list, c) => list.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
||||
const round = (v) => Math.round(v * 1000) / 1000;
|
||||
const heightsOf = (list, c) => {
|
||||
const culvert = computeStoredLayouts(at(list, c), list)?.culvert;
|
||||
return {
|
||||
pure: culvert?.pureHeights
|
||||
? { inlet: round(culvert.pureHeights.inlet), outlet: round(culvert.pureHeights.outlet) }
|
||||
: null,
|
||||
walls: (culvert?.walls ?? []).map((w) => ({ role: w.role, height: round(w.height) })),
|
||||
shift: culvert?.revetShift ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const out = { owner: heightsOf(sections, 352.14), linked: {} };
|
||||
for (const c of [340, 360]) out.linked[c] = heightsOf(sections, c);
|
||||
|
||||
// 340 연동 풂 + 유출 흙막이만 +0.1 m(좌측 「이 측점 높이」 줄이 그 측점 값을 고친 것)
|
||||
const shift = out.linked[340].shift;
|
||||
const detached = sections.map((s) => {
|
||||
if (Math.abs(s.chainage_m - 340) >= 0.6) return s;
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
copy.design.revet_link_detached = true;
|
||||
copy.design.revet_adjust = {
|
||||
...(copy.design.revet_adjust ?? {}),
|
||||
inlet: { ...shift.inlet },
|
||||
outlet: { ...shift.outlet, h: round(shift.outlet.h + 0.1) },
|
||||
};
|
||||
return copy;
|
||||
});
|
||||
out.detached = {
|
||||
340: heightsOf(detached, 340),
|
||||
360: heightsOf(detached, 360),
|
||||
owner: heightsOf(detached, 352.14),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,55 @@
|
||||
"""152-2 연동 흙막이 높이 — 본 흙막이가 그린 순수 높이를 옆 카드가 물려받음 · 풀면 그 측점만 따로.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_152_2_link_height.cjs`) · 픽스처 = 73-4(박스 352.14 · 높이 칸 비움).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_152_2_link_height.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def screen() -> 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 _src(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_연동_옆_카드_높이_본_흙막이와_같음(screen: dict) -> None:
|
||||
owner = screen["owner"]["pure"]
|
||||
assert owner == {"inlet": 2.0, "outlet": 2.0} # 본체 끝 높이(근입 0.5 아님)
|
||||
for chainage in ("340", "360"):
|
||||
assert screen["linked"][chainage]["pure"] == owner
|
||||
assert screen["linked"][chainage]["walls"] == screen["owner"]["walls"]
|
||||
|
||||
|
||||
def test_연동_풀린_카드만_따로(screen: dict) -> None:
|
||||
detached = screen["detached"]
|
||||
assert detached["340"]["pure"] == {"inlet": 2.0, "outlet": 2.1}
|
||||
assert detached["360"]["pure"] == {"inlet": 2.0, "outlet": 2.0}
|
||||
assert detached["owner"]["pure"] == {"inlet": 2.0, "outlet": 2.0}
|
||||
|
||||
|
||||
def test_풀린_카드_이_측점_높이_줄() -> None:
|
||||
panel = _src("B06_Section/B06_Section_UI_Cross_Structure_Panel.ts")
|
||||
assert "deps.linkState()?.linked === false && guardKey && !isBasin" in panel
|
||||
assert '"이 측점 높이"' in panel
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
assert 'const OWN_HEIGHT_ROW = ".b06-structure-panel__struct--own-height";' in dock
|
||||
assert "return OWN_HEIGHT_ROW;" in dock # 세월교 · 박스 날개벽 칸도 받음
|
||||
wire = _src("B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts")
|
||||
assert "h: shift.inlet.h ?? heights.inlet" in wire
|
||||
@@ -88,5 +88,8 @@ def test_자동_자리_읽기_글이_없음() -> None:
|
||||
|
||||
def test_배관_유출_칸은_옛_단_수_줄을_안_받음() -> None:
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
# 54-29 — 배관 유출 칸 = 집수정 내공 줄만(단 수 · 읽기 · 옵션 줄 안 받음)
|
||||
assert 'const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";' in dock
|
||||
# 54-29 — 배관 유출 칸 = 집수정 내공 줄만(단 수 · 읽기 · 옵션 줄 안 받음) · 152-2 풀린 카드 높이 줄
|
||||
assert (
|
||||
"const PIPE_OUTLET_ROWS = `${OWN_HEIGHT_ROW}, .b06-structure-panel__struct--basin-size`;"
|
||||
in dock
|
||||
)
|
||||
|
||||
@@ -168,8 +168,11 @@ def test_집계표_세월교_박스_흙막이_상자는_유출_단_수만큼():
|
||||
|
||||
|
||||
def test_B06_세월교_박스_칸은_받는_줄_없음():
|
||||
"""107-3 — 측벽 높이 줄 지움."""
|
||||
"""107-3 — 측벽 높이 줄 지움 · 152-2 연동 풀린 카드 「이 측점 높이」 줄만 받음."""
|
||||
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 OWN_HEIGHT_ROW"
|
||||
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
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_세월교_바닥_손_값은_원지반을_바꿔도_그대로(fit: dict)
|
||||
def test_세월교_박스_칸은_측벽_높이_줄_없음() -> None:
|
||||
"""107-3 — 「측벽 높이(자동)」 줄 지움(유입 · 유출)."""
|
||||
dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert "sideSlots?.wingOutlet) return null" in dock
|
||||
assert "sideSlots?.wingOutlet) return OWN_HEIGHT_ROW" in dock # 152-2 풀린 카드 높이 줄만
|
||||
assert "wall-height" not in dock
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user