Files
Aislo/resources/tester/helper_b05_routeedit_initial.cjs
T
eomsangdonandClaude Opus 5.5 1758ba0fb2 feat(B05): 초기노선 갈래 — 제약 못 지킨 구간 한 줄 · 상한 풂 안내 · 기울기 잴 지반 고름 칸
- 비교값 줄 아래 「제약 못 지킨 구간 N곳: No.9 16.4%(지형상 불가능) · …」(없으면 「모두 지킴」 · 5곳 넘으면 「외 N곳」 · 풍선에 전부와 사유)
- relaxed 면 「지형상 14.0% 로 못 이어져 상한을 29.1% 로 풂」(경고 색)
- 공통 제약 줄에 서버 grade_smooth_m 을 「기울기 잴 지반 5m 고름」으로
- 시험 — 글 만들기(test_b05_routeedit_initial) · 사본 왕복에서 목록 · 풂 · 고름 칸이 화면 줄에(test_b05_routeedit_initial_roundtrip)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sCeZz8z8YnbovsKgn7HK7
2026-09-29 10:42:14 +09:00

253 lines
7.8 KiB
JavaScript

/* 계획노선 편집 창 **초기노선 갈래**(계획서 20) 셈 단위검증 헬퍼 —
* `B05_Profile_UI_RouteEdit_Initial_Model.ts` 를 그 자리에서 트랜스파일해 Node 로 돌린다.
*
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_cut_merged_loop.cjs` 머리에 적힌 것과 같다.
* 브라우저 전용인 저장소는 가짜로 채운다 — 셈만 본다.
*/
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();
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
_map: map,
};
}
const session = memoryStorage();
globalThis.window = { localStorage: memoryStorage(), sessionStorage: session };
globalThis.fetch = () => Promise.resolve({ ok: false });
const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } };
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
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 localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const model = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Initial_Model.ts"));
const { DEFAULT_WEIGHTS } = model;
// ① 비중 맞추기 — 하나를 옮기면 나머지 둘이 비율대로 나눠 합 100.
const moves = [
model.rebalanceWeights(DEFAULT_WEIGHTS, "expected", 70),
model.rebalanceWeights(DEFAULT_WEIGHTS, "earthwork", 40),
model.rebalanceWeights({ expected: 100, earthwork: 0, grade: 0 }, "expected", 60),
model.rebalanceWeights({ expected: 20, earthwork: 30, grade: 50 }, "grade", 100),
model.rebalanceWeights({ expected: 33, earthwork: 33, grade: 34 }, "earthwork", 33.4),
model.rebalanceWeights(DEFAULT_WEIGHTS, "grade", -5),
];
// ② 서버 답 펴기 — 노드 목록 꼴과 `/route/plan` 꼴(노드+곡선) 둘 다.
const plain = model.normalizeRouteInitial(
{
nodes: [
{ x: 0, y: 0 },
{ x: 50, y: 10, curve: true, radius_m: 30 },
{ x: 100, y: 0, curve: false },
],
// 계획서 20 「API」 줄 모양.
metrics: {
length_m: 1234.56,
max_grade_pct: 12.34,
avg_grade_pct: 6.1,
cut_m3: 1500.4,
fill_m3: 900,
over_limit: { grade: 1, radius: 0, stream: 1, total: 2 },
offset_avg_m: 3.25,
offset_max_m: 9.8,
raw_max_grade_pct: 41.2,
violations: [
{
kind: "grade",
from_m: 180,
to_m: 220,
station: "No.9+0.0",
value: 29.13,
limit: 14,
reason: "지형상 불가능 — 복도 안 어떤 길도 23.5% 이상 기울기를 지나야 이어짐",
},
{
kind: "radius",
from_m: 400,
to_m: 412,
station: "No.20+0.0",
value: 9.5,
limit: 12,
reason: "곡선 둘 자리 모자람 — 앞뒤 직선이 짧아 최소 반지름 원호가 안 들어감",
},
{
kind: "stream",
from_m: 600,
to_m: 700,
station: "No.30+0.0",
value: 32,
limit: 100,
reason: "계류 버퍼 안 — 건너기 · 기점 · 종점 봐줌을 넘음",
},
],
},
criteria: {
max_grade_pct: 14,
min_radius_m: 12,
stream_offset_m: 5,
road_width_m: 4,
corridor_m: 100,
grade_smooth_m: 5,
pending: ["max_grade_pct", "stream_offset_m"],
},
relaxed: true,
terrain_bound_pct: 23.46,
},
"earthwork",
null,
);
const planShape = model.normalizeRouteInitial(
{
nodes: [
{ x: 0, y: 0, radius_m: null, inner_angle_deg: null, tangent_m: null, violations: [] },
{ x: 40, y: 30, radius_m: 25, inner_angle_deg: 140, tangent_m: 9, violations: [] },
{ x: 90, y: 30, radius_m: null, inner_angle_deg: null, tangent_m: null, violations: [] },
],
curves: [
{
apex: [40, 30],
radius_m: 25,
tangent_m: 9,
inner_angle_deg: 140,
start: [33, 25],
end: [49, 30],
node_first: 1,
node_last: 1,
violations: [],
},
],
},
"weighted",
{ expected: 60, earthwork: 20, grade: 20 },
);
let tooShort = "";
try {
model.normalizeRouteInitial({ nodes: [{ x: 1, y: 1 }] }, "gentle", null);
} catch (error) {
tooShort = error.message;
}
// ③ 글 — 현재 초기값 · 비교값 · 기준값.
const labels = {
follow: model.choiceLabel("follow", null),
weighted: model.choiceLabel("weighted", { expected: 60, earthwork: 20, grade: 20 }),
metrics: model.metricsLine(plain.metrics),
metricsNone: model.metricsLine(null),
criteria: model.criteriaLine(plain.criteria, 0),
criteriaConfirmed: model.criteriaLine({ ...plain.criteria, confirmed: true }, 0),
criteriaFallback: model.criteriaLine(null, 15),
violations: model.violationsLine(plain.metrics),
violationsNone: model.violationsLine({ ...plain.metrics, violations: [] }),
violationsOld: model.violationsLine({ ...plain.metrics, violations: null }),
violationsMany: model.violationsLine({
...plain.metrics,
violations: Array.from({ length: 7 }, (_, i) => ({
kind: "grade",
station: `No.${i}+0.0`,
value: 15 + i,
limit: 14,
reason: "곡선 맞춤 뒤 지반 차 — 측점 사이 지반이 상한을 넘음",
})),
}),
relaxed: model.relaxedLine(plain),
notRelaxed: model.relaxedLine(planShape),
pendingEmpty: model.normalizeRouteInitial(
{
nodes: [
{ x: 0, y: 0 },
{ x: 9, y: 9 },
],
criteria: { pending: [] },
},
"gentle",
null,
).criteria.confirmed,
};
// ⑤ 서버가 적어 둔 현재 초기값(`current_initial`) — 펴기와 캐시 견주기.
const bases = {
follow: model.serverBase({ logic: "follow", weights: { expected: 100, earthwork: 0, grade: 0 } }),
weighted: model.serverBase({
logic: "weighted",
weights: { expected: 70.0, earthwork: 15.0, grade: 15.0 },
}),
unknown: model.serverBase({ logic: "nope" }),
none: model.serverBase(null),
};
const sameAsServer = {
earthVsFollow: model.sameBase(plain, bases.follow),
earthVsEarth: model.sameBase(plain, model.serverBase({ logic: "earthwork" })),
weightedSame: model.sameBase(
planShape,
model.serverBase({ logic: "weighted", weights: { expected: 60, earthwork: 20, grade: 20 } }),
),
weightedOther: model.sameBase(planShape, bases.weighted),
};
// ④ 캐시 왕복 — 결과 통 프로젝트 칸. 꼴이 틀린 값은 안 읽는다.
model.writeInitialChoice("p-1", plain);
const readBack = model.readInitialChoice("p-1");
const other = model.readInitialChoice("p-2");
const keys = [...session._map.keys()];
session._map.set(
keys[0],
JSON.stringify({
logic: "nope",
snapshot: {
planned: [
[0, 0],
[1, 1],
],
},
}),
);
const badLogic = model.readInitialChoice("p-1");
model.writeInitialChoice("p-1", null);
const cleared = model.readInitialChoice("p-1");
process.stdout.write(
JSON.stringify({
moves,
plain,
planShape,
tooShort,
labels,
readBack,
other,
keys,
badLogic,
cleared,
bases,
sameAsServer,
}),
);