Files
Aislo/resources/tester/helper_164_detail_zoom.cjs
T
eomsangdonandClaude Opus 5.5 b400993b82 feat(b06): [상세] 그림 휠 줌 · 가운데 단추 팬 · 표 강조 붉은 한 벌(164-1 · 164-2)
- 복제 자식을 g 하나로 감싸 공용 b_svg_zoom_pan(wheelZoom · 커서 고정 · 상한 16 · 1배 팬) · [+ − ⤢] · 칸 비율 = viewBox
- 강조 띠 · 판정 선 · 십자 · 사토장 모두 --color-danger · 고른 칸 붉은 테두리 · 줄 표시 · hover 옅게
- 그림에 대상 없는 칸 data-empty 회색 · 누름 없음(presentKeys) · 이름 칸도 그 줄 첫 키로 눌림
- 시험 test_164_detail_zoom 새로 · test_150_detail_pick presentKeys 사례

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-10 00:01:27 +09:00

118 lines
4.8 KiB
JavaScript

/* PLAN 164-1 단위검증 헬퍼 — [상세] 그림 줌 · 팬(`B06_Section_UI_Cross_Detail.ts` `attachDetailZoom`).
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 가짜 svg 에 휠 · 가운데 단추 · 더블클릭을 쏨.
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_164_detail_zoom.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const B06 = path.join(ROOT, "B06_Section");
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
// 카드 여백은 원본에서 읽음(화면과 같은 값)
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
const STUBS = {
"@ui/ui_template_modal": { openModal: () => {} },
"../A00_Common/b_page_state": {},
"./B06_Section_Api_Fetch": {},
"./B06_Section_UI_Section_Common": { CROSS_PAD, L: (key) => key },
"./B06_Section_UI_Cross_Detail_Pick": { DETAIL_TARGETS: {} },
};
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
function loadTs(full) {
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
const localRequire = (request) => {
if (request.endsWith(".css")) return {};
if (STUBS[request]) return STUBS[request];
return loadTs(resolve(path.join(path.dirname(full), request)));
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
function el(tag) {
const attrs = {};
const listeners = {};
const self = {
tag,
parent: null,
childNodes: [],
attrs,
style: { setProperty: (name, value) => (attrs[`style:${name}`] = value) },
classList: { add: () => {}, remove: () => {} },
setAttribute: (name, value) => (attrs[name] = String(value)),
getAttribute: (name) => attrs[name] ?? null,
// 진짜 DOM 처럼 옛 부모에서 옮겨 옴
append: (...nodes) =>
nodes.forEach((node) => {
if (node.parent) node.parent.childNodes.splice(node.parent.childNodes.indexOf(node), 1);
node.parent = self;
self.childNodes.push(node);
}),
addEventListener: (type, fn) => (listeners[type] ??= []).push(fn),
fire: (type, event) => {
const full = { preventDefault() {}, stopPropagation() {}, pointerId: 1, ...event };
(listeners[type] ?? []).forEach((fn) => fn(full));
return full;
},
setPointerCapture: () => {},
releasePointerCapture: () => {},
// 칸 = viewBox 의 2 배(비율 같음)
getBoundingClientRect: () => ({ left: 0, top: 0, width: 1200, height: 600 }),
};
return self;
}
globalThis.document = { createElementNS: (_ns, tag) => el(tag), createElement: el };
const { attachDetailZoom } = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail.ts"));
const svg = el("svg");
svg.setAttribute("viewBox", "0 0 600 300");
svg.append(el("rect"), el("g"), el("g"));
let saved;
const handle = attachDetailZoom(svg, undefined, (next) => (saved = next));
const layer = svg.childNodes[0];
const out = {
wrapped: svg.childNodes.length === 1 && layer.tag === "g" && layer.childNodes.length === 3,
ratio: svg.attrs["style:--ratio"],
before: layer.getAttribute("transform"),
};
// 휠 당김(deltaY > 0) = 확대 · 커서(화면 600,300 = svg 300,150) 고정
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
out.wheelIn = { ...saved };
out.cursorFixed = saved.tx + saved.scale * 300;
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: -100 });
out.wheelOut = saved.scale;
// 원배율에서 좌클릭 끌기 = 팬 아님
handle.reset();
svg.fire("pointerdown", { button: 0, clientX: 100, clientY: 100 });
svg.fire("pointermove", { clientX: 300, clientY: 100 });
svg.fire("pointerup", {});
out.leftDrag = { ...saved };
// 가운데 단추 끌기 = 팬(1배에서도 · 화면 200px = svg 100)
svg.fire("pointerdown", { button: 1, clientX: 100, clientY: 100 });
svg.fire("pointermove", { clientX: 300, clientY: 140 });
svg.fire("pointerup", {});
out.middlePan = { ...saved };
out.panClickStopped = svg.fire("click", {
stopPropagation() {
this.stopped = true;
},
}).stopped;
// 다시 떠도 배율 이어 감
const again = el("svg");
again.setAttribute("viewBox", "0 0 600 300");
attachDetailZoom(again, saved, () => {});
out.restored = again.childNodes[0].getAttribute("transform");
// 더블클릭 원복
svg.fire("dblclick", {});
out.reset = layer.getAttribute("transform");
out.max = (() => {
for (let i = 0; i < 60; i++) svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
return saved.scale;
})();
process.stdout.write(JSON.stringify(out));