From 4f4cc22c36893b540f2dd79617e8e01f7d895ad5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 09:46:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EA=B0=80=EC=9D=B4=EB=93=9C):=20=EC=A2=8C?= =?UTF-8?q?=EC=B8=A1=20=ED=8C=A8=EB=84=90=20[=3F]=20=EB=8A=A6=EA=B2=8C=20?= =?UTF-8?q?=EB=B6=99=EC=9D=B8=20=EB=8B=A8=EC=B6=94=20=EC=A4=84=EC=97=90?= =?UTF-8?q?=EB=8F=84=20=EC=9E=90=EB=A6=AC=20=E2=80=94=20E03=20=C2=B7=20E04?= =?UTF-8?q?=20=C2=B7=20E06=20=C2=B7=20E07(=EC=9A=94=EA=B5=AC=20359-2=20?= =?UTF-8?q?=C2=B7=20360-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭 = 패널을 세운 뒤 단추 줄을 붙이는 화면은 쪼갬 · [?] [<] [>] 자리를 못 찾음 - 고침 = 공용 좌측 패널이 단추 줄이 붙는 때를 지켜보다 한 번 쪼갬 · 붙으면 지켜보기 끊음 - 시험 = 늦게 붙인 줄 · 먼저 붙인 줄 [?] 한 벌(새) · 280-2 헬퍼 대역에 538-3 고침 모듈 더함(낡음) Co-Authored-By: Claude Opus 5.5 --- resources/tester/helper_280_2_step_nav.cjs | 1 + .../tester/helper_359_2_late_actions.cjs | 141 ++++++++++++++++++ .../tester/test_359_2_help_late_actions.py | 41 +++++ ui_template/ui_template_overlay.ts | 8 + 4 files changed, 191 insertions(+) create mode 100644 resources/tester/helper_359_2_late_actions.cjs create mode 100644 resources/tester/test_359_2_help_late_actions.py diff --git a/resources/tester/helper_280_2_step_nav.cjs b/resources/tester/helper_280_2_step_nav.cjs index 92ed1250a..01d9efb6d 100644 --- a/resources/tester/helper_280_2_step_nav.cjs +++ b/resources/tester/helper_280_2_step_nav.cjs @@ -123,6 +123,7 @@ const STUBS = { }, "./ui_template_locale": { t: (key) => key }, "./ui_template_modal": { openModal: () => undefined }, + "./ui_template_help_edit": { attachHelpEditing: () => ({ flush: () => undefined }) }, "./ui_template_elements": { showToast: (message) => toasts.push(message), el: (tag) => new FakeNode(tag), diff --git a/resources/tester/helper_359_2_late_actions.cjs b/resources/tester/helper_359_2_late_actions.cjs new file mode 100644 index 000000000..2d9f78f8a --- /dev/null +++ b/resources/tester/helper_359_2_late_actions.cjs @@ -0,0 +1,141 @@ +/* 요구 359-2 · 360-2 — 좌측 패널 [?] 자리. `ui_template_overlay.ts` 를 트랜스파일해 가짜 DOM 위에서 + * 액션 줄을 ① 패널 세우기 전(E05 꼴) ② 세운 뒤(E03 · E04 · E06 · E07 꼴)에 붙여 쪼갬 · [?] 를 JSON 한 벌로 냄. + */ +const path = require("path"); +const { makeLoader } = require("./helper_ts_stub_loader.cjs"); + +const ROOT = path.join(__dirname, "..", ".."); + +class FakeNode { + constructor(tag) { + this.tagName = tag; + this.children = []; + this.parent = null; + this.style = {}; + this._classes = new Set(); + const self = this; + this.classList = { + add: (...n) => n.forEach((x) => self._classes.add(x)), + remove: (...n) => n.forEach((x) => self._classes.delete(x)), + toggle: (n, f) => + (f === undefined ? !self._classes.has(n) : !!f) + ? self._classes.add(n) + : self._classes.delete(n), + contains: (n) => self._classes.has(n), + }; + } + set className(v) { + this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); + } + get className() { + return [...this._classes].join(" "); + } + get parentElement() { + return this.parent; + } + get firstChild() { + return this.children[0] ?? null; + } + get childElementCount() { + return this.children.length; + } + _adopt(kid) { + if (kid.parent) kid.parent.children = kid.parent.children.filter((c) => c !== kid); + kid.parent = this; + return kid; + } + append(...kids) { + kids.forEach((k) => this.children.push(this._adopt(k))); + mutated(); + } + prepend(...kids) { + this.children.unshift(...kids.map((k) => this._adopt(k))); + mutated(); + } + setAttribute() {} + addEventListener() {} + querySelector(selector) { + const cls = selector.slice(1); + let hit = null; + const walk = (n) => + n.children.forEach((k) => { + if (!hit && k._classes.has(cls)) hit = k; + if (!hit) walk(k); + }); + walk(this); + return hit; + } +} +// 진짜 MutationObserver 처럼 바뀐 뒤 한꺼번에 — `flush()` 가 부름(지켜보는 것만 · 끊으면 안 부름) +const observers = new Set(); +let dirty = false; +function mutated() { + dirty = true; +} +function flush() { + while (dirty) { + dirty = false; + [...observers].forEach((o) => o.cb()); + } +} +globalThis.MutationObserver = class { + constructor(cb) { + this.cb = cb; + } + observe() { + observers.add(this); + } + disconnect() { + observers.delete(this); + } +}; +globalThis.document = { createElement: (tag) => new FakeNode(tag) }; +globalThis.window = { innerWidth: 1600 }; + +const { loadTs } = makeLoader({ + root: ROOT, + stubs: { + "./ui_template_locale": { t: (key) => key }, + "./ui_template_help": { + placeHelpButton: (actions) => { + const button = new FakeNode("button"); + button.className = "ui-help-btn"; + actions.prepend(button); + }, + }, + "./ui_template_overlay_drag": { makePanelDraggable: () => ({ refresh() {} }) }, + "/b_workflow_nav": { fetchWorkflowState: async () => ({}) }, + "/b_page_state": { readByKey: () => null, writeByKey: () => undefined }, + "/device_context": { SIZE_BREAKS: { expanded: 840 } }, + }, +}); +const overlay = loadTs(path.join(ROOT, "ui_template", "ui_template_overlay.ts")); + +function mount(late) { + const panel = new FakeNode("div"); + const actions = new FakeNode("div"); + actions.className = "ui-sidebar-actions"; + panel.append(new FakeNode("p")); + if (!late) panel.append(actions); + const { root } = overlay.createWorkflowOverlays({ + title: "t", + optionsContent: panel, + showProjectName: false, + }); + const before = !!actions.querySelector(".ui-help-btn"); + if (late) panel.append(actions); + flush(); + const body = root.querySelector(".ui-workflow-overlay__body"); + return { + before, + help: actions.querySelector(".ui-help-btn") ? 1 : 0, + split: body.classList.contains("ui-sidebar-split"), + scroll: !!panel.querySelector(".ui-sidebar-scroll"), + lastIsActions: panel.children[panel.children.length - 1] === actions, + watching: observers.size, + }; +} + +const early = mount(false); +const late = mount(true); +process.stdout.write(JSON.stringify({ early, late })); diff --git a/resources/tester/test_359_2_help_late_actions.py b/resources/tester/test_359_2_help_late_actions.py new file mode 100644 index 000000000..8ba004403 --- /dev/null +++ b/resources/tester/test_359_2_help_late_actions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""요구 359-2 · 360-2 — 좌측 패널 아래 [?] 는 액션 줄을 언제 붙여도 한 벌. + +① 패널 세우기 전에 붙인 줄(E05 꼴) = 바로 쪼갬 · [?] +② 세운 뒤에 붙인 줄(E03 · E04 · E06 · E07 꼴) = 붙는 때 쪼갬 · [?] · 지켜보기 끊음 +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def results(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_359_2_late_actions.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_먼저_붙인_줄은_바로_물음(results): + early = results["early"] + assert early["before"] is True + assert early["help"] == 1 and early["split"] and early["scroll"] and early["lastIsActions"] + assert early["watching"] == 0 + + +def test_늦게_붙인_줄도_물음_한_벌(results): + late = results["late"] + assert late["before"] is False + assert late["help"] == 1 and late["split"] and late["scroll"] and late["lastIsActions"] + assert late["watching"] == 0 diff --git a/ui_template/ui_template_overlay.ts b/ui_template/ui_template_overlay.ts index bc3f46928..f102253af 100644 --- a/ui_template/ui_template_overlay.ts +++ b/ui_template/ui_template_overlay.ts @@ -201,6 +201,14 @@ function createPanel( if (body.childElementCount > 0) { body.classList.add("ui-workflow-overlay__body"); if (isSidebar) splitSidebarActions(body); + // 액션 줄을 패널을 세운 뒤에 붙이는 화면(E03 · E04 · E06 · E07) — 붙는 때 쪼갬 · [?] 자리 + if (isSidebar && !body.classList.contains("ui-sidebar-split")) { + const watch = new MutationObserver(() => { + splitSidebarActions(body); + if (body.classList.contains("ui-sidebar-split")) watch.disconnect(); + }); + watch.observe(body, { childList: true, subtree: true }); + } root.append(body); }