Files
Aislo/resources/tester/helper_294_1_chute_remove_pick.cjs
T
eomsangdonandClaude Opus 5.5 704e193c1b fix(b06): 물넘이포장 콘크리트 형상 고름 · 강조 = 좌측 「경사각도」 상자 (294-2)
- 형상 누름 키 "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
2026-10-12 21:45:53 +09:00

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));