From 4a47b9048d116e469668aed123a1f39586ac1a72 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 18:29:26 +0900 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=EC=A2=81=EC=9D=80=20=ED=8F=AD?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8A=94=20=EB=81=8C=EC=96=B4=20=EB=91=94=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=9E=90=EB=A6=AC=EB=A5=BC=20=EC=9E=A0?= =?UTF-8?q?=EC=8B=9C=20=EB=AC=BC=EB=A6=BC=20=E2=80=94=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EA=B0=92=EC=9D=80=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ 인라인 좌표는 미디어 규칙보다 세다. 넓은 화면에서 끌어 둔 자리가 좁은 화면에도 그대로 남아 본문을 덮었음(앞 커밋으로 폭은 줄였으나 자리는 그대로였음). - 좁아지면 인라인 좌표를 **떼어** CSS 자리로 보냄. ⚠ **저장값은 안 지움** — 지우면 사용자가 맞춰 둔 자리가 사라져 더 나쁨. - 넓어지면 **끌어 둔 자리를 되찾음.** - ⚠ 좁은 폭에서 사용자가 직접 끌면 **그 뜻이 이김** — 규칙이 다시 덮지 않음. 그 자리는 **저장하지 않아** 넓은 화면 자리를 밀어내지도 않음. 실측(진행단계 패널, 저장값 {left:420, top:300} 을 심어 두고) 1400 420/300 (끌어 둔 자리) 저장값 유지 760 물러남 · 본문 남음 288px 저장값 유지 500 물러남 · 본문 남음 194px 저장값 유지 620 에서 직접 끌기 → 410/309 로 붙고, 폭을 600 으로 바꿔도 그 자리 유지 다시 1400 → 420/300 되찾음 · 저장값 안 바뀜 곁들여 — 좁은 폭 진행단계 규칙에서 `top: auto; bottom: …` 을 뺌. 위·아래가 함께 잡혀 패널이 화면 높이만큼 늘어났음(실측 717px). 폭만 줄이고 자리는 그대로 둠. Co-Authored-By: Claude Opus 5 (1M context) --- ui_template/ui_template_overlay.css | 5 +- ui_template/ui_template_overlay_drag.ts | 64 +++++++++++++++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/ui_template/ui_template_overlay.css b/ui_template/ui_template_overlay.css index 97a6daad..f95397ff 100644 --- a/ui_template/ui_template_overlay.css +++ b/ui_template/ui_template_overlay.css @@ -73,12 +73,11 @@ width: min(var(--wf-left-panel-narrow-width), calc(100vw - var(--wf-min-content-width))); } + /* ⚠ 아래로 내리려고 `top: auto; bottom: …` 을 썼더니 위·아래가 함께 잡혀 패널이 + 화면 높이만큼 늘어났다(실측 717px). 폭만 줄이고 자리는 그대로 둔다. */ .ui-workflow-overlay__panel--progress { - top: auto; - bottom: var(--spacing-16); right: var(--spacing-8); width: min(calc(var(--wf-left-panel-width) / 2), 22vw); - max-height: 40vh; } } diff --git a/ui_template/ui_template_overlay_drag.ts b/ui_template/ui_template_overlay_drag.ts index 35e205cc..0ef11079 100644 --- a/ui_template/ui_template_overlay_drag.ts +++ b/ui_template/ui_template_overlay_drag.ts @@ -9,6 +9,13 @@ * 안 그러면 옮길 때마다 패널이 접힌다. * * 자리는 세션에만 남긴다(5장 데이터 3층 — 화면 조작값은 캐시 몫). + * + * ⚠ 좁은 폭에서는 **끌어 둔 자리를 잠시 물린다**(2026-09-08). 인라인 좌표는 미디어 + * 규칙보다 세서, 넓은 화면에서 끌어 둔 자리가 좁은 화면에서도 그대로 남아 본문을 + * 덮었다(실측: 본문에 4px 만 남았다). ⇒ 좁아지면 인라인 좌표를 **지우지 않고 잠깐 + * 떼어** CSS 자리로 보내고, 넓어지면 **끌어 둔 자리를 되찾는다.** + * ⚠ 다만 **좁은 폭에서 사용자가 직접 끌면 그 뜻이 이긴다** — 그때는 규칙이 다시 덮지 + * 않는다. 그 자리는 저장하지 않아 넓은 화면 자리를 밀어내지도 않는다. */ import { readByKey, writeByKey } from "../A00_Common/b_page_state"; @@ -18,6 +25,8 @@ const DRAG_THRESHOLD_PX = 4; const MIN_TOP_PX = 64; /** 화면 가장자리에서 남겨 둘 여백. */ const EDGE_GAP_PX = 8; +/** 좁은 폭 경계 — `ui_template_overlay.css` 의 미디어 규칙과 같은 값이어야 한다. */ +const NARROW_MAX_PX = 860; export interface PanelDragHandle { /** 펼침/접힘이 바뀐 뒤 열리는 방향을 다시 판정한다. */ @@ -46,7 +55,10 @@ export function makePanelDraggable( header: HTMLElement, storageKey: string, ): PanelDragHandle { + /** 끌어 둔 자리(넓은 화면 기준) — 좁은 폭에서도 **지우지 않는다.** */ let position: PanelPosition | null = readPosition(storageKey); + /** 좁은 폭에서 사용자가 직접 끌어 둔 자리 — 저장하지 않고 그 폭에서만 쓴다. */ + let narrowPosition: PanelPosition | null = null; let pointerId: number | null = null; let startX = 0; let startY = 0; @@ -76,8 +88,33 @@ export function makePanelDraggable( * 자리를 인라인 좌표로 박고, 아래 공간이 모자라면 헤더 **위로** 펼치게 뒤집는다. * 뒤집을 때는 `bottom` 기준으로 잡아 헤더가 놓아 둔 자리에 그대로 남는다. */ - function apply(next: PanelPosition): void { - position = next; + function isNarrow(): boolean { + return window.innerWidth <= NARROW_MAX_PX; + } + + /** 인라인 좌표를 뗀다 — CSS(미디어 규칙)가 정한 자리로 돌아간다. 저장값은 그대로 둔다. */ + function detach(): void { + root.style.left = ""; + root.style.right = ""; + root.style.top = ""; + root.style.bottom = ""; + root.classList.remove("is-flip-up"); + } + + /** 지금 폭에 맞는 자리를 앉힌다. 좁은 폭에서 직접 끈 자리가 있으면 그것이 이긴다. */ + function settle(): void { + if (isNarrow()) { + if (narrowPosition) apply(clamp(narrowPosition.left, narrowPosition.top), false); + else detach(); + return; + } + if (position) apply(clamp(position.left, position.top), false); + else detach(); + } + + function apply(next: PanelPosition, persist = true): void { + if (persist && isNarrow()) narrowPosition = next; + else if (persist) position = next; const headerHeight = header.offsetHeight; const body = bodyHeight(); const spaceBelow = window.innerHeight - (next.top + headerHeight) - EDGE_GAP_PX; @@ -94,7 +131,9 @@ export function makePanelDraggable( root.style.bottom = "auto"; root.style.top = `${next.top}px`; } - writeByKey(storageKey, JSON.stringify(next)); + // ⚠ 좁은 폭에서 끈 자리는 **저장하지 않는다** — 저장하면 넓은 화면에서 맞춰 둔 + // 자리를 밀어낸다. 그 폭에 있는 동안만 쓴다. + if (persist && !isNarrow()) writeByKey(storageKey, JSON.stringify(next)); } function swallowNextClick(): void { @@ -153,7 +192,8 @@ export function makePanelDraggable( root.classList.remove("is-dragging"); if (!moved) return; // 놓는 시점에 열리는 방향을 다시 판정한다. - if (position) apply(clamp(position.left, position.top)); + const held = isNarrow() ? narrowPosition : position; + if (held) apply(clamp(held.left, held.top)); swallowNextClick(); } @@ -161,11 +201,19 @@ export function makePanelDraggable( window.addEventListener("pointercancel", endDrag); // 만들어진 직후에는 아직 DOM에 붙기 전이라 실측 크기가 0이다 — 한 프레임 뒤에 앉힌다. - if (position) requestAnimationFrame(() => position && apply(clamp(position.left, position.top))); + requestAnimationFrame(settle); + + // 창 폭이 바뀌면 다시 판정한다 — 좁아지면 물러나고 넓어지면 끌어 둔 자리를 되찾는다. + let pending = 0; + window.addEventListener("resize", () => { + if (pending) return; + pending = requestAnimationFrame(() => { + pending = 0; + settle(); + }); + }); return { - refresh: () => { - if (position) apply(clamp(position.left, position.top)); - }, + refresh: settle, }; }