- 형상 누름 키 "pipe" 가 배관 명세 「관」 상자를 찾다 물넘이엔 없어 강조가 안 섬 → 관 상자가 없거나 숨었으면 보이는 「경사각도」(slope) 상자 - 배관은 「관」 상자 그대로 · 알약 · 목록 · 카드 고름 · 다시 누름 = 풂 그대로 - 시험 test_294_1_chute_remove_pick(가짜 DOM 위 조정 강조 — [삭제] 단추 · 원래 글 · 경사각도 상자) 새로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
272 lines
9.6 KiB
JavaScript
272 lines
9.6 KiB
JavaScript
/* PLAN 294-1 · 294-2 시험 헬퍼 — `B06_Section_UI_Adjust_Dock.ts` 를 트랜스파일해 작은 가짜 DOM 위에서 돌림.
|
|
* 294-1 — 강조 켜고 끈 뒤에도 이름표 [삭제] 단추 · 원래 글 · 괄호 붙임도 글 칸만.
|
|
* 294-2 — 물넘이포장(배관 명세 숨음)에서 side "pipe" = 「경사각도」 상자 강조.
|
|
* 결과 = 표준출력 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, "..", "..");
|
|
|
|
// ── 작은 가짜 DOM — 태그 · 클래스 · 속성 · 자식 · 단순 선택자(쉼표 · `>` 하나) ──────────
|
|
const TEXT = 3;
|
|
class Text {
|
|
constructor(data) {
|
|
this.nodeType = TEXT;
|
|
this.textContent = data;
|
|
this.parentNode = null;
|
|
}
|
|
}
|
|
class Comment {
|
|
constructor() {
|
|
this.nodeType = 8;
|
|
this.parentNode = null;
|
|
}
|
|
}
|
|
const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
class El {
|
|
constructor(tag) {
|
|
this.nodeType = 1;
|
|
this.tagName = tag.toUpperCase();
|
|
this.attrs = {};
|
|
this.childNodes = [];
|
|
this.parentNode = null;
|
|
this.hidden = false;
|
|
const self = this;
|
|
this.dataset = new Proxy(
|
|
{},
|
|
{
|
|
get: (_, key) => self.attrs[`data-${kebab(String(key))}`],
|
|
set: (_, key, value) => ((self.attrs[`data-${kebab(String(key))}`] = String(value)), true),
|
|
deleteProperty: (_, key) => delete self.attrs[`data-${kebab(String(key))}`],
|
|
},
|
|
);
|
|
this.classList = {
|
|
set: new Set(),
|
|
add: (...n) => n.forEach((c) => self.classList.set.add(c)),
|
|
remove: (...n) => n.forEach((c) => self.classList.set.delete(c)),
|
|
contains: (c) => self.classList.set.has(c),
|
|
toggle: (c, on) =>
|
|
(on ?? !self.classList.set.has(c)) ? self.classList.add(c) : self.classList.remove(c),
|
|
};
|
|
}
|
|
set className(v) {
|
|
this.classList.set = new Set(String(v).split(/\s+/).filter(Boolean));
|
|
}
|
|
get children() {
|
|
return this.childNodes.filter((n) => n instanceof El);
|
|
}
|
|
get firstChild() {
|
|
return this.childNodes[0] ?? null;
|
|
}
|
|
get firstElementChild() {
|
|
return this.children[0] ?? null;
|
|
}
|
|
get parentElement() {
|
|
return this.parentNode instanceof El ? this.parentNode : null;
|
|
}
|
|
get textContent() {
|
|
return this.childNodes.map((n) => (n.nodeType === 8 ? "" : n.textContent)).join("");
|
|
}
|
|
set textContent(v) {
|
|
this.childNodes.forEach((n) => (n.parentNode = null));
|
|
this.childNodes = [];
|
|
this.append(String(v));
|
|
}
|
|
setAttribute(k, v) {
|
|
this.attrs[k] = String(v);
|
|
}
|
|
getAttribute(k) {
|
|
return this.attrs[k] ?? null;
|
|
}
|
|
hasAttribute(k) {
|
|
return k === "hidden" ? this.hidden : k in this.attrs;
|
|
}
|
|
append(...kids) {
|
|
for (const k of kids) {
|
|
const node = typeof k === "string" ? new Text(k) : k;
|
|
node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1);
|
|
node.parentNode = this;
|
|
this.childNodes.push(node);
|
|
}
|
|
}
|
|
after(node) {
|
|
const list = this.parentNode.childNodes;
|
|
node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1);
|
|
node.parentNode = this.parentNode;
|
|
list.splice(list.indexOf(this) + 1, 0, node);
|
|
}
|
|
insertBefore(node, ref) {
|
|
node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1);
|
|
node.parentNode = this;
|
|
const at = ref ? this.childNodes.indexOf(ref) : -1;
|
|
if (at < 0) this.childNodes.push(node);
|
|
else this.childNodes.splice(at, 0, node);
|
|
return node;
|
|
}
|
|
replaceChild(node, old) {
|
|
const at = this.childNodes.indexOf(old);
|
|
node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1);
|
|
node.parentNode = this;
|
|
this.childNodes[at] = node;
|
|
old.parentNode = null;
|
|
}
|
|
replaceChildren(...kids) {
|
|
this.childNodes.forEach((n) => (n.parentNode = null));
|
|
this.childNodes = [];
|
|
this.append(...kids);
|
|
}
|
|
scrollIntoView() {}
|
|
all() {
|
|
return this.children.flatMap((c) => [c, ...c.all()]);
|
|
}
|
|
matches(selector) {
|
|
return selector.split(",").some((one) => {
|
|
const parts = one.trim().split(/\s*>\s*/);
|
|
if (parts.length === 2)
|
|
return (
|
|
matchOne(this, parts[1]) && !!this.parentElement && matchOne(this.parentElement, parts[0])
|
|
);
|
|
return matchOne(this, parts[0]);
|
|
});
|
|
}
|
|
closest(selector) {
|
|
for (let at = this; at instanceof El; at = at.parentElement)
|
|
if (at.matches(selector)) return at;
|
|
return null;
|
|
}
|
|
querySelectorAll(selector) {
|
|
return this.all().filter((n) => n.matches(selector));
|
|
}
|
|
querySelector(selector) {
|
|
return this.querySelectorAll(selector)[0] ?? null;
|
|
}
|
|
}
|
|
function matchOne(el, compound) {
|
|
const tag = compound.match(/^[a-z]+/i)?.[0];
|
|
if (tag && el.tagName !== tag.toUpperCase()) return false;
|
|
for (const [, c] of compound.matchAll(/\.([\w-]+)/g)) if (!el.classList.contains(c)) return false;
|
|
for (const [, k, v] of compound.matchAll(/\[([\w-]+)(?:="([^"]*)")?\]/g)) {
|
|
if (k === "hidden") {
|
|
if (!el.hidden) return false;
|
|
} else if (v === undefined ? !(k in el.attrs) : el.attrs[k] !== v) return false;
|
|
}
|
|
return true;
|
|
}
|
|
const body = new El("body");
|
|
globalThis.Node = { TEXT_NODE: TEXT };
|
|
globalThis.document = {
|
|
createElement: (tag) => new El(tag),
|
|
createTextNode: (data) => new Text(data),
|
|
createComment: () => new Comment(),
|
|
querySelectorAll: (s) => body.querySelectorAll(s),
|
|
querySelector: (s) => body.querySelector(s),
|
|
};
|
|
|
|
function loadTs(file) {
|
|
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, require);
|
|
return box.exports;
|
|
}
|
|
const dock = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Adjust_Dock.ts"));
|
|
|
|
// ── 폼 흉내 — 명세 상자 = fieldset[data-spec-container] > legend(글 + [삭제]) ──────────
|
|
const el = (tag, attrs = {}, ...kids) => {
|
|
const node = new El(tag);
|
|
for (const [k, v] of Object.entries(attrs)) {
|
|
if (k === "class") node.className = v;
|
|
else if (k === "hidden") node.hidden = v;
|
|
else node.setAttribute(k, v);
|
|
}
|
|
node.append(...kids);
|
|
return node;
|
|
};
|
|
const removeButton = () => el("button", { "data-spec-remove": "chute" }, "삭제");
|
|
const box = (id, title, withRemove, ...kids) => {
|
|
const legend = el("legend", {}, title);
|
|
if (withRemove) legend.append(" ", removeButton());
|
|
return el("fieldset", { "data-spec-container": id }, legend, ...kids);
|
|
};
|
|
const slots = {
|
|
inlet: el("div"),
|
|
outlet: el("div"),
|
|
extra: el("div"),
|
|
revetInlet: el("div"),
|
|
revetOutlet: el("div"),
|
|
wingInlet: el("div"),
|
|
wingOutlet: el("div"),
|
|
};
|
|
// 배관 명세(숨음 · 물넘이포장 고른 때) — 「관」 · 유입 · 유출 칸
|
|
const pipeSpec = el(
|
|
"div",
|
|
{ class: "b05-structure__spec", hidden: true },
|
|
box("pipe", "관", false),
|
|
box("inlet", "유입구", false, slots.inlet),
|
|
box("outlet", "유출구", false, slots.outlet),
|
|
box("revet_in", "기슭막이 유입", false, slots.revetInlet),
|
|
box("revet_out", "기슭막이 유출", false, slots.revetOutlet),
|
|
box("extra", "추가", false, slots.extra),
|
|
);
|
|
// 보이는 명세 — 날개벽 유출 칸(괄호 붙임 + [삭제] 단추 상자) · 물넘이포장 경사각도 · 도수로/돌붙임
|
|
const wingOut = box("wing_out", "유출 날개벽", true, slots.wingOutlet);
|
|
const ford = el(
|
|
"div",
|
|
{ class: "b05-structure__spec" },
|
|
box("wing_in", "유입 날개벽", false, slots.wingInlet),
|
|
wingOut,
|
|
box("slope", "경사각도", false),
|
|
box("chute", "도수로/돌붙임", true),
|
|
);
|
|
body.append(pipeSpec, ford);
|
|
dock.setAdjustSideSlots(slots);
|
|
|
|
const legendOf = (sel) => {
|
|
const fs_ = body.querySelector(sel);
|
|
const legend = fs_.querySelector("legend");
|
|
return {
|
|
active: fs_.classList.contains("is-active"),
|
|
text: legend.childNodes.find((n) => n.nodeType === TEXT)?.textContent,
|
|
buttons: legend.children.filter((c) => c.tagName === "BUTTON").map((c) => c.textContent),
|
|
base: legend.dataset.baseLabel ?? null,
|
|
};
|
|
};
|
|
const CHUTE = 'fieldset[data-spec-container="chute"]';
|
|
const SLOPE = 'fieldset[data-spec-container="slope"]';
|
|
const WING = 'fieldset[data-spec-container="wing_out"]';
|
|
const PIPE = 'fieldset[data-spec-container="pipe"]';
|
|
|
|
const panel = el("div");
|
|
const out = {};
|
|
// 294-1 ① 도수로 띠 고름 → 상자 강조 · 단추 그대로
|
|
panel.dataset.side = "chute";
|
|
dock.adoptAdjustPanel(panel);
|
|
out.chuteOn = legendOf(CHUTE);
|
|
// ② 날개벽 유출 고름 — 괄호 붙임(글 칸만) · 단추 그대로 · 도수로 강조 풂
|
|
panel.dataset.side = "outlet";
|
|
panel.dataset.panelTitle = "기슭막이";
|
|
dock.refreshAdjustSlots();
|
|
out.wingOn = legendOf(WING);
|
|
out.chuteOff = legendOf(CHUTE);
|
|
// 294-2 ③ 물넘이포장 콘크리트(side "pipe" · 관 상자 숨음) = 경사각도 상자
|
|
panel.dataset.side = "pipe";
|
|
dock.refreshAdjustSlots();
|
|
out.slopeOn = legendOf(SLOPE);
|
|
out.pipeOn = legendOf(PIPE).active;
|
|
out.wingOff = legendOf(WING);
|
|
// ④ 풂 — 모두 원래 글 · 단추
|
|
panel.dataset.side = "";
|
|
dock.refreshAdjustSlots();
|
|
out.released = [CHUTE, WING, SLOPE].map((sel) => legendOf(sel));
|
|
// ⑤ 배관을 고른 때(배관 명세 보임 · 물넘이 숨음)는 그대로 「관」 상자
|
|
pipeSpec.hidden = false;
|
|
ford.hidden = true;
|
|
panel.dataset.side = "pipe";
|
|
dock.refreshAdjustSlots();
|
|
out.pipeVisible = { pipe: legendOf(PIPE).active, slope: legendOf(SLOPE).active };
|
|
console.log(JSON.stringify(out));
|