Files
Aislo/resources/tester/helper_322_0_golden.cjs
T
eomsangdonandClaude Opus 5.5 3abd2bdfd4 feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림
- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로]
- 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만
- 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만)
- 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참
- 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만
- 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름
- 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번
- 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 15:40:38 +09:00

348 lines
13 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 ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
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("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 cache = new Map();
function loadTs(target) {
const full = path.resolve(target);
if (STUBS.has(full)) return STUBS.get(full);
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 need = (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);
};
new Function("exports", "module", "require", js)(box.exports, box, need);
return box.exports;
}
/* ── 견본 횡단 ───────────────────────────────────────────────────────────── */
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("B05_Profile_UI_Corridor_Structures"));
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment"));
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"));
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);
const detail = await loadSectionDetail(PROJECT, route, { force: true });
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);
});