Files
Aislo/resources/tester/helper_130_b07_plan_manage.cjs
T

374 lines
13 KiB
JavaScript
Raw 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.
/* 130-1 [평면도 관리] 모달 헬퍼 — 좌측 「평면도」 묶음 · 모달(종류 추가 · 업데이트 · 지우기 · 장 미리보기)을
* Node 로 돌려 표준출력 JSON 한 벌로. TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 ·
* 서버 길(`_Api_Fetch`)은 메모리 가짜 · 공용 모달 · CAD · 드롭다운 · 체크박스는 기록용 가짜.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const B07 = path.join(ROOT, "B07_DesignDetail");
const cache = new Map();
// ── 화면 그릇 흉내 ─────────────────────────────────────────────────────────────
class FakeNode {
constructor(tag) {
this.tagName = tag.toUpperCase();
this.children = [];
this.dataset = {};
this.listeners = {};
this._text = "";
this._classes = new Set();
this.value = "";
this.hidden = false;
const self = this;
this.classList = {
add: (...names) => names.forEach((n) => self._classes.add(n)),
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("");
}
append(...kids) {
kids.forEach((k) => this.children.push(k));
}
replaceChildren(...kids) {
this.children = [...kids];
}
addEventListener(type, handler) {
(this.listeners[type] ||= []).push(handler);
}
click() {
(this.listeners.click || []).forEach((handler) => handler({}));
}
/** `.a` · `.a .b`(자손) 만. */
querySelectorAll(selector) {
const parts = selector
.trim()
.split(/\s+/)
.map((p) => p.slice(1));
const found = [];
const walk = (node, depth) =>
node.children.forEach((kid) => {
let next = depth;
if (kid.classList && kid.classList.contains(parts[depth])) {
if (depth === parts.length - 1) found.push(kid);
else next = depth + 1;
}
walk(kid, next);
});
walk(this, 0);
return found;
}
querySelector(selector) {
return this.querySelectorAll(selector)[0] ?? null;
}
}
const all = (node) => [node, ...node.children.flatMap(all)];
const byText = (node, tag, text) =>
all(node).filter((n) => n.tagName === tag && n.textContent === text);
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
const confirms = [];
globalThis.window = { confirm: (message) => (confirms.push(message), true) };
// ── 공용 부품 가짜 ─────────────────────────────────────────────────────────────
const toasts = [];
const modals = [];
const cads = [];
const selects = [];
const STUBS = {
"@ui/ui_template_elements": {
showToast: (message, kind) => toasts.push([kind, message]),
createSelectField: (opts) => {
const root = new FakeNode("select");
const select = { value: opts.options[0]?.value ?? "", disabled: false };
const field = {
root,
select,
label: opts.ariaLabel,
change: (value) => ((select.value = value), opts.onChange && opts.onChange()),
setOptions: (options, value) => (select.value = value ?? options[0]?.value ?? ""),
};
selects.push(field);
return field;
},
},
"@ui/ui_template_checkbox": {
createCheckbox: (label, opts = {}) => {
const root = new FakeNode("label");
root.textContent = label;
return { root, input: { value: opts.value ?? "", checked: !!opts.checked } };
},
},
"@ui/ui_template_modal": {
openModal: (opts) => {
const body = new FakeNode("div");
const modal = { opts, body, open: true };
modals.push(modal);
opts.mount(body, () => (modal.open = false));
return { close: () => (modal.open = false) };
},
},
"@ui/cad_host/cad_host": {
createCadHost: (opts) => {
const cad = { opts, element: new FakeNode("div"), loads: [], destroyed: false };
cad.beginLoading = () => {};
cad.fail = (detail) => toasts.push(["error", detail]);
cad.load = (drawing, meta, frameEdit, fields, extra) =>
cad.loads.push({ drawing, meta, extra });
cad.destroy = () => (cad.destroyed = true);
cads.push(cad);
return cad;
},
},
"@ui/ui_template_collapsible": { attachCollapsible: () => {} },
"@ui/ui_template_locale": {
currentLanguageIndex: 0,
ui_locales: new Proxy({}, { get: (_, key) => [String(key)] }),
},
"../B05_Profile/B05_Profile_UI_Structures_Fields": {
stationFields: () => ({ wrap: new FakeNode("div"), write: () => {}, read: () => 0 }),
},
};
// ── 서버 길 가짜 — 종류 파일 · 장 목록이 메모리에 ───────────────────────────────
const calls = [];
const state = {
pieces: [
{ id: "terrain", name: "지형" },
{ id: "route", name: "노선" },
],
scales: [1000, 1200],
default_scale: 1200,
watershed_scale: 6000,
kinds: [
{ id: "k_terrain", name: "지형", pieces: ["terrain"], layout: "plan", scale: 1200 },
{ id: "k_route", name: "노선배치도", pieces: ["route"], layout: "plan", scale: 1200 },
],
marks: [],
station_interval_m: 20,
};
let confirmed = new Set(["plan_k_route"]);
/** 종류 하나 = 장 하나 · 「지형」만 두 장. */
const sheets = () =>
state.kinds.flatMap((kind) =>
(kind.id === "k_terrain" ? [1, 2] : [1]).map((n) => {
const id = n === 1 ? `plan_${kind.id}` : `plan_${kind.id}_${n}`;
return {
id,
kind: "plan",
label: kind.name,
chainage_m: null,
confirmed: confirmed.has(id),
plan_kind: kind.id,
plan_kind_name: kind.name,
};
}),
);
const copy = () => JSON.parse(JSON.stringify(state));
const API = {
fetchPlanKinds: async () => copy(),
fetchDesignDrawingList: async () => ({ drawings: [{ id: "cover", kind: "cover" }, ...sheets()] }),
fetchDesignDrawing: async (_, id) => (
calls.push(["fetchDrawing", id]),
{ id, confirmed: confirmed.has(id), drawing: { entities: [{ id }], layers: [] } }
),
addPlanKind: async (_, name, pieces, layout, scale) => {
calls.push(["add", name, pieces, layout, scale]);
state.kinds.push({ id: "k_new", name, pieces, layout, scale });
return copy();
},
updatePlanKind: async (_, id, name, pieces, scale) => {
calls.push(["update", id, name, pieces, scale]);
Object.assign(
state.kinds.find((k) => k.id === id),
{ name, pieces, scale },
);
return copy();
},
deletePlanKind: async (_, id) => {
calls.push(["delete", id]);
state.kinds = state.kinds.filter((k) => k.id !== id);
return copy();
},
savePlanMarks: async () => copy(),
invalidateDesignDrawing: async (_, id) => {
calls.push(["invalidate", id]);
confirmed.delete(id);
},
};
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 === "./B07_DesignDetail_Api_Fetch") return API;
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
throw new Error(`stub 없음: ${request}`);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const settle = () => new Promise((done) => setTimeout(done, 0));
const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Panels.ts"));
(async () => {
const out = {};
const closes = [];
const selected = [];
const plan = { projectId: "p1", onClosed: (changed, openId) => closes.push([changed, openId]) };
const panel = buildDrawingSidePanel(
[{ id: "cover", kind: "cover", label: "표지", confirmed: false }, ...sheets()],
(drawing) => selected.push(drawing.id),
undefined,
false,
plan,
);
const section = panel.querySelector(".b07-plan");
out.left = {
manage: byText(section, "BUTTON", "평면도 관리").length,
sheetButtons: section.querySelectorAll(".b07-drawing-button").map((b) => b.dataset.drawingId),
formFields: all(section).filter((n) => ["SELECT", "INPUT", "LABEL"].includes(n.tagName)).length,
remove: byText(section, "BUTTON", "×").length,
texts: section.textContent,
};
// 단추 → 모달
byText(section, "BUTTON", "평면도 관리")[0].click();
await settle();
const modal = modals[0];
const cad = cads[0];
const kindRows = () =>
modal.body
.querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head")
.map((row) => row.dataset.planKind);
const sheetIds = () =>
modal.body
.querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button")
.map((b) => b.dataset.drawingId);
const kindPick = () => selects.filter((s) => s.label === "종류").at(-1);
const action = () =>
all(modal.body).find(
(n) => n.tagName === "BUTTON" && ["추가", "업데이트"].includes(n.textContent),
);
out.open = {
title: modal.opts.title,
dialogClass: modal.opts.dialogClass,
kinds: kindRows(),
sheets: sheetIds(),
hasKindForm: !!kindPick(),
hasMarks: all(modal.body).some((n) => n.textContent === "대피소 · 사토장 자리"),
preview: cad.loads.at(-1).drawing.entities[0].id,
extra: cad.loads.at(-1).extra,
};
// 종류 추가 — 「새 종류」 · 이름 · 조각 하나
kindPick().change("");
all(modal.body).find((n) => n.tagName === "INPUT" && n.placeholder === "종류 이름").value =
"sub6임시";
action().click();
await settle();
await settle();
out.add = { kinds: kindRows(), sheets: sheetIds(), call: calls.find((c) => c[0] === "add") };
// 업데이트 — 새 종류를 골라 이름 · 축척 바꿈
kindPick().change("k_new");
out.updateLabel = action().textContent;
all(modal.body).find((n) => n.tagName === "INPUT" && n.placeholder === "종류 이름").value =
"sub6임시2";
selects.filter((s) => s.label === "축척").at(-1).select.value = "1000";
action().click();
await settle();
await settle();
out.update = {
call: calls.find((c) => c[0] === "update"),
rowText: modal.body
.querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head")
.find((row) => row.dataset.planKind === "k_new").textContent,
};
// 지우기 — 그 줄의 ×
const row = modal.body
.querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head")
.find((r) => r.dataset.planKind === "k_new");
byText(row, "BUTTON", "×")[0].click();
await settle();
await settle();
out.remove = {
kinds: kindRows(),
call: calls.find((c) => c[0] === "delete"),
confirms: confirms.length,
};
// 미리보기 장 바꾸기 — 「지형 2장」 → 「노선배치도」(확정 → [확정 풀기])
const pickSheet = async (id) => {
modal.body
.querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button")
.find((b) => b.dataset.drawingId === id)
.click();
await settle();
};
await pickSheet("plan_k_terrain_2");
const head = () => modal.body.querySelector(".b07-plan-manage__head");
out.sheet2 = {
preview: cad.loads.at(-1).drawing.entities[0].id,
head: head().textContent,
active: modal.body
.querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button")
.filter((b) => b.dataset.active === "true")
.map((b) => b.dataset.drawingId),
};
await pickSheet("plan_k_route");
out.routeHead = head().textContent;
byText(head(), "BUTTON", "확정 풀기")[0].click();
await settle();
await settle();
out.release = {
call: calls.find((c) => c[0] === "invalidate"),
head: head().textContent,
preview: cad.loads.at(-1).drawing.entities[0].id,
};
// 닫기 — 고친 게 있으니 changed = true · 미리보기 듣기 멈춤
modal.opts.beforeClose();
out.close = { closes: [...closes], destroyed: cad.destroyed, open: modal.open };
// 다시 열어 고치지 않고 [CAD 에서 열기] → changed = false · 그 장 id
byText(section, "BUTTON", "평면도 관리")[0].click();
await settle();
await settle();
const modal2 = modals[1];
modal2.body
.querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button")
.find((b) => b.dataset.drawingId === "plan_k_route")
.click();
await settle();
byText(modal2.body.querySelector(".b07-plan-manage__head"), "BUTTON", "CAD 에서 열기")[0].click();
out.openInCad = { closes: closes.slice(1), destroyed: cads[1].destroyed, open: modal2.open };
out.toasts = toasts;
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error && error.stack ? error.stack : error));
process.exit(1);
});