/* 종단 새 창 최대화 — 같은 꼴 나머지(계획서 230-2) 단위검증 헬퍼. * 메인 창은 「가려짐」(화면 틀 · 시계 멈춤) · 종단 요소는 새 창 소속. 실제 TS 의 해당 함수 본문을 * 잘라 그 자리에서 트랜스파일해 돌리고 JSON 으로 냄 — 다시 그리기 · 높이 맞춤 · 표 · 유토곡선 · * 끌기 뒤 그리기 · 길게 누르기 · 배수유역도가 새 창 틀 · 시계로 도는지. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); const run = (code, scope) => { const js = ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const names = Object.keys(scope); return new Function(...names, js)(...names.map((n) => scope[n])); }; /** `start` 부터 짝 맞는 닫는 중괄호까지 잘라 냄 — 본문 여는 괄호가 반환 타입 뒤면 `open` 으로 짚음. */ function block(src, start, open = "{") { const at = src.indexOf(start); if (at < 0) throw new Error(`본문 없음: ${start}`); let depth = 0; for (let i = src.indexOf(open, at) + open.length - 1; i < src.length; i++) { if (src[i] === "{") depth++; else if (src[i] === "}" && --depth === 0) return src.slice(at, i + 1); } throw new Error(`닫힘 없음: ${start}`); } // ---- 가짜 창 — frozen 이면 화면 틀 · 시계가 안 돎 ---- function fakeWindow(name, frozen) { const win = { name, frozen, now: 0, timers: [], seq: 0, frames: 0, listeners: [] }; win.setTimeout = (fn, ms) => (win.timers.push({ id: ++win.seq, at: win.now + ms, fn }), win.seq); win.clearTimeout = (id) => (win.timers = win.timers.filter((t) => t.id !== id)); win.setInterval = (fn, ms) => { const id = ++win.seq; const again = (at) => win.timers.push({ id, at, fn: () => (fn(), again(win.now + ms)) }); again(win.now + ms); return id; }; win.clearInterval = win.clearTimeout; win.requestAnimationFrame = (fn) => win.setTimeout(() => (win.frames++, fn(win.now)), 16); win.cancelAnimationFrame = win.clearTimeout; win.addEventListener = (type, fn) => win.listeners.push([type, fn]); win.removeEventListener = (type, fn) => (win.listeners = win.listeners.filter(([t, f]) => t !== type || f !== fn)); win.tick = (ms) => { if (win.frozen) return; const end = win.now + ms; for (;;) { const next = win.timers.filter((t) => t.at <= end).sort((a, b) => a.at - b.at)[0]; if (!next) break; win.now = next.at; win.clearTimeout(next.id); next.fn(); } win.now = end; }; return win; } const main = fakeWindow("main", true); const popup = fakeWindow("popup", false); globalThis.window = main; globalThis.requestAnimationFrame = main.requestAnimationFrame; // 옛 코드가 쓰던 전역 = 메인 창 globalThis.cancelAnimationFrame = main.cancelAnimationFrame; const inPopup = () => ({ ownerDocument: { defaultView: popup }, style: { setProperty() {} } }); const vw = {}; run( read("common_util", "common_util_view_window.ts").replace(/export /g, "") + "\nObject.assign(vw, { viewOf, frameOf });", { vw }, ); const { viewOf, frameOf } = vw; const out = {}; const mainBusy = () => main.timers.length; // ① 공용 함수 — 새 창 요소는 새 창 · 안 붙은 요소는 메인 창 out.viewOf = { popup: viewOf(inPopup()).name, detached: viewOf({ ownerDocument: { defaultView: null } }).name, none: viewOf(null).name, }; // ② 계획고 편집 뒤 다시 그리기(design_profile rebuild) — 두 번 불러도 한 번만 그림 { const s = { draws: 0, previews: 0 }; const profileSrc = read("A00_Common", "design", "design_profile.ts"); // 다시 그림 예약 = drawSoon(349-0s · 같은 틀 한 번) const src = [ block(profileSrc, "function rebuild(): void {"), block(profileSrc, "function drawSoon(): void {"), ].join("\n"); run(`let redrawPending = false; ${src}\nrebuild(); rebuild(); s.rebuild = true;`, { s, base: null, alignment: null, buildAlignment: () => null, store: { edits: () => ({}) }, projectId: "p", pagePipeline: { pipelineGrade: () => undefined }, // 계산 묶음(346-1) — 묶음 없으면 아무 일 없음 scheduleCrossPreview: () => s.previews++, draw: () => s.draws++, root: inPopup(), frameOf, }); popup.tick(50); out.rebuild = { draws: s.draws, previews: s.previews, mainTimers: mainBusy() }; } // ③ 높이 맞춤(Heights scheduleLightSync) { const s = { syncs: 0 }; const src = block( read("B05_Profile", "B05_Profile_UI_Profile_Heights.ts"), "function scheduleLightSync(): void {", ); run(`let lightSyncPending = false; ${src}\nscheduleLightSync(); scheduleLightSync();`, { deps: { body: inPopup() }, syncHeightsLight: () => s.syncs++, frameOf, }); popup.tick(50); out.heights = { syncs: s.syncs, mainTimers: mainBusy() }; } // ④ 표 · 유토곡선 크기 끌기(TableOverlay · MassHaul onResize) for (const [key, file] of [ ["table", "B05_Profile_UI_Profile_TableOverlay.ts"], ["massHaul", "B05_Profile_UI_Profile_MassHaul.ts"], ]) { const s = { changed: 0, handles: 0 }; const src = block(read("B05_Profile", file), "onResize: () => {").replace( /^onResize: /, "const onResize = ", ); run(`let resizeRedrawPending = false; ${src}\nonResize(); onResize();`, { syncHandlePosition: () => s.handles++, onChanged: () => s.changed++, overlay: inPopup(), frameOf, }); popup.tick(50); out[key] = { changed: s.changed, handles: s.handles, mainTimers: mainBusy() }; } // ⑤ 유토곡선 세로창 미끄러짐(scheduleMassHaulSettle) { const s = { redraws: 0 }; const src = block( read("common_util", "common_util_mass_haul_view.ts"), "export function scheduleMassHaulSettle(", ).replace(/^export /, ""); const state = { held: null, settled: false, frame: 0 }; run( `${src}\nscheduleMassHaulSettle(state, () => s.redraws++, node); scheduleMassHaulSettle(state, () => s.redraws++, node);`, { s, state, node: inPopup(), viewOf, }, ); popup.tick(50); out.settle = { redraws: s.redraws, frame: state.frame, view: state.view?.name, mainTimers: mainBusy(), }; } // ⑥ 끌기 뒤 다시 그리기(Profile_Drag scheduleDraw · 120ms 모음) { const s = { draws: 0 }; const src = block( read("B05_Profile", "B05_Profile_UI_Profile_Drag.ts"), "const scheduleDraw = (): void => {", ); run(`let resizeTimer = 0; let timerView = window; ${src}\nscheduleDraw(); scheduleDraw();`, { window: main, root: inPopup(), ctx: { draw: () => s.draws++ }, viewOf, }); popup.tick(200); out.drag = { draws: s.draws, mainTimers: mainBusy() }; } // ⑦ ▲▼ 길게 누르기(Profile_Edit createHoldRepeater) — 0.5초 뒤 초당 10회 { const s = { acts: 0 }; const src = block( read("B05_Profile", "B05_Profile_UI_Profile_Edit.ts"), "function createHoldRepeater(", "} {", ); run(`${src}\nconst r = createHoldRepeater(node); r.start(() => s.acts++); s.r = r;`, { s, window: main, node: inPopup(), viewOf, HOLD_DELAY_MS: 500, HOLD_INTERVAL_MS: 100, }); popup.tick(1000); const acts = s.acts; s.r.stop(); popup.tick(1000); out.hold = { acts, afterStop: s.acts, mainTimers: mainBusy(), popupTimers: popup.timers.length }; } // ⑧ 배수유역도(Drainage_Panel scheduleDraw) { const s = { draws: 0 }; const src = block( read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts"), "function scheduleDraw(): void {", ); run(`let frameHandle = 0; let frameView = window; ${src}\nscheduleDraw(); scheduleDraw();`, { window: main, root: inPopup(), draw: () => s.draws++, viewOf, }); popup.tick(50); out.drainage = { draws: s.draws, mainTimers: mainBusy() }; } process.stdout.write(JSON.stringify(out));