Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
257 lines
8.9 KiB
JavaScript
257 lines
8.9 KiB
JavaScript
/* 311 좌측 패널 기본 접힘 헬퍼 — B05 「계획노선」 · B06 「횡단 보기 설정」(한 벌 패널 `design_panel`)과
|
|
* B07 도면 목록(평면도 · 종단도 · 횡단도)을 가짜 DOM 위에 세워 첫 그림 접힘 · 편 뒤 다시 그림 유지를 본다.
|
|
* 결과는 JSON 한 벌. 꼴은 `helper_design_view_parity.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();
|
|
|
|
// ── 화면 그릇 흉내 — 요소 · 클래스 · 속성 · 글자 · 자식 ──────────────────────────
|
|
class FakeNode {
|
|
constructor(tag) {
|
|
this.tagName = tag;
|
|
this.children = [];
|
|
this.parent = null;
|
|
this.dataset = {};
|
|
this.style = {};
|
|
this.attrs = {};
|
|
this._text = "";
|
|
this._classes = new Set();
|
|
this.hidden = false;
|
|
this.disabled = false;
|
|
this.title = "";
|
|
this.listeners = {};
|
|
const self = this;
|
|
this.classList = {
|
|
add: (...n) => n.forEach((x) => self._classes.add(x)),
|
|
remove: (...n) => n.forEach((x) => self._classes.delete(x)),
|
|
toggle: (n, f) => {
|
|
const on = f === undefined ? !self._classes.has(n) : !!f;
|
|
on ? self._classes.add(n) : self._classes.delete(n);
|
|
return on;
|
|
},
|
|
contains: (n) => self._classes.has(n),
|
|
};
|
|
}
|
|
set className(v) {
|
|
this._classes = new Set(String(v).split(/\s+/).filter(Boolean));
|
|
}
|
|
get className() {
|
|
return [...this._classes].join(" ");
|
|
}
|
|
set textContent(v) {
|
|
this._text = String(v);
|
|
this.children.forEach((c) => (c.parent = null));
|
|
this.children = [];
|
|
}
|
|
get textContent() {
|
|
return this._text + this.children.map((c) => c.textContent).join("");
|
|
}
|
|
_adopt(kid) {
|
|
const node =
|
|
typeof kid === "string" ? Object.assign(new FakeNode("#text"), { _text: kid }) : kid;
|
|
if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node);
|
|
node.parent = this;
|
|
return node;
|
|
}
|
|
append(...kids) {
|
|
kids.forEach((k) => this.children.push(this._adopt(k)));
|
|
}
|
|
prepend(...kids) {
|
|
this.children.unshift(...kids.map((k) => this._adopt(k)));
|
|
}
|
|
insertBefore(node, ref) {
|
|
const adopted = this._adopt(node);
|
|
const at = this.children.indexOf(ref);
|
|
if (at < 0) this.children.push(adopted);
|
|
else this.children.splice(at, 0, adopted);
|
|
}
|
|
replaceChildren(...kids) {
|
|
this.children = [];
|
|
this.append(...kids);
|
|
}
|
|
setAttribute(k, v) {
|
|
this.attrs[k] = String(v);
|
|
}
|
|
removeAttribute(k) {
|
|
delete this.attrs[k];
|
|
if (k === "title") this.title = "";
|
|
}
|
|
addEventListener(type, handler) {
|
|
(this.listeners[type] ||= []).push(handler);
|
|
}
|
|
click() {
|
|
if (this.disabled) return;
|
|
(this.listeners.click || []).forEach((h) => h({ target: this }));
|
|
}
|
|
querySelector(selector) {
|
|
return this.querySelectorAll(selector)[0] ?? null;
|
|
}
|
|
querySelectorAll(selector) {
|
|
const cls = selector.slice(1);
|
|
const out = [];
|
|
const walk = (n) =>
|
|
n.children.forEach((k) => {
|
|
if (k.classList && k.classList.contains(cls)) out.push(k);
|
|
walk(k);
|
|
});
|
|
walk(this);
|
|
return out;
|
|
}
|
|
}
|
|
// 공용 attachCollapsible 이 쓰는 closest(`.클래스` 하나)
|
|
FakeNode.prototype.closest = function (selector) {
|
|
for (let n = this; n; n = n.parent)
|
|
if (n.classList && n.classList.contains(selector.slice(1))) return n;
|
|
return null;
|
|
};
|
|
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
|
/** 제목 누름 — 패널 뿌리에 걸린 공용 위임 손잡이로 올려 보냄(가짜 DOM 은 거품이 없음). */
|
|
const press = (root, section) => {
|
|
const target = section.querySelector(".ui-collapsible__title");
|
|
(root.listeners.click || []).forEach((h) => h({ target }));
|
|
};
|
|
|
|
const node = (tag = "div") => new FakeNode(tag);
|
|
const STUBS = {
|
|
"@ui/ui_template_locale": {
|
|
ui_locales: new Proxy({}, { get: (_, k) => [String(k)] }),
|
|
currentLanguageIndex: 0,
|
|
},
|
|
"@ui/ui_template_elements": {
|
|
createButton: ({ label }) => Object.assign(node("button"), { _text: label }),
|
|
createInputField: () => ({ root: node(), input: node("input") }),
|
|
createSelectField: () => ({ root: node(), select: { value: "" } }),
|
|
showToast: () => undefined,
|
|
},
|
|
"@ui/ui_template_checkbox": { createCheckbox: () => ({ root: node("label"), input: {} }) },
|
|
"../B05_Profile/B05_Profile_UI_Structures_Fields": { stationFields: () => ({ wrap: node() }) },
|
|
"./B07_DesignDetail_UI_PlanManage": { openPlanManager: () => undefined },
|
|
"./B07_DesignDetail_Api_Fetch": {},
|
|
"./B06_Section_UI_Page_Common": { L: (key) => key },
|
|
};
|
|
|
|
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 === "@ui/ui_template_collapsible")
|
|
return loadTs(path.join(ROOT, "ui_template", "ui_template_collapsible.ts"));
|
|
if (request.endsWith(".css")) return {};
|
|
if (request.startsWith(".")) return loadTs(`${path.resolve(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 out = {};
|
|
const titleOf = (n) => n.children.find((c) => c.tagName === "h3")?.textContent ?? "";
|
|
const folds = (root) =>
|
|
Object.fromEntries(
|
|
root
|
|
.querySelectorAll(".ui-collapsible")
|
|
.map((s) => [titleOf(s), s.classList.contains("is-collapsed")]),
|
|
);
|
|
|
|
// ── B05 · B06 한 벌 패널 ───────────────────────────────────────────────────────
|
|
const design = loadTs(path.join(ROOT, "A00_Common", "design", "design_panel.ts"));
|
|
loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Panel_Sections.ts"));
|
|
loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Panel.ts"));
|
|
const text = (t) => Object.assign(node(), { _text: t });
|
|
const run = { run: () => undefined };
|
|
const panel = design.createDesignPanel(
|
|
{
|
|
projectId: "p",
|
|
parts: {
|
|
structures: { root: node("section"), listRoot: node() },
|
|
settings: { nodes: [text("a")] },
|
|
route: {
|
|
plannedRouteButton: text("plan"),
|
|
selectedNodes: [text("sel")],
|
|
bindSelectedRoot: () => undefined,
|
|
},
|
|
crossView: { row: text("cross-row") },
|
|
standardSlot: text("std"),
|
|
},
|
|
dock: { reset: run, save: run, confirm: run, switchView: run },
|
|
},
|
|
"profile",
|
|
);
|
|
const byTitle = (title) =>
|
|
panel.root.querySelectorAll(".ui-collapsible").find((s) => titleOf(s) === title);
|
|
out.profileFirst = folds(panel.root);
|
|
const route = byTitle("계획노선");
|
|
press(panel.root, route); // 사용자가 폄(공용 attachCollapsible)
|
|
panel.setView("cross");
|
|
out.crossFirst = folds(panel.root);
|
|
press(panel.root, byTitle("B06_View_Title"));
|
|
panel.setView("profile");
|
|
panel.setView("cross");
|
|
out.afterRedraw = folds(panel.root);
|
|
out.sameRouteNode = byTitle("계획노선") === route;
|
|
|
|
// ── B07 도면 목록 ─────────────────────────────────────────────────────────────
|
|
const b07 = loadTs(path.join(ROOT, "B07_DesignDetail", "B07_DesignDetail_UI_Panels.ts"));
|
|
const d = (id, kind) => ({ id, kind, label: id, confirmed: false });
|
|
const drawings = [
|
|
d("longitudinal", "longitudinal"),
|
|
d("longitudinal_2", "longitudinal"),
|
|
d("cross_1", "cross"),
|
|
d("cross_2", "cross"),
|
|
d("plan_a", "plan"),
|
|
d("cover", "cover"),
|
|
d("cross_standard", "cross_standard"),
|
|
d("mass_haul", "mass_haul"),
|
|
d("mass_haul_2", "mass_haul"),
|
|
];
|
|
const groups = (p) =>
|
|
Object.fromEntries(
|
|
p
|
|
.querySelectorAll(".b07-drawing-group")
|
|
.map((s) => [s.dataset.kind, s.classList.contains("is-collapsed")]),
|
|
);
|
|
const fresh = node();
|
|
out.b07EntryFolded = [...b07.foldedGroups(fresh)];
|
|
const first = b07.buildDrawingSidePanel(
|
|
drawings,
|
|
() => undefined,
|
|
undefined,
|
|
false,
|
|
undefined,
|
|
undefined,
|
|
b07.foldedGroups(fresh),
|
|
);
|
|
out.b07First = groups(first);
|
|
const host = node();
|
|
host.append(first);
|
|
first
|
|
.querySelectorAll(".b07-drawing-group")
|
|
.find((s) => s.dataset.kind === "cross")
|
|
.classList.remove("is-collapsed");
|
|
first
|
|
.querySelectorAll(".b07-drawing-group")
|
|
.find((s) => s.dataset.kind === "mass_haul")
|
|
.classList.add("is-collapsed");
|
|
const again = b07.buildDrawingSidePanel(
|
|
drawings,
|
|
() => undefined,
|
|
undefined,
|
|
false,
|
|
undefined,
|
|
undefined,
|
|
b07.foldedGroups(host),
|
|
);
|
|
out.b07Redraw = groups(again);
|
|
process.stdout.write(JSON.stringify(out));
|