/* M02 일위대가 줄 통째 끌기(`M02_MasterTemplete_Labor_RowDrag.ts` · 102-2) 단위검증 헬퍼 — * 트랜스파일해 Node 로 돌림 · 작은 가짜 DOM(줄 높이 20px · 셀렉터는 그 파일이 쓰는 꼴만) · JSON 으로 냄. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const file = path.join( __dirname, "..", "..", "M02_MasterTemplete", "M02_MasterTemplete_Labor_RowDrag.ts", ); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; /* ---- 가짜 DOM ---- */ const one = (node, sel) => { const m = /^(\w+)?(?:\[([\w-]+)\])?$/.exec(sel.trim()); if (!m) throw new Error(`셀렉터 모름: ${sel}`); return (!m[1] || node.tag === m[1]) && (!m[2] || m[2] in node.attrs); }; const matches = (node, sel) => sel.split(",").some((part) => { const [up, me] = part.includes(">") ? part.split(">") : [null, part]; return one(node, me) && (!up || (node.parent && one(node.parent, up))); }); class Node { constructor(tag, attrs = {}, parent = null) { this.tag = tag; this.attrs = attrs; this.parent = parent; this.kids = []; this.on = {}; this.style = {}; const set = new Set(); this.classList = { add: (...c) => c.forEach((x) => set.add(x)), remove: (...c) => c.forEach((x) => set.delete(x)), contains: (c) => set.has(c), list: () => [...set], }; if (parent) parent.kids.push(this); } matches(sel) { return matches(this, sel); } closest(sel) { for (let n = this; n; n = n.parent) if (n.matches(sel)) return n; return null; } get previousElementSibling() { const k = this.parent.kids; return k[k.indexOf(this) - 1] ?? null; } get nextElementSibling() { const k = this.parent.kids; return k[k.indexOf(this) + 1] ?? null; } querySelectorAll(sel) { const out = []; const walk = (n) => n.kids.forEach((c) => (c.matches(sel) && out.push(c), walk(c))); walk(this); return out; } addEventListener(type, fn) { (this.on[type] ??= []).push(fn); } } /* 표 — 줄 0 · 1(옵션 값 줄 · 하위 줄 둘 = 열린 줄) · 2 · 3 */ const root = new Node("div"); const tbody = new Node("tbody", {}, new Node("table", {}, root)); const tr = (attrs) => new Node("tr", attrs, tbody); const rows = [ tr({ "data-labor-logic": "A" }), tr({ "data-labor-logic": "B" }), tr({ "data-labor-opt": "B" }), tr({ "data-labor-sub": "" }), tr({ "data-labor-sub": "" }), tr({ "data-labor-logic": "C" }), tr({ "data-labor-logic": "D" }), ]; const cell = (r, tag = "td", attrs = {}) => new Node(tag, attrs, rows[r]); const td = rows.map((_, r) => cell(r)); const btn = new Node("button", {}, cell(1)); const input = new Node("input", { "data-labor-alias": "" }, cell(1)); const qty = new Node("span", {}, new Node("button", { "data-labor-qty-open": "" }, cell(1))); const grip = new Node("span", { "data-labor-grip": "0" }, td[0]); const Y = (r) => r * 20 + 10; let capture = null; root.hasPointerCapture = (id) => capture === id; root.setPointerCapture = (id) => (capture = id); root.releasePointerCapture = () => (capture = null); const winClick = []; let frames = []; global.window = { addEventListener: (t, fn) => winClick.push(fn), removeEventListener: (t, fn) => { const i = winClick.indexOf(fn); if (i >= 0) winClick.splice(i, 1); }, getSelection: () => ({ removeAllRanges() {} }), }; global.requestAnimationFrame = (fn) => frames.push(fn); global.document = { elementFromPoint: (x, y) => td[Math.floor(y / 20)] ?? null }; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, require); const m = box.exports; /* ---- 부르기 ---- */ const moves = []; m.attachRowDrag(root, (from, to) => moves.push([from, to])); const fire = (type, e) => root.on[type]?.forEach((fn) => fn({ preventDefault() {}, ...e })); const down = (target, y) => fire("pointerdown", { button: 0, pointerId: 1, clientX: 50, clientY: y, target }); const mv = (y, dx = 0) => fire("pointermove", { pointerId: 1, buttons: 1, clientX: 50 + dx, clientY: y }); /** 누름(click) — 창 캡처 삼킴을 먼저 거침 · 남으면 줄 열고 닫기 */ const click = () => { let stopped = false; [...winClick].forEach((fn) => fn({ stopPropagation: () => (stopped = true), preventDefault() {} }), ); winClick.length = 0; // once return !stopped; }; const frame = () => { const f = frames; frames = []; f.forEach((fn) => fn()); }; const classes = () => rows.map((r) => r.classList.list().sort().join(" ")); const out = {}; out.threshold = [ [5, 0], [6, 0], [0, -6], [5, 5], [-7, 2], ].map(([dx, dy]) => m.pastThreshold(dx, dy)); // 1) 5px 만 움직임 = 누름 — 옮김 없음 · 누름 지나감(열고 닫기) down(td[0], Y(0)); mv(Y(0) + 5); fire("pointerup", {}); out.small = { moves: moves.length, clickPasses: click(), capture }; // 2) 줄 A(0) → 줄 C(차례 2) 로 끌기 — 끄는 동안 표시 · 다시 그림 미룸 · 놓으면 move(0,2) · 누름 삼킴 let drawn = 0; const host = {}; down(td[0], Y(0)); mv(Y(0) + 7); m.afterRowDrag(host, () => drawn++); m.afterRowDrag(host, () => drawn++); // 마지막 것만 mv(Y(5)); out.during = { drawn, capture, classes: classes(), select: root.style.userSelect }; fire("pointerup", {}); out.drop = { moves: moves.splice(0), drawn, clickPasses: click(), classes: classes() }; frame(); out.afterFrame = { listeners: winClick.length }; // 3) 열린 줄 B 의 하위 줄에서 시작 → 위 줄 A 로 = move(1,0) · 표시 = A 위 선 · B 무리 흐림 down(td[4], Y(4)); mv(Y(0)); out.fromSub = { classes: classes() }; fire("pointerup", {}); out.fromSub.moves = moves.splice(0); click(); frame(); // 4) 줄 A → 열린 줄 B(아래로) — 표시 = B 무리 맨 아래(하위 줄 둘째) 선 down(td[0], Y(0)); mv(Y(2)); out.downMark = classes(); fire("pointerup", {}); out.downMark.push(JSON.stringify(moves.splice(0))); click(); frame(); // 5) 단추 · 입력칸 · 수량 여는 자리에서는 시작 안 함 for (const [name, t] of [ ["button", btn], ["input", input], ["qty", qty], ]) { down(t, Y(1)); mv(Y(6)); fire("pointerup", {}); out[`block_${name}`] = { moves: moves.splice(0).length, capture, clickPasses: click() }; } // 6) 손잡이도 같은 길 · 제자리에 놓음 = 옮김 없음 · 누름은 삼킴(열림 그대로) down(grip, Y(0)); mv(Y(0) + 8); fire("pointerup", {}); out.gripSelf = { moves: moves.splice(0).length, clickPasses: click() }; frame(); // 7) 끄는 중이 아니면 바로 그림 let now = 0; m.afterRowDrag(host, () => now++); out.idleDraw = now; process.stdout.write(JSON.stringify(out));