From 543d7bb8a585f09a256da5cba065363df11337e0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 11:27:09 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=C2=B7B06):=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=95=84=EB=9E=98=20=EB=8B=A8=EC=B6=94=20?= =?UTF-8?q?=EB=AC=B6=EC=9D=8C=20=EB=B0=94=EB=8B=A5=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC(PLAN=2044-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭 = 473d959d6 이 좌측 자리를 빈 채 틀에 넘겨 공용 쪼개기 건너뜀 · 보기 전환이 패널 클래스를 통째 덮어 ui-sidebar-fill 지움 - 공용 쪼개기 내보냄 · 두 번 불러도 한 번만 · 패널을 붙인 뒤 다시 부름 - 늦게 서는 칸은 스크롤 칸 안으로 · 보기 클래스는 toggle - 시험 test_design_panel_dock_fixed · 화면 시나리오 ui_checks/44-3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- A00_Common/design/design_panel.ts | 15 +++- A00_Common/design/design_shared.ts | 4 ++ .../tester/test_design_panel_dock_fixed.py | 38 ++++++++++ .../ui_checks/44-3_좌측패널_하단고정.js | 69 +++++++++++++++++++ ui_template/ui_template_overlay.ts | 4 +- 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 resources/tester/test_design_panel_dock_fixed.py create mode 100644 resources/tester/ui_checks/44-3_좌측패널_하단고정.js diff --git a/A00_Common/design/design_panel.ts b/A00_Common/design/design_panel.ts index 76094010f..08333ed9b 100644 --- a/A00_Common/design/design_panel.ts +++ b/A00_Common/design/design_panel.ts @@ -112,6 +112,13 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De const dockParts: HTMLElement[] = []; const dock = createDesignDock(view, dockParts, ctx.dock); + // 칸은 공용 스크롤 칸(쪼갠 뒤) 안에 둔다 — 쪼개기 전이면 패널 루트의 단추 묶음 앞. + const place = (element: HTMLElement): void => { + const scroll = root.querySelector(":scope > .ui-sidebar-scroll"); + if (scroll) scroll.append(element); + else root.insertBefore(element, dock.root); + }; + const show = (target: DesignView): void => { const wanted = new Set(panelSectionsFor(target).map((def) => def.id)); for (const def of panelSectionsFor(target)) { @@ -120,12 +127,12 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De const sectionRoot = "root" in made ? made.root : made; if ("root" in made && made.dock) dock.root.prepend(made.dock); built.set(def.id, sectionRoot); - root.insertBefore(sectionRoot, dock.root); + place(sectionRoot); } // 차례를 정의 순서로 다시 맞춘다 — 늦게 만들어진 칸이 맨 아래에 남지 않게. for (const def of panelSectionsFor(target)) { const element = built.get(def.id); - if (element) root.insertBefore(element, dock.root); + if (element) place(element); } built.forEach((element, id) => { // 칸이 스스로 숨긴 것(선택 포인트)은 건드리지 않고, 보기 밖 칸만 가리고 되살린다. @@ -144,7 +151,9 @@ export function createDesignPanel(ctx: DesignPanelContext, view: DesignView): De dock, setView: (next) => { dock.setView(next); - root.className = next === "profile" ? "b05-route__panel" : "b06-profile__form"; + // 클래스 통째 덮기 금지 — 공용 쪼개기가 붙인 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다. + root.classList.toggle("b05-route__panel", next === "profile"); + root.classList.toggle("b06-profile__form", next !== "profile"); show(next); }, }; diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 18fe1a8ba..622ceb954 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -12,6 +12,7 @@ import { ROUTES } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; +import { splitSidebarActions } from "@ui/ui_template_overlay"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav"; import { navigateTo } from "../router"; @@ -229,6 +230,9 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel { shared.view(), ); shared.left.append(panel.root); + // 틀이 먼저 섰으면 좌측 자리가 빈 채로 지나갔다 — 붙인 지금 [스크롤 칸][아래 고정 단추]로 쪼갠다. + const body = shared.left.closest(".ui-workflow-overlay__body"); + if (body) splitSidebarActions(body); shared.panel = panel; return panel; } diff --git a/resources/tester/test_design_panel_dock_fixed.py b/resources/tester/test_design_panel_dock_fixed.py new file mode 100644 index 000000000..d507560c9 --- /dev/null +++ b/resources/tester/test_design_panel_dock_fixed.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""B05 · B06 좌측 패널 아래 고정(계획서 44-3) — 단추 묶음이 패널 바닥에 붙고 위 칸만 스크롤. + +공용 쪼개기(`splitSidebarActions`)가 [스크롤 칸][아래 고정 단추] 로 나누는데, 한 페이지(design_page)는 +좌측 자리를 빈 채 틀에 넘겨 쪼개기가 지나가고, 보기 전환이 패널 클래스를 통째 덮어 `ui-sidebar-fill` 을 +지웠다. 세 자리가 다시 어긋나면 빨강. 화면 수치는 `ui_checks/44-3_좌측패널_하단고정.js`. +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +OVERLAY = ROOT / "ui_template" / "ui_template_overlay.ts" +PANEL = ROOT / "A00_Common" / "design" / "design_panel.ts" +SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts" + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_split_is_exported_and_idempotent(): + src = read(OVERLAY) + assert "export function splitSidebarActions(" in src + assert 'if (body.classList.contains("ui-sidebar-split")) return;' in src + + +def test_shared_panel_splits_after_attach(): + src = read(SHARED) + attach = src.index("shared.left.append(panel.root);") + assert "splitSidebarActions(body)" in src[attach : attach + 400] + + +def test_panel_keeps_fill_class_and_scroll_host(): + src = read(PANEL) + # 보기 전환이 클래스를 통째 덮으면 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다. + assert "root.className = next" not in src + # 늦게 서는 칸은 스크롤 칸 안으로 — 패널 루트에 바로 끼우면 스크롤 밖에 남는다. + assert ":scope > .ui-sidebar-scroll" in src diff --git a/resources/tester/ui_checks/44-3_좌측패널_하단고정.js b/resources/tester/ui_checks/44-3_좌측패널_하단고정.js new file mode 100644 index 000000000..67dc1ff1b --- /dev/null +++ b/resources/tester/ui_checks/44-3_좌측패널_하단고정.js @@ -0,0 +1,69 @@ +/* 44-3 — B05 · B06 좌측 패널 아래 고정(구조물 목록 · 초기화 · 저장 · 보기 전환 단추 묶음) + * 시작 화면: B05 종단설계(#/b05-profile) · 좌측 패널 열림 · 프로젝트 = 환경 배정 storage 프로젝트 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 기대 수치(B05 → B06 → B05 · 각 보기에서 보통 · 긴 패널 둘): + * - split = true · 패널 루트 자식 = [ui-sidebar-scroll, 단추 묶음] · 칸은 모두 스크롤 칸 안(loose = 0) + * - dockGap = 패널 몸통 바닥 − 단추 묶음 바닥 = 0(±1px) + * - 긴 패널(스크롤 칸에 2000px 임시 칸): overflow = true · dockGap 그대로 0 · 단추 묶음 top 안 바뀜 + * - 끝나면 임시 칸 지우고 처음 주소로 되돌림 + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const start = location.hash; + const measure = async (tag) => { + const panel = document.querySelector(".b05-route__panel, .b06-profile__form"); + if (!panel) return { tag, error: "패널 없음" }; + const body = panel.closest(".ui-workflow-overlay__body"); + const scroll = panel.querySelector(":scope > .ui-sidebar-scroll"); + const dock = panel.querySelector(":scope > .ui-sidebar-actions"); + const kids = [...panel.children].map((c) => c.classList[0]); + const gap = () => + Math.round(body.getBoundingClientRect().bottom - dock.getBoundingClientRect().bottom); + const res = { + tag, + view: panel.classList.contains("b06-profile__form") ? "B06" : "B05", + split: body.classList.contains("ui-sidebar-split"), + kids, + loose: kids.filter((k) => k !== "ui-sidebar-scroll" && !dock?.classList.contains(k)).length, + dockGap: dock ? gap() : null, + }; + if (!scroll || !dock) return res; + const top0 = Math.round(dock.getBoundingClientRect().top); + const tall = document.createElement("div"); + tall.style.cssText = "height: 2000px; flex: none"; + scroll.append(tall); + await w(100); + res.long = { + overflow: scroll.scrollHeight > scroll.clientHeight, + dockGap: gap(), + dockMoved: Math.round(dock.getBoundingClientRect().top) - top0, + }; + tall.remove(); + res.ok = + res.split && + res.loose === 0 && + Math.abs(res.dockGap) <= 1 && + res.long.overflow && + Math.abs(res.long.dockGap) <= 1 && + res.long.dockMoved === 0; + return res; + }; + const go = async (hash) => { + location.hash = hash; + for (let i = 0; i < 40; i++) { + await w(250); + const want = hash.includes("b06") ? ".b06-profile__form" : ".b05-route__panel"; + if (document.querySelector(want + " > .ui-sidebar-actions")) break; + } + await w(500); + }; + const out = []; + await go("#/b05-profile"); + out.push(await measure("B05")); + await go("#/b06-section"); + out.push(await measure("B06")); + await go("#/b05-profile"); + out.push(await measure("B05 다시")); + if (location.hash !== start) await go(start); + return JSON.stringify({ ok: out.every((r) => r.ok), out }); +})(); diff --git a/ui_template/ui_template_overlay.ts b/ui_template/ui_template_overlay.ts index 53cf25053..e25fb4236 100644 --- a/ui_template/ui_template_overlay.ts +++ b/ui_template/ui_template_overlay.ts @@ -112,7 +112,9 @@ function readOpenState(key: string, collapseWhenNarrow = false): boolean { * 아예 빼고, 나머지 그룹만 마진·패딩 없는 래퍼(`.ui-sidebar-scroll`)에 담아 그쪽만 * 스크롤시킨다. 페이지는 지금처럼 본문 마지막에 액션 줄을 붙이기만 하면 된다. */ -function splitSidebarActions(body: HTMLElement): void { +export function splitSidebarActions(body: HTMLElement): void { + // 이미 쪼갠 body 는 그대로 — 패널을 늦게 붙이는 화면(B05·B06 한 페이지)이 붙인 뒤 다시 부른다. + if (body.classList.contains("ui-sidebar-split")) return; // 액션 줄은 보통 페이지 패널 루트(b05-route__panel 등) **안**에 있다 — body 직계가 // 아니므로 깊이 무관하게 찾고, 액션 줄의 실제 부모 기준으로 쪼갠다. const actions = body.querySelector(".ui-sidebar-actions");