Files
Aislo/resources/tester/helper_322_0_golden.cjs
T
eomsangdonandClaude Opus 5.5 dc74627c7d merge(472-3): origin/main_laptop_1(518-1 B05 코리도 logic) 합침 · 518-3 import 줄과 양쪽 살림 · 세션 끌림 빈 목록
- 충돌 6 = 같은 import 줄 — B05 logic 새 자리(sub5) + B06 logic 새 경로(518-3)
- test_472_3 SESSION_PULLS 빈 목록 — 측점 간격 common_util_station 으로 ui_template_locale 끌림 끊김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:30:22 +09:00

352 lines
14 KiB
JavaScript

/* PLAN 322-0 전수 표 시험 헬퍼 — 42종 × 견본(구조물 · 관 지점 · 빈칸)마다 지금 화면 함수 결과를 표준출력 JSON 한 벌로.
* 측점(`structureStationMarks` · `pipeStationMarks`) · 카드 이름 · 형상 열쇠(`Section_Store` 초안 얹기 → 미리 뜬 횡단 ·
* 벽 · 노면 · 맹암거) · 3D 몸체(`buildCorridorStructures`) · 알약 띠(`buildStructureLane`) · 고르기 목록(B05 · B06 `pickGroups`).
* 입력 = argv[2] JSON(서버 꼴 types · 정의 · 견본 · 격자 · 맨 횡단) — 파이썬 짝이 만듦. 짝 = `test_322_0_golden.py`.
* 진짜 TS 를 그 자리에서 트랜스파일해 Node 로 돌림(서버 부르는 곳 · 무거운 화면 모듈만 흉내).
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const clone = (v) => JSON.parse(JSON.stringify(v));
/* ── 브라우저 흉내 ─────────────────────────────────────────────────────── */
class FakeStorage {
constructor() {
this.map = new Map();
}
get length() {
return this.map.size;
}
key(i) {
return [...this.map.keys()][i] ?? null;
}
getItem(k) {
return this.map.has(k) ? this.map.get(k) : null;
}
setItem(k, v) {
this.map.set(k, String(v));
}
removeItem(k) {
this.map.delete(k);
}
clear() {
this.map.clear();
}
}
class FakeElement {
constructor(tag) {
this.tagName = tag;
this.className = "";
this.style = {};
this.dataset = {};
this.children = [];
this.textContent = "";
this.classList = { toggle: () => {}, add: () => {}, remove: () => {}, contains: () => false };
}
append(...nodes) {
this.children.push(...nodes);
}
prepend(...nodes) {
this.children.unshift(...nodes);
}
appendChild(node) {
this.children.push(node);
return node;
}
addEventListener() {}
setAttribute() {}
}
globalThis.window = {
sessionStorage: new FakeStorage(),
localStorage: new FakeStorage(),
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
requestAnimationFrame: (fn) => fn(),
setTimeout,
clearTimeout,
location: { origin: "http://localhost", pathname: "/" },
};
globalThis.sessionStorage = window.sessionStorage;
globalThis.localStorage = window.localStorage;
globalThis.requestAnimationFrame = (fn) => fn();
const docListeners = new Map();
globalThis.document = {
activeElement: null,
hidden: false,
visibilityState: "visible",
addEventListener: (type, fn) => docListeners.set(type, fn),
removeEventListener: (type, fn) => docListeners.get(type) === fn && docListeners.delete(type),
createElement: (tag) => new FakeElement(tag),
};
/** 서버 흉내 — 정의 판(`server.version`) · 종류 받은 횟수(`server.typeCalls`). */
const server = { version: input.definition.판, typeCalls: 0 };
globalThis.fetch = async (url) => {
const at = String(url);
if (at.includes("/structure-types")) server.typeCalls += 1;
const body = at.includes("structure-definition/version")
? { 판: server.version }
: at.includes("structure-definition")
? { ...clone(input.definition), 판: server.version }
: at.includes("/route/structures")
? { status: "success", project_id: PROJECT, revision: 0, structures: [] }
: { status: "success", schema_version: 1, types: clone(input.types) };
return { ok: true, status: 200, json: async () => body };
};
/* ── TS 싣기 — 흉내 파일만 갈음 ──────────────────────────────────────────── */
const file = (...parts) => path.resolve(ROOT, ...parts);
const B05 = (name) => file("B05_Profile", `${name}.ts`);
const B06 = (name) => file("B06_Section", `${name}.ts`);
const PROJECT = "P322";
let pendingPipes = null;
const STUBS = new Map([
[
B06("B06_Section_Api_Fetch"),
{
fetchSectionDetail: async () => currentDetail(),
saveSections: async () => ({}),
previewStructureSections: async (_p, chainages) =>
chainages.map((at) => ({ ...plainAt(at), kind: "irregular" })),
},
],
[B06("B06_Section_Cross_Refresh"), { USER_TOUCHED_KEYS: [] }],
[
B06("B06_Section_Api_Culvert_Options"),
{ kindChangedChainages: () => [], STRUCTURE_ADJUST_KEYS: [] },
],
[
B06("logic/B06_Section_Structure_Layouts"),
{ applyStructureAreaRows: () => {}, structureAreaRows: () => [] },
],
[
B05("B05_Profile_Api_Pipes_Draft"),
{ readPendingPipes: () => pendingPipes, writePendingPipes: () => {} },
],
[
file("B04_PreProcess", "B04_PreProcess_Api_Fetch.ts"),
{ fetchDetailPipePoints: async () => ({ pipe_points: [] }), noteSnapshotFailed: () => {} },
],
[file("A00_Common", "touch_bridge.ts"), { pickPx: () => 6 }],
[file("ui_template", "ui_template_toast.ts"), { showToast: () => {} }],
[file("ui_template", "ui_template_elements.ts"), { showToast: () => {} }],
]);
const resolveFile = (base) =>
[base, `${base}.ts`, base.replace(/\.js$/, ".ts"), path.join(base, "index.ts")].find(
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
) ?? `${base}.ts`;
const { loadTs } = makeLoader({
root: ROOT,
interop: true,
fileStub: (full) => STUBS.get(full) ?? (full.endsWith(".css") ? {} : undefined), // .css = 화면 꼴(vite 몫)
requireFor: (full) => (spec) => {
if (spec === "three") return require(path.join(ROOT, "config", "node_modules", "three"));
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
for (const [prefix, dir] of Object.entries(alias))
if (spec.startsWith(prefix))
return loadTs(resolveFile(path.join(ROOT, dir, spec.slice(prefix.length))));
if (spec.startsWith(".")) return loadTs(resolveFile(path.join(path.dirname(full), spec)));
return require(spec);
},
});
/* ── 견본 횡단 ───────────────────────────────────────────────────────────── */
const plainAt = (at) => ({ ...clone(input.plain_section), chainage_m: at, kind: "regular" });
const currentDetail = () => ({
longitudinal: { stations: input.grid.map((at) => ({ chainage_m: at, kind: "regular" })) },
cross_sections: input.grid.map(plainAt),
});
const frameAtOf = (sections) => (atM) => {
const s = sections.reduce((b, x) =>
Math.abs(x.chainage_m - atM) < Math.abs(b.chainage_m - atM) ? x : b,
);
const [lx, ly] = s.frame.left_xy;
const along = atM - s.chainage_m;
return {
cx: s.center_x - ly * along,
cy: s.center_y + lx * along,
leftX: lx,
leftY: ly,
designZ: s.design?.design_elevation_m ?? null,
};
};
/** 횡단 하나에 얹힌 구조물 그림 자료 — 열쇠 + 고르는 데 쓰는 값만(파이썬 짝 `_shape_of` 와 같은 꼴). */
const SHAPE_KEYS = [
"culvert",
"ford",
"box",
"ford_pavement",
"revetment",
"pavement",
"underdrain",
];
function shapeOf(section) {
const out = {};
for (const key of SHAPE_KEYS) {
const spec = section[key];
if (!spec) continue;
if (key === "culvert") out[key] = { hidden_pipe: !!spec.hidden_pipe, body: !!spec.body };
else if (key === "revetment")
out[key] = { type_id: spec.type_id ?? null, form: spec.form ?? null };
else if (key === "pavement") out[key] = spec.map((p) => p.type_id).sort();
else out[key] = true;
}
return out;
}
const at2 = (n) => Number(n).toFixed(2);
const { structureStationMarks, pipeStationMarks } = loadTs(
file("common_util", "common_util_structure_station.ts"),
);
const {
dropStaleStructureTypes,
fetchStructureTypes,
onStaleStructureTypes,
pipesToStructureMarks,
staleDefinitionNotice,
writePendingStructures,
} = loadTs(B05("B05_Profile_Api_Structures"));
const { loadSectionDetail } = loadTs(B06("B06_Section_Section_Store"));
const { forgetDraftStations } = loadTs(B06("B06_Section_Section_Drafts"));
const { buildCorridorStructures } = loadTs(B05("logic/B05_Profile_Corridor_Structures"));
const { computeRevetmentLayout } = loadTs(B06("logic/B06_Section_Cross_Revetment_Geom"));
const { buildStructureLane } = loadTs(B05("B05_Profile_UI_Structures_Marks"));
const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types"));
const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common"));
const { listPick } = loadTs(file("common_util", "common_util_structure_catalog.ts"));
/** 415 M4 — 새 자리 = 계산 묶음 측점 갈아끼움 흉내(측점 계획 자리 중 상세에 없는 것 · 서버 안 물음). */
function swapIn(detail, types) {
const plan = loadTs(file("common_util", "common_util_structure_station.ts"));
const { registryDefaults } = loadTs(file("common_util", "common_util_culvert_sets.ts"));
const structures = JSON.parse(
window.sessionStorage.getItem(`aislo:draft:structures:${PROJECT}`) ?? "null",
);
const planned = plan.planStations(
pendingPipes ?? [],
structures ?? [],
types,
registryDefaults(types),
);
const near = (a, b) => Math.abs(a - b) < plan.SAME_STATION_M;
for (const { chainage_m: at } of planned)
if (detail.cross_sections.every((s) => !near(s.chainage_m, at)))
detail.cross_sections.push({ ...plainAt(at), kind: "irregular" });
detail.cross_sections.sort((a, b) => a.chainage_m - b.chainage_m);
}
async function main() {
const types = await fetchStructureTypes(true);
const byId = new Map(types.map((type) => [type.type_id, type]));
const out = { samples: {}, pick: {} };
// 고르기 목록 — 페이지가 넘기는 거르기 그대로(B05 = 켜진 것 · B06 = 전부)
const pagePicks = { b05: listPick("b05"), b06: listPick("b06") }; // 페이지 거르기 = 표 목록(322-1)
for (const [page, pick] of Object.entries(pagePicks)) {
const seen = {};
for (const group of pickGroups(types, pick))
for (const member of group.types)
seen[member.type.type_id] = group.hidden || member.hidden ? "hidden" : "shown";
for (const type of types)
(out.pick[type.type_id] ??= {})[page] = seen[type.type_id] ?? "absent";
}
let route = 0;
for (const sample of input.samples) {
route += 1;
const type = byId.get(sample.type_id);
const row = {};
// 측점
const marks = sample.pipe
? pipeStationMarks(sample.pipe)
: structureStationMarks(sample.structure, type);
row.station = marks.map((m) => `${m.role}@${at2(m.chainage_m)}`);
// 초안 얹기(Section_Store) → 카드 이름 · 형상 열쇠
window.sessionStorage.clear();
forgetDraftStations(PROJECT);
pendingPipes = sample.pipe ? [clone(sample.pipe)] : null;
writePendingStructures(PROJECT, sample.structure ? [clone(sample.structure)] : null);
swapIn(await loadSectionDetail(PROJECT, route, { force: true }), types);
const detail = await loadSectionDetail(PROJECT, route); // 갈아끼움 뒤 다시 그림 = 캐시 다시 맞춤
const touched = detail.cross_sections.filter(
(section) => section.structure || Object.keys(shapeOf(section)).length,
);
row.cards = Object.fromEntries(
touched.map((section) => [
at2(section.chainage_m),
{
title: section.structure ?? null,
display: section.structure ? structureDisplayName(section.structure) : null,
shape: shapeOf(section),
},
]),
);
// 벽 물빼기(옹벽만 · 형상칸.weep)
row.weep = touched
.filter((section) => section.revetment)
.some((section) =>
(computeRevetmentLayout(section, undefined)?.walls ?? []).some((wall) => wall.retaining),
);
// 3D 몸체
const solids = touched.length ? buildCorridorStructures(touched, frameAtOf(touched)) : [];
row.model3d = [...new Set(solids.map((s) => `${s.kind}:${s.key ?? ""}`))].sort();
// 알약 띠
const lane = buildStructureLane({
structures: sample.pipe
? pipesToStructureMarks([sample.pipe])
: [{ ...sample.structure, structure_id: "s1" }],
types,
x: (m) => m,
chainageAt: (px) => px,
maxChainageM: 400,
widthPx: 400,
axisWidthPx: 40,
stationIntervalM: 20,
selectedId: null,
onSelect: () => {},
});
row.band = lane.children.filter((el) => el.className.startsWith("b05-structure__band")).length;
out.samples[`${sample.type_id}/${sample.case}`] = row;
}
// 판 확인(322-0 ③) — 같은 판 = 캐시 그대로 · 바뀐 판 = 버리고 다시 받음 · 탭이 다시 보이면 알림
await fetchStructureTypes(true);
server.typeCalls = 0;
const same = await dropStaleStructureTypes();
await fetchStructureTypes(true);
const sameCalls = server.typeCalls;
let reloads = 0;
const off = onStaleStructureTypes(() => (reloads += 1));
server.version += 1;
docListeners.get("visibilitychange")?.();
await new Promise((resolve) => setTimeout(resolve, 0));
await fetchStructureTypes(true);
const staleCalls = server.typeCalls - sameCalls;
off();
// 판 알림(322-2) — 프로젝트 정본이 옛 판으로 적혔으면 한 번만 · 같은 판 · 판 없음은 안 알림
const old = server.version - 1;
out.notice = [
staleDefinitionNotice(PROJECT, old),
staleDefinitionNotice(PROJECT, old),
staleDefinitionNotice(PROJECT, server.version),
staleDefinitionNotice(PROJECT, null),
];
out.version = {
same,
sameCalls,
reloads,
staleCalls,
removed: !docListeners.has("visibilitychange"),
};
process.stdout.write(JSON.stringify(out));
}
main().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});