- 공용 모달 바깥 누르기 = 누름 · 뗌 둘 다 배경일 때만 닫음(숫자 칸 끌기로 안 닫힘 · 평면도 ⚙ 포함 모든 openModal) - ⚙ 설정 모달(종단 · 횡단 · 토적 · 구조물 · 전개도) 폭 720 → 360 px · 그룹 상자(legend) · 맨 아래 단추 줄 하나([안내선 숨기기] [기본값] [취소] [확인]) - [확인] = 바뀐 것만 저장(축척 → 배치) 뒤 다시 그림 · 붉은 칸이면 꺼짐 · 횡단 [이 장] [모든 장] [배율 적용] 단추 → 「적용」 고르기 + [확인] - 축척 「자동」 체크 걷음 — 기본 = 지정값(종단 평면도 1/5,000 · 토적 X 1/2,000 · 전개도 X · Y 1/50) · 옛 null = 기본으로 읽음 - 낡은 시험 고침: 153-1(지운 buildCrossLayoutControls 대신 ⚙ 횡단 모달) · 212-3 · 180-3(안내선 단추 자리) · 280-5(자동 체크 · 단추 판정) · 485 축척 · 토적 · 종단 평면 칸(옛 자동 축척 값 → 지정 기본값) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
242 lines
8.5 KiB
JavaScript
242 lines
8.5 KiB
JavaScript
/* 153-1 · 523-1 B07 횡단 장 배치 헬퍼 — 좌측 「횡단도」 카드 ⚙ 하나 · ⚙ 횡단 모달(그룹 상자
|
||
* 「장 배치」 · 「표」 · 맨 아래 단추 줄 하나 · [확인] = 바뀐 것만 저장)을 Node 로 돌려 표준출력 JSON 한 벌로.
|
||
* TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 · 공용 단추 · 입력칸 · 고르기칸은 진짜
|
||
* (`ui_template_elements`) · 모달 겉틀 · 알림만 기록용 가짜.
|
||
*/
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||
|
||
const ROOT = path.join(__dirname, "..", "..");
|
||
const B07 = path.join(ROOT, "B07_DesignDetail");
|
||
const UI = path.join(ROOT, "ui_template");
|
||
|
||
class FakeNode {
|
||
constructor(tag) {
|
||
this.tagName = tag.toUpperCase();
|
||
this.children = [];
|
||
this.dataset = {};
|
||
this.attrs = {};
|
||
this.listeners = {};
|
||
this._text = "";
|
||
this._classes = new Set();
|
||
this.value = "";
|
||
this.disabled = false;
|
||
const self = this;
|
||
this.classList = {
|
||
add: (...names) => names.forEach((n) => self._classes.add(n)),
|
||
remove: (...names) => names.forEach((n) => self._classes.delete(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("");
|
||
}
|
||
setAttribute(key, value) {
|
||
this.attrs[key] = String(value);
|
||
}
|
||
append(...kids) {
|
||
kids.forEach((k) => this.children.push(k));
|
||
}
|
||
replaceChildren(...kids) {
|
||
this.children = [...kids];
|
||
}
|
||
addEventListener(type, handler) {
|
||
(this.listeners[type] ||= []).push(handler);
|
||
}
|
||
click() {
|
||
const event = { stopPropagation() {}, preventDefault() {} };
|
||
if (!this.disabled) (this.listeners.click || []).forEach((handler) => handler(event));
|
||
}
|
||
}
|
||
const all = (node) => [node, ...node.children.flatMap(all)];
|
||
const byClass = (node, name) => all(node).filter((n) => n.classList.contains(name));
|
||
|
||
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
||
|
||
const toasts = [];
|
||
const modals = [];
|
||
const openModal = (opts) => {
|
||
const body = new FakeNode("div");
|
||
const modal = { opts, body, open: true };
|
||
modals.push(modal);
|
||
opts.mount(body, () => (modal.open = false), new FakeNode("div"));
|
||
return { close: () => (modal.open = false) };
|
||
};
|
||
const { loadTs } = makeLoader({
|
||
root: ROOT,
|
||
requireFor: (full) => (request) => {
|
||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||
if (request === "@ui/ui_template_elements")
|
||
return {
|
||
...loadTs(path.join(UI, "ui_template_elements.ts")),
|
||
showToast: (message, kind) => toasts.push([kind, message]),
|
||
};
|
||
if (request === "@ui/ui_template_modal") return { openModal };
|
||
if (request === "./B07_DesignDetail_UI_PlanManage") return {};
|
||
if (request === "./B07_DesignDetail_Api_Fetch") return {};
|
||
// 공용 카드(269) = 진짜 모듈
|
||
if (request === "@ui/ui_template_collapsible")
|
||
return loadTs(path.join(UI, "ui_template_collapsible.ts"));
|
||
if (request === "@ui/ui_template_locale")
|
||
return {
|
||
currentLanguageIndex: 0,
|
||
ui_locales: new Proxy({}, { get: (_, key) => [String(key)] }),
|
||
};
|
||
if (request.startsWith(".")) {
|
||
const target = path.join(path.dirname(full), `${request}.ts`);
|
||
return fs.existsSync(target) ? loadTs(target) : {};
|
||
}
|
||
return {}; // 이 시험이 안 부르는 바깥 길(API · 설정 · CAD)
|
||
},
|
||
});
|
||
|
||
const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Panels.ts"));
|
||
const { openDrawingSettings } = loadTs(path.join(B07, "B07_DesignDetail_UI_Settings.ts"));
|
||
|
||
const out = {};
|
||
const settings = [];
|
||
const drawings = [
|
||
{ id: "cross_standard", kind: "cross_standard", label: "표준 횡단면도", confirmed: false },
|
||
{ id: "cross_s1", kind: "cross", label: "1장", confirmed: false },
|
||
{ id: "cross_s2", kind: "cross", label: "2장", confirmed: true },
|
||
];
|
||
const panel = buildDrawingSidePanel(
|
||
drawings,
|
||
() => {},
|
||
undefined,
|
||
false,
|
||
(kind) => settings.push(kind),
|
||
);
|
||
// 280-5 배치 칸은 ⚙ 횡단 모달로 — 카드엔 ⚙ 하나 · 배치 칸 없음
|
||
const cross = all(panel).find((n) => n.dataset.kind === "cross");
|
||
const gears = byClass(cross, "ui-collapsible__settings");
|
||
out.card = {
|
||
gears: gears.length,
|
||
layoutInCard: byClass(cross, "b07-cross-layout").length,
|
||
};
|
||
gears[0].click();
|
||
out.card.settings = settings;
|
||
|
||
// ── ⚙ 횡단 모달(523-1) ─────────────────────────────────────────────────────
|
||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||
const saved = [];
|
||
let current = drawings[1]; // cross_s1 = 1장
|
||
const guideToggle = new FakeNode("button");
|
||
guideToggle.textContent = "안내선 숨기기";
|
||
const deps = {
|
||
drawings,
|
||
current: () => current,
|
||
canRedraw: () => false,
|
||
redraw: async () => false,
|
||
cross: {
|
||
layout: async () => ({ default: [4, 4], sheets: { 1: [3, 4] }, table_scale: 1 }),
|
||
save: (next, sheet) => saved.push([next, sheet]),
|
||
guideToggle,
|
||
},
|
||
};
|
||
const open = async () => {
|
||
openDrawingSettings("cross", deps);
|
||
await tick();
|
||
const modal = modals.at(-1);
|
||
const inputs = all(modal.body).filter((n) => n.tagName === "INPUT");
|
||
const select = all(modal.body).find((n) => n.tagName === "SELECT");
|
||
const footer = byClass(modal.body, "b07-settings__actions")[0];
|
||
const btn = (label) => all(footer).find((n) => n.textContent === label);
|
||
const type = (input, value) => {
|
||
input.value = value;
|
||
(input.listeners.input || []).forEach((h) => h());
|
||
};
|
||
return { modal, inputs, select, footer, btn, type };
|
||
};
|
||
|
||
(async () => {
|
||
// 장 — 그룹 상자 둘 · 공용 칸 · 단추 줄 하나
|
||
let m = await open();
|
||
const legends = byClass(m.modal.body, "b07-settings__group").map(
|
||
(g) => g.children[0].textContent,
|
||
);
|
||
const bodyButtons = all(m.modal.body).filter((n) => n.tagName === "BUTTON");
|
||
out.sheet = {
|
||
legends,
|
||
labels: byClass(m.modal.body, "ui-field__label").map((n) => n.textContent),
|
||
inputs: m.inputs.map((n) => ({
|
||
value: n.value,
|
||
type: n.attrs.type,
|
||
min: n.attrs.min,
|
||
max: n.attrs.max,
|
||
step: n.attrs.step ?? null,
|
||
cls: n.className,
|
||
})),
|
||
scope: { options: m.select.children.map((o) => o.textContent), value: m.select.value },
|
||
footers: byClass(m.modal.body, "b07-settings__actions").length,
|
||
footerButtons: m.footer.children.map((n) => n.textContent),
|
||
buttonsOutsideFooter: bodyButtons.filter((n) => !all(m.footer).includes(n)).length,
|
||
};
|
||
// 이 장만 — 2 × 5
|
||
m.type(m.inputs[0], "2");
|
||
m.type(m.inputs[1], "5");
|
||
m.btn("확인").click();
|
||
await tick();
|
||
out.applySheet = { saved: [...saved], closed: !m.modal.open };
|
||
// 모든 장 — 고르기만 바꿔도 씀(장마다 값 지움)
|
||
saved.length = 0;
|
||
m = await open();
|
||
m.select.value = "all";
|
||
m.btn("확인").click();
|
||
await tick();
|
||
out.applyAll = [...saved];
|
||
// 범위 밖 = 칸 붉음 · [확인] 꺼짐 · 저장 없음
|
||
saved.length = 0;
|
||
m = await open();
|
||
m.type(m.inputs[0], "11");
|
||
const confirm = m.btn("확인");
|
||
out.badGrid = {
|
||
disabled: confirm.disabled,
|
||
error: m.inputs[0].className.includes("ui-input--error"),
|
||
};
|
||
confirm.click();
|
||
await tick();
|
||
out.badGrid.saved = saved.length;
|
||
out.badGrid.open = m.modal.open;
|
||
// 표 배율 — 1.5 = 배율만 · 2.5 = 꺼짐
|
||
m = await open();
|
||
m.type(m.inputs[2], "2.5");
|
||
out.badTable = m.btn("확인").disabled;
|
||
m.type(m.inputs[2], "1.5");
|
||
m.btn("확인").click();
|
||
await tick();
|
||
out.applyTable = [...saved];
|
||
// 장 아님(표준 횡단면도) — 고르기 = 모든 장 하나 · 손대지 않고 [확인] = 저장 없이 닫음
|
||
saved.length = 0;
|
||
current = drawings[0];
|
||
m = await open();
|
||
out.notSheet = {
|
||
options: m.select.children.map((o) => o.textContent),
|
||
values: m.inputs.map((n) => n.value),
|
||
};
|
||
m.btn("확인").click();
|
||
await tick();
|
||
out.notSheet.saved = saved.length;
|
||
out.notSheet.closed = !m.modal.open;
|
||
// [취소] = 저장 없이 닫음
|
||
current = drawings[1];
|
||
m = await open();
|
||
m.type(m.inputs[0], "2");
|
||
m.btn("취소").click();
|
||
out.cancel = { saved: saved.length, closed: !m.modal.open };
|
||
|
||
process.stdout.write(JSON.stringify(out));
|
||
})();
|