Files
Aislo/resources/tester/test_revet_link_level.mjs
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt`
가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는
시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음.

- `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침**
- `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시)
- 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q`
  옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0

⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:12:30 +09:00

96 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 기슭막이 연동 하이라이트 짝짓기 + 경사 해제(수평) d 역산 자체검증 — 로직 복제.
// 2026-08-30 사용자 확정: ① 한 기슭막이는 연장 안 모든 횡단도에서 같이 강조된다
// ② 경사를 풀면 벽 상단 표고가 소유 측점 값과 같아진다(차이는 노견 확장이 흡수).
// 실행: node tmp/tests/test_revet_link_level.mjs
import assert from "node:assert/strict";
const FILL_SLOPE_RATIO_MIN = 1.2;
/* ── 복제: spanCovers / tierSpanOf / wallStandsAt ────────────────────── */
function spanCovers(span, deltaM) {
return deltaM < 0 ? -deltaM <= span.beforeM + 1e-9 : deltaM <= span.afterM + 1e-9;
}
function revetSpanOfSpec(spec) {
const length = Math.max(spec?.revet_length_m ?? 10, 0.1);
const beforeM = Math.max(spec?.revet_before_m ?? length / 2, 0);
const afterM = Math.max(spec?.revet_after_m ?? Math.max(length - beforeM, 0), 0);
return { beforeM, afterM };
}
function tierSpanOf(section, key) {
const stored = section.design?.extra_spans?.[key];
if (stored) return { beforeM: Math.max(stored.before_m, 0), afterM: Math.max(stored.after_m, 0) };
const spec = key.startsWith("bextra") ? section.culvert?.inlet : section.culvert?.outlet;
if (!spec) return { beforeM: 5, afterM: 5 };
return revetSpanOfSpec(spec);
}
function wallStandsAt(owner, section, key) {
const deltaM = section.chainage_m - owner.chainage_m;
if (Math.abs(deltaM) < 0.01) return true;
const spec = owner.culvert;
if (!spec) return false;
if (key.startsWith("extra") || key.startsWith("bextra")) {
return spanCovers(tierSpanOf(owner, key), deltaM);
}
const side = key === "inlet" ? spec.inlet : key === "outlet" ? spec.outlet : null;
if (!side) return false;
if (side.structure === "집수정" && !spec.hidden_pipe) return false;
return spanCovers(revetSpanOfSpec(side), deltaM);
}
/* ── ① 하이라이트 짝짓기 ─────────────────────────────────────────────── */
const owner = {
chainage_m: 200,
culvert: {
inlet: { structure: "기슭막이", revet_length_m: 10, revet_before_m: 5, revet_after_m: 5 },
outlet: { structure: "기슭막이", revet_length_m: 10, revet_before_m: 5, revet_after_m: 5 },
},
design: { extra_spans: { extra0: { length_m: 20, before_m: 12, after_m: 8 } } },
};
const at = (chainage) => ({ chainage_m: chainage });
// 기준벽 — 전 5·후 5 안의 측점에서만 선다.
assert.equal(wallStandsAt(owner, at(200), "outlet"), true, "소유 측점은 언제나");
assert.equal(wallStandsAt(owner, at(205), "outlet"), true, "후 5m 끝");
assert.equal(wallStandsAt(owner, at(195), "outlet"), true, "전 5m 끝");
assert.equal(wallStandsAt(owner, at(206), "outlet"), false, "연장 밖");
// 다단은 **단별 구간값**을 따른다 — 기준벽보다 길 수 있다(전 12·후 8).
assert.equal(wallStandsAt(owner, at(190), "extra0"), true, "다단 전 12m");
assert.equal(wallStandsAt(owner, at(208), "extra0"), true, "다단 후 8m");
assert.equal(wallStandsAt(owner, at(209), "extra0"), false, "다단 연장 밖");
// 단별 값이 없는 단은 소유 벽 연장을 상속한다.
assert.equal(wallStandsAt(owner, at(205), "extra1"), true);
assert.equal(wallStandsAt(owner, at(206), "extra1"), false);
// 집수정이 선 쪽은 소유 측점 하나뿐 — 옆 카드에는 없다(강조도 없다).
const basinOwner = {
chainage_m: 200,
culvert: {
inlet: { structure: "집수정", basin_length_m: 2, basin_before_m: 1, basin_after_m: 1 },
outlet: { structure: "기슭막이" },
},
};
assert.equal(wallStandsAt(basinOwner, at(201), "inlet"), false, "집수정은 옆 측점에 안 선다");
assert.equal(wallStandsAt(basinOwner, at(201), "outlet"), true);
/* ── ② 경사 해제(수평) d 역산 ────────────────────────────────────────── */
// 배관 벽 자리 규칙: 상단 = 노견 표고 d/1.2(placePipeWall). 이 측점 d를
// `소유 d + 1.2 × dz`로 밀면 상단 표고가 소유 측점 값 그대로다.
const levelDrop = (ownerD, dz) => ownerD + FILL_SLOPE_RATIO_MIN * dz;
const topOf = (edgeElevation, d) => edgeElevation - d / FILL_SLOPE_RATIO_MIN;
const edgeOwner = 100.0;
const ownerD = 4.8;
const topOwner = topOf(edgeOwner, ownerD);
for (const dz of [-1.5, -0.4, 0, 0.4, 2.0]) {
const edgeHere = edgeOwner + dz;
const here = topOf(edgeHere, levelDrop(ownerD, dz));
assert.ok(Math.abs(here - topOwner) < 1e-9, `dz=${dz} 상단 어긋남: ${here} != ${topOwner}`);
}
// 경사를 켜 두면(보정 없음) 벽은 종단을 탄다 — 상단이 dz만큼 따라 움직인다.
const sloped = topOf(edgeOwner + 1.2, ownerD);
assert.ok(Math.abs(sloped - topOwner - 1.2) < 1e-9, "경사 반영이면 상단이 계획고를 따라간다");
// 이 측점 노견이 소유 벽 상단보다 낮으면 d가 음수 — 기하가 0에서 자른다(노견까지만).
const dNegative = levelDrop(ownerD, -(ownerD / FILL_SLOPE_RATIO_MIN) - 1);
assert.ok(dNegative < 0, "노견보다 위로는 못 올라간다 — 0에서 잘릴 요청");
console.log("test_revet_link_level.mjs OK");