Files
Aislo/resources/tester/helper_318_cross_card.cjs
T
eomsangdonandClaude Opus 5.5 2641c15e29 fix(b05·b06): 정본 다른 종류(관 ↔ 맹암거 · 개거 등)로 바꿈 = 옛 것 지우고 같은 측점에 새 종류 · 일반 측점 자리 종류 바꿈 · 지움 = 구조물만 뺀 사본에 새 종류 · 중심고 상자에 겹친 Y 눈금 숨기지 않음(상자 위 · 안 비치는 바탕) (319 · 요구 217)
- 공용 구조물 패널 종류 바꿈: 관 지점 ↔ 구조물이면 새로 놓기 폼으로 빠지던 것 → 옛 것 지움 → 우클릭 추가 길(addAt)로 같은 측점 · 옛 측점 조정값 버림(318-4)
- 횡단 초안 맞춤: 일반 측점 자리(grid)는 종류가 바뀌어도 저장분(옛 관 그림)이 남던 것 → 구조물만 뺀 사본(지형 쪽 고친 값 그대로) + 새 종류
- 시험: test_319_kind_swap 8 · test_318 중심고 겹침 기대값 바꿈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
2026-10-14 06:52:02 +09:00

505 lines
18 KiB
JavaScript

/* PLAN 318 단위검증 헬퍼 — B06 횡단 카드: 단추 줄 빈자리 고름 · 중심고 Y축 상자 · 빈 아래 띠 ·
* 관 종류 바꿈 = 옛 측점 조정값 버림(세션 · 미리 뜬 횡단 · [저장] patch).
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림(서버 · 무거운 화면 모듈은 흉내) · 결과 = 표준출력 JSON 한 벌.
* 짝 = `test_318_cross_card.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 B06 = (name) => path.join(ROOT, "B06_Section", `${name}.ts`);
// ── 화면 그릇 흉내 — 쓰는 만큼만 ─────────────────────────────────────────────
class FakeNode {
constructor(tag) {
this.tagName = String(tag).toLowerCase();
this.children = [];
this.attrs = {};
this.listeners = {};
this.parent = null;
this.dataset = {};
this.style = { setProperty: () => {} };
this._text = "";
this._classes = new Set();
const self = this;
this.classList = {
add: (...names) => names.forEach((n) => self._classes.add(n)),
remove: (...names) => names.forEach((n) => self._classes.delete(n)),
toggle: (name, force) => {
const on = force === undefined ? !self._classes.has(name) : !!force;
on ? self._classes.add(name) : self._classes.delete(name);
return on;
},
contains: (name) => self._classes.has(name),
};
}
set className(value) {
this._classes = new Set(String(value).split(/\s+/).filter(Boolean));
}
get className() {
return [...this._classes].join(" ");
}
set textContent(value) {
this._text = String(value);
this.children = [];
}
get textContent() {
return this._text + this.children.map((c) => c.textContent).join("");
}
get childElementCount() {
return this.children.length;
}
append(...kids) {
kids.forEach((kid) => {
if (typeof kid === "string") {
const text = new FakeNode("#text");
text._text = kid;
kid = text;
}
if (kid.parent) kid.parent.children = kid.parent.children.filter((k) => k !== kid);
kid.parent = this;
this.children.push(kid);
});
}
appendChild(kid) {
this.append(kid);
return kid;
}
prepend(...kids) {
const rest = this.children;
this.children = [];
this.append(...kids, ...rest);
}
replaceChildren(...kids) {
this.children.forEach((k) => (k.parent = null));
this.children = [];
this.append(...kids);
}
remove() {
if (this.parent) this.parent.children = this.parent.children.filter((k) => k !== this);
this.parent = null;
}
setAttribute(name, value) {
this.attrs[name] = String(value);
if (name === "class") this.className = value;
}
getAttribute(name) {
return name === "class" ? this.className : (this.attrs[name] ?? null);
}
removeAttribute(name) {
delete this.attrs[name];
}
hasAttribute(name) {
return name in this.attrs;
}
addEventListener(type, fn) {
(this.listeners[type] ??= []).push(fn);
}
removeEventListener() {}
matches(selector) {
return selector
.split(",")
.map((part) => part.trim())
.some((part) =>
part.startsWith(".") ? this._classes.has(part.slice(1)) : this.tagName === part,
);
}
closest(selector) {
for (let node = this; node; node = node.parent) if (node.matches(selector)) return node;
return null;
}
querySelectorAll(selector) {
const out = [];
const walk = (node) =>
node.children.forEach((kid) => {
if (kid.matches(selector)) out.push(kid);
walk(kid);
});
walk(this);
return out;
}
querySelector(selector) {
return this.querySelectorAll(selector)[0] ?? null;
}
/** 붙기 전 = 0 — 상자 폭은 어림으로 감. */
getComputedTextLength() {
return 0;
}
}
/** 누름 — 눌린 자리에서 위로 올라가며 듣개를 부름(stopPropagation 이면 멈춤). */
function click(target) {
const event = {
type: "click",
target,
stopped: false,
stopPropagation() {
this.stopped = true;
},
preventDefault() {},
};
for (let node = target; node && !event.stopped; node = node.parent)
(node.listeners.click ?? []).forEach((fn) => fn(event));
return event;
}
class FakeStorage {
constructor() {
this.map = new Map();
}
get length() {
return this.map.size;
}
key(index) {
return [...this.map.keys()][index] ?? null;
}
getItem(key) {
return this.map.has(key) ? this.map.get(key) : null;
}
setItem(key, value) {
this.map.set(key, String(value));
}
removeItem(key) {
this.map.delete(key);
}
}
globalThis.window = {
sessionStorage: new FakeStorage(),
localStorage: new FakeStorage(),
addEventListener: () => {},
removeEventListener: () => {},
setTimeout,
clearTimeout,
};
globalThis.sessionStorage = window.sessionStorage;
globalThis.localStorage = window.localStorage;
globalThis.requestAnimationFrame = () => 0;
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
globalThis.document = {
createElement: (tag) => new FakeNode(tag),
createElementNS: (_ns, tag) => new FakeNode(tag),
addEventListener: () => {},
removeEventListener: () => {},
documentElement: new FakeNode("html"),
};
const anything = () =>
new Proxy({}, { get: (_t, key) => (key === "__esModule" ? false : () => undefined) });
/**
* 로더 하나 — `real` 에 든 파일만 진짜로 읽고, 그 밖 상대 경로는 흉내(무거운 화면 사슬을 안 끌어옴).
* `stubs` 는 지정자 그대로 갈음.
*/
function makeLoader(real, stubs = {}) {
const cache = new Map();
const realSet = new Set(real.map((file) => path.resolve(file)));
const load = (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 need = (spec) => {
if (spec in stubs) return stubs[spec];
let target = null;
if (spec.startsWith("@util/")) target = path.join(ROOT, "common_util", `${spec.slice(6)}.ts`);
else if (spec.startsWith(".")) target = path.join(path.dirname(full), `${spec}.ts`);
if (target && realSet.has(path.resolve(target))) return load(target);
return anything();
};
new Function("exports", "module", "require", js)(box.exports, box, need);
return box.exports;
};
return load;
}
const out = {};
// ── 1. 중심고 Y축 상자(318-2) ──────────────────────────────────────────────
{
const load = makeLoader(
[
B06("B06_Section_UI_Cross_Axes"),
B06("B06_Section_UI_Section_Common"),
path.join(ROOT, "common_util", "common_util_svg.ts"),
],
{
"@config/config_frontend": { PIPE_TYPES: [] },
"../B05_Profile/B05_Profile_UI_IrregularStations": { PIPE_DISPLAY_NAME: {} },
},
);
const { createCrossAxes, IDENTITY_VIEW } = load(B06("B06_Section_UI_Cross_Axes"));
const { CROSS_PAD } = load(B06("B06_Section_UI_Section_Common"));
const base = {
widthPx: 480,
heightPx: 300,
pixelsPerMeter: 10,
maxOffset: 20,
displayMax: 890,
elevationMid: 880,
exaggeration: 1,
};
const draw = (centerZ) => {
const layer = new FakeNode("g");
createCrossAxes(layer, { ...base, centerZ })(IDENTITY_VIEW);
return layer;
};
const layer = draw(876.57);
const box = layer.querySelector(".b06-chart__center-box");
const tag = layer.querySelector(".b06-chart__center-text");
const centerY = CROSS_PAD.top + (base.displayMax - 876.57) * base.pixelsPerMeter;
const ticks = layer
.querySelectorAll("text")
.filter(
(t) => !t.classList.contains("b06-chart__center-text") && t.attrs["text-anchor"] === "end",
);
out.center = {
axis_x: CROSS_PAD.left,
box: box && {
x: Number(box.attrs.x),
width: Number(box.attrs.width),
y: Number(box.attrs.y),
height: Number(box.attrs.height),
rx: Number(box.attrs.rx),
},
center_y: centerY,
text: tag?.textContent ?? null,
text_classes: tag?.className ?? null,
text_x: tag ? Number(tag.attrs.x) : null,
tick_x: ticks.length ? Number(ticks[0].attrs.x) : null,
tick_near_center: ticks.filter((t) => Math.abs(Number(t.attrs.y) - 3 - centerY) < 14).length,
tick_count: ticks.length,
on_top: layer.children.slice(-2).map((node) => node.className), // 319-1 상자 · 글 = 맨 뒤(위)
hidden_when_out_of_view: draw(800).querySelector(".b06-chart__center-box") === null,
absent_without_center: draw(null).querySelector(".b06-chart__center-box") === null,
};
}
// ── 2. 아래 띠(318-2 · 318-3) — 중심고 글 없음 · 암 경계 맨 왼쪽 · 토사도 띠 남음 ─────
{
const control = (cls) => () => {
const node = new FakeNode("div");
node.className = cls;
return node;
};
const load = makeLoader([B06("B06_Section_UI_Cross_Card_Chrome")], {
"./B06_Section_UI_Cross_CutSlope": { buildCutSlopeControl: control("cutslope") },
"./B06_Section_UI_Cross_Design": {
buildRockBoundaryControl: control("rockb"),
buildDesignControls: () => ({ bar: new FakeNode("div"), groundSegment: new FakeNode("div") }),
sectionModeLabel: () => "",
},
"./B06_Section_UI_Cross_Fit": { fillSlopeLengths: () => [] },
"./B06_Section_UI_Section_Common": {
L: (key) => (key === "B06_Profile_View_CenterElevation" ? "중심고" : key),
stationLabel: () => "",
structureDisplayName: () => "",
},
});
const { appendCardFooter } = load(B06("B06_Section_UI_Cross_Card_Chrome"));
const footerOf = (preset) => {
const card = new FakeNode("article");
appendCardFooter(card, { center_z: 876.57, design: { geometry_preset: preset } }, {}, {});
return card.querySelector("footer");
};
const rock = footerOf("rock");
const soil = footerOf("soil");
out.footer = {
rock_first: rock?.children[0]?.className ?? null,
rock_classes: rock?.children.map((kid) => kid.className) ?? null,
rock_text: rock?.textContent ?? null,
soil_exists: soil !== null,
soil_children: soil?.childElementCount ?? null,
};
}
// ── 3. 단추 줄(318-1) — 빈자리 누름 = 카드 고름 · 조작 누름 = 안 넘김 ───────────
{
const load = makeLoader([B06("B06_Section_UI_Cross_Design")], {
"@ui/ui_template_locale": {
currentLanguageIndex: 0,
ui_locales: new Proxy({}, { get: (_t, key) => [String(key)] }),
},
});
const { buildDesignControls } = load(B06("B06_Section_UI_Cross_Design"));
const { bar } = buildDesignControls(
{
chainage_m: 100,
uphill_side: "left",
design: { ground_type: "soil", section_mode: "left_cut" },
},
() => {},
);
const card = new FakeNode("article");
let picked = 0;
card.addEventListener("click", () => (picked += 1));
card.append(bar);
const pickedBy = (node) => {
const before = picked;
click(node);
return picked - before;
};
const button = bar.querySelector("button");
const label =
bar.querySelectorAll("div").find((node) => !node.closest("button, details")) ?? null;
const summary = bar.querySelector("summary");
out.bar = {
empty: pickedBy(bar),
button: button ? pickedBy(button) : null,
legend: label ? pickedBy(label) : null,
summary: summary ? pickedBy(summary) : null,
};
}
// ── 4. 종류 바꿈 = 그 측점 세션 조정값 버림 · 비울 자리 적음(318-4) ─────────────
{
const previewCalls = [];
const load = makeLoader(
[
B06("B06_Section_Api_Culvert_Options"),
path.join(ROOT, "A00_Common", "b_page_state.ts"),
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
],
{
"./B06_Section_Section_Drafts": {
forgetPreviewAdjusts: (...args) => previewCalls.push([args[0], args[1], [...args[2]]]),
},
},
);
const mod = load(B06("B06_Section_Api_Culvert_Options"));
const store = window.sessionStorage;
const put = (key, value) => store.setItem(key, JSON.stringify(value));
const get = (key) => JSON.parse(store.getItem(key) ?? "null");
put("aislo:draft:culvertopt:P:7", { "100.00": { inlet_revet_length_m: 3 }, "140.00": { a: 1 } });
put("aislo:draft:culvertopt:P:8", { "100.00": { a: 1 } });
put("aislo:draft:culvertopt:Q:7", { "100.00": { a: 1 } });
put("aislo:draft:revetx:P:7", {
"100.00:inlet": { x: 1 },
"100.00:extra0": { x: 2 },
"140.00:inlet": { x: 3 },
});
put("aislo:draft:extrawall:P:7", { "100.00": 2, "b100.00": 1, "140.00": 1 });
put("aislo:draft:extraspan:P:7", { "100.00:extra0": { length_m: 1 } });
put("aislo:draft:revetlink:P:7", {
detached: { "100.00": true, "140.00": true },
grade: { "100.00": true },
});
put("aislo:draft:fordadjust:P:7", { "100.00": { w: 1 } });
put("aislo:draft:boxadjust:P:7", { "100.30": { w: 1 } });
put("aislo:draft:basinadjust:P:7", { "100.00": { d: 1 } });
put("aislo:draft:inletstruct:P:7", { "100.00": "basin" });
put("aislo:draft:crossw:P:7", { "100.00": 5 });
let reloads = 0;
mod.setStationAdjustReload(() => (reloads += 1));
mod.forgetStationAdjusts("P", 100, "pipe", "box_culvert");
out.forget = {
culvertopt: get("aislo:draft:culvertopt:P:7"),
culvertopt_other_route: get("aislo:draft:culvertopt:P:8"),
culvertopt_other_project: get("aislo:draft:culvertopt:Q:7"),
revetx: get("aislo:draft:revetx:P:7"),
extrawall: get("aislo:draft:extrawall:P:7"),
extraspan: get("aislo:draft:extraspan:P:7"),
revetlink: get("aislo:draft:revetlink:P:7"),
fordadjust: get("aislo:draft:fordadjust:P:7"),
boxadjust: get("aislo:draft:boxadjust:P:7"),
basinadjust: get("aislo:draft:basinadjust:P:7"),
inletstruct: get("aislo:draft:inletstruct:P:7"),
crossw: get("aislo:draft:crossw:P:7"),
kindreset: get("aislo:draft:kindreset:P"),
changed: mod.kindChangedChainages("P"),
preview_calls: previewCalls,
reloads,
};
mod.forgetStationAdjusts("P", 100, "box_culvert", "ford_bridge");
out.forget.second = get("aislo:draft:kindreset:P");
mod.forgetStationAdjusts("P", 100, "ford_bridge", "pipe");
out.forget.reverted = get("aislo:draft:kindreset:P");
const before = store.length;
mod.forgetStationAdjusts("P", 140, "pipe", "pipe");
out.forget.same_kind_untouched =
store.length === before && get("aislo:draft:culvertopt:P:7")["140.00"] !== undefined;
out.forget.keys = [...mod.STRUCTURE_ADJUST_KEYS];
}
// ── 5. 미리 뜬 횡단 사본 걷음(318-4) — 그 자리 조정값만 · 다른 칸 그대로 ─────────
const state = { draftPipes: null, savedPipes: [] };
const loadDrafts = makeLoader(
[
B06("B06_Section_Section_Drafts"),
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
path.join(ROOT, "common_util", "common_util_culvert_sets.ts"),
],
{
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
fetchDetailPipePoints: async () => ({ pipe_points: state.savedPipes }),
},
"../B05_Profile/B05_Profile_Api_Structures": {
fetchStructures: async () => ({ structures: [] }),
fetchStructureTypes: async () => [],
readPendingStructures: () => null,
},
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
"./B06_Section_Api_Fetch": {
previewStructureSections: async (_p, chainages) =>
chainages.map((at) => ({
chainage_m: at,
kind: "irregular",
design: { ground_type: "soil", ford_adjust: { w: 1 }, revet_adjust: { inlet: { x: 1 } } },
})),
},
},
);
// ── 6. B05 [저장](캐시 patch) — 종류 바꾼 측점 = structure_reset · 옛 조정값 안 실음 ─────
const loadStore = makeLoader(
[
B06("B06_Section_Section_Store"),
B06("B06_Section_Api_Culvert_Options"),
path.join(ROOT, "A00_Common", "b_page_state.ts"),
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
],
{
"./B06_Section_Cross_Refresh": {
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
},
},
);
(async () => {
const { withDraftStations, forgetPreviewAdjusts } = loadDrafts(B06("B06_Section_Section_Drafts"));
const detail = { cross_sections: [{ chainage_m: 0, kind: "regular", design: {} }] };
state.draftPipes = [{ chainage_m: 50, facility: "box_culvert" }];
await withDraftStations(detail, "P", 7);
const at50 = () => detail.cross_sections.find((s) => Math.abs(s.chainage_m - 50) < 0.01);
const had = Object.keys(at50()?.design ?? {}).sort();
forgetPreviewAdjusts("P", 50, ["ford_adjust", "revet_adjust"]);
forgetPreviewAdjusts("OTHER", 0, ["ground_type"]);
out.preview = { had, after: Object.keys(at50()?.design ?? {}).sort() };
const { crossPatchesFromCache } = loadStore(B06("B06_Section_Section_Store"));
out.cache_patches = crossPatchesFromCache(
{
cross_sections: [
{ chainage_m: 100, design: { ford_adjust: { w: 1 }, display_half_width_m: 6 } },
{ chainage_m: 140, design: { ford_adjust: { w: 2 } } },
],
},
[100],
);
out.cache_patches_plain = crossPatchesFromCache({
cross_sections: [{ chainage_m: 140, design: { ford_adjust: { w: 2 } } }],
});
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});