- 280-1 이름 칸 = 고르기(누름 · Enter · Space · 손 커서 · 풍선 · 초점 테두리) · [선택] 단추 지움 · [해제] 그대로 · 본체 · 그룹 · 명세 잇기 같은 줄 부품 - 280-2 모달 제목 「구조물 조합 선택」 · 위 표 열 고름 → 나무 펼쳐 고름 · 카드 · 층 단추 → 위 표 = 그 층 표 · B08 연결 같은 모달 - 280-3 M02 구조물 조합 = 따로 짠 길로 두 고리 정상 · 고침 없음 - 시험 test_280_1_combo_label_pick 새로 · test_224_1 · test_b05_structures_form_summary 새 꼴에 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
265 lines
9.4 KiB
JavaScript
265 lines
9.4 KiB
JavaScript
/* 구조물 조합 선택 모달(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);
|
|
});
|