/* 구조물 조합 선택 모달(PLAN 280-2) 시험 헬퍼 — `ui_template_item_picker.ts` 를 트랜스파일해 * 화면 부품(요소 · 모달 · 나무 · 카드 · 위 표)을 대역으로 갈아 끼우고 돌림 · 자료 쪽(`_data`)은 진짜. * 위 표 고름 → 나무(펼쳐 고름) · 오른쪽 카드 · 층 단추 → 위 표 = 그 층 — 결과는 표준출력에 JSON 한 벌. * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.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 UI = path.join(ROOT, "ui_template"); // ── 화면 그릇 흉내 — 클래스 · 자식 · 이벤트 · 숨김만 ───────────────────── class FakeNode { constructor(props = {}) { this._classes = new Set( String(props.className ?? "") .split(/\s+/) .filter(Boolean), ); this.textContent = props.text ?? ""; this.children = [...(props.children ?? [])]; this.hidden = !!(props.attrs && "hidden" in props.attrs); this.disabled = false; this.dataset = {}; this.attrs = {}; this.listeners = {}; this.value = ""; this.style = { setProperty() {} }; 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), }; } append(...kids) { this.children.push(...kids); } replaceChildren(...kids) { this.children = kids; } setAttribute(name, value) { this.attrs[name] = value; } getAttribute(name) { return this.attrs[name]; } addEventListener(type, handler) { (this.listeners[type] ||= []).push(handler); } click() { (this.listeners.click || []).forEach((h) => h({ target: this })); } scrollIntoView() { scrolled.push(this.dataset.id ?? ""); } all() { return this.children.flatMap((c) => (c instanceof FakeNode ? [c, ...c.all()] : [])); } querySelector(selector) { const name = selector.replace(/^\./, ""); return this.all().find((n) => n.classList.contains(name)) ?? null; } } const scrolled = []; const tableShows = []; const detailShows = []; let onColumn = null; let modal = null; const button = ({ label, variant, onClick }) => { const node = new FakeNode({ className: `ui-btn ui-btn--${variant ?? "filled"}`, text: label }); if (onClick) node.addEventListener("click", onClick); return node; }; const STUBS = { "./ui_template_item_picker.css": {}, "@config/config_frontend": { API_BASE_URL: "/api" }, "./ui_template_elements": { el: (_tag, props) => new FakeNode(props), createButton: button, createInputField: () => { const input = new FakeNode({ className: "ui-input" }); return { root: new FakeNode({ children: [input] }), input }; }, showToast: () => {}, }, "./ui_template_locale": { t: (key) => key }, "./ui_template_modal": { openModal: (opts) => { const body = new FakeNode({ className: "ui-modal__body" }); modal = { title: opts.title, body }; opts.mount(body); return { close: () => {} }; }, }, // 나무 — 펼친(`open`) 갈래만 아래 줄을 그림 = 보이는 줄만 생김 "./ui_template_tree": { renderTree: function render(nodes, make, depth = 0) { return nodes.flatMap((node) => { if (!node.children?.length) return [make(node, "", depth, () => node.run?.())]; const row = make(node, node.open ? "▾" : "▸", depth, () => {}); return [row, ...(node.open ? render(node.children, make, depth + 1) : [])]; }); }, createTreeRow: (node, _caret, _depth, onClick) => { const row = new FakeNode({ className: "ui-tree__row", text: node.label }); row.dataset.id = node.id; row.addEventListener("click", onClick); return row; }, openTreeMenu: () => {}, }, "./ui_template_item_picker_ask": { askFields: async () => null }, "./ui_template_item_picker_project": { projectItems: () => ({}) }, "./ui_template_item_picker_detail": { createItemDetail: () => ({ root: new FakeNode({ className: "detail" }), show: (row) => detailShows.push(row ? row.key : null), markEdited: () => {}, reload: () => {}, }), }, "./ui_template_item_picker_edit": { createEditView: () => ({}) }, "./ui_template_item_picker_table": { createComboTable: (cb) => { onColumn = cb; return { root: new FakeNode({ className: "table" }), show: (at, column) => tableShows.push(at ? `${at.layer}|${at.owner ?? ""}|${column}` : null), destroy: () => {}, }; }, }, }; const cache = new Map(); 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 (request in STUBS) return STUBS[request]; 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 item = (key, 열, 경로) => ({ key, 열, 경로 }); const GROUPS = [ { layer: "system", owner: null, items: [ item("SS1", "rvS1", ["돌기슭막이", "메쌓기", "1.2m"]), item("SS2", "rvS2", ["돌기슭막이", "찰쌓기", "1.5m"]), ], }, { layer: "company", owner: "c1", items: [item("SC1", "rvC1", ["배수관", "흄관"])] }, { layer: "personal", owner: "u1", items: [item("SP1", "rvP1", ["옹벽", "기초"]), item("SP2", "rvP2", ["옹벽", "벽체", "1m"])], }, ]; globalThis.fetch = async () => ({ ok: true, json: async () => GROUPS }); const flush = () => new Promise((resolve) => setTimeout(resolve, 20)); (async () => { const { openItemPicker } = loadTs(path.join(UI, "ui_template_item_picker.ts")); const out = {}; void openItemPicker({ current: null, canCopy: false }); await flush(); const [, cols] = modal.body.children; const [search, layerBar, treeHost] = cols.children[0].children; const searchInput = search.children[0]; const active = () => treeHost.all().find((n) => n.classList.contains("is-active"))?.dataset.id; const leaves = () => treeHost .all() .filter((n) => n.dataset.id && n.dataset.id.split("|").length === 3) .map((n) => n.dataset.id); const layerButton = (key) => layerBar.children.find((b) => b.textContent === key); const pressed = () => layerBar.children .filter((b) => b.getAttribute("aria-pressed") === "true") .map((b) => b.textContent); out.title = modal.title; out.start = { table: tableShows.at(-1), layer: pressed(), detail: detailShows.at(-1) }; // ① 위 표에서 열 고름(개인 층) → 나무 고름 · 펼쳐 보임 · 굴림 · 오른쪽 카드 · 표 열 scrolled.length = 0; onColumn("rvP2"); out.tablePick = { active: active(), visible: leaves().includes("personal|u1|SP2"), scrolled: scrolled.includes("personal|u1|SP2"), detail: detailShows.at(-1), table: tableShows.at(-1), }; // 같은 열 다시 · 표에 없는 열 = 아무것도 안 바뀜(표 → 나무 → 표 되돌이 없음) const before = { detail: detailShows.length, table: tableShows.length }; onColumn("rvP2"); onColumn("rvNone"); out.sameAgain = { detail: detailShows.length - before.detail, table: tableShows.length - before.table, }; // ② 층 단추 → 위 표 = 그 층 표(고른 항목은 다른 층이라 열 없음) · 나무 = 그 층 layerButton("M02_PickerSystem").click(); out.layerSystem = { table: tableShows.at(-1), layer: pressed(), detail: detailShows.at(-1) }; layerButton("M02_PickerCompany").click(); out.layerCompany = { table: tableShows.at(-1), layer: pressed() }; // 고른 항목의 층으로 돌아오면 그 열이 다시 골라짐 layerButton("M02_PickerPersonal").click(); out.layerBack = tableShows.at(-1); // ③ 다른 층 표에서 고름 — 접힌 폴더가 펼쳐지고 그 항목 · 카드 · 표 열 layerButton("M02_PickerSystem").click(); out.systemLeavesBefore = leaves(); onColumn("rvS2"); out.systemPick = { active: active(), visible: leaves().includes("system||SS2"), detail: detailShows.at(-1), table: tableShows.at(-1), }; // ④ 나무 고름 → 위 표 열(지금 길 그대로) onColumn("rvS1"); const leaf = treeHost.all().find((n) => n.dataset.id === "system||SS2"); leaf.click(); out.treePick = { active: active(), detail: detailShows.at(-1), table: tableShows.at(-1) }; // ⑤ 찾기에 가려진 항목을 표에서 고르면 찾기를 풂 searchInput.value = "찰쌓기"; (searchInput.listeners.input || []).forEach((h) => h()); onColumn("rvS1"); out.searchCleared = { value: searchInput.value, active: active() }; console.log(JSON.stringify(out)); })().catch((error) => { console.error(error); process.exit(1); });