fix(ui): 좁은 폭에서는 끌어 둔 패널 자리를 잠시 물림 — 저장값은 그대로

⚠ 인라인 좌표는 미디어 규칙보다 세다. 넓은 화면에서 끌어 둔 자리가 좁은 화면에도
그대로 남아 본문을 덮었음(앞 커밋으로 폭은 줄였으나 자리는 그대로였음).

- 좁아지면 인라인 좌표를 **떼어** 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 18:29:26 +09:00
co-authored by Claude Opus 5
parent ef4cdb667a
commit 4a47b9048d
2 changed files with 58 additions and 11 deletions
+2 -3
View File
@@ -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;
}
}
+56 -8
View File
@@ -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,
};
}