From 6273abc1ae00978c73a0135722422b5655520612 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 5 Aug 2026 19:57:06 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=ED=95=98=EB=8B=A8=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A7=84?= =?UTF-8?q?=EB=8F=99=C2=B7=EC=84=9C=EB=B8=8C=ED=8C=A8=EB=84=90=20=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=20=EA=B0=92=20=EB=B3=B5=EA=B7=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인 두 가지(2026-08-05 사용자 보고 분석): 1. 캐스케이드의 임시 축소(인라인 height)가 서브패널 자체 리사이저의 변수 값을 덮어씀 — 드래그 값이 화면에 안 먹다가 인라인이 걷힐 때 한 번에 튐 ('이전 배치로 복귀' 증상). → 서브패널 드래그 중엔 인라인을 걷고 임시 축소를 중단, 자리가 모자라면 메인 패널을 즉시 키워 밀어올림 2. 자동 확장(grow)이 메인 패널 드래그 중에도 개입해 포인터가 정한 높이를 되돌림 — 서로 밀고 당기는 진동('반복적으로 돌아가려는' 증상). → 드래그 출처 추적(pointerdown/up 플래그): 메인 드래그 중 grow 금지, 경량 동기화는 서브패널 드래그일 때만 grow 허용 Co-Authored-By: Claude Fable 5 --- .../B05_wf2_Route_UI_Profile_Panel.ts | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index 37c31a1b4..e55065289 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -304,6 +304,26 @@ export function createRouteProfilePanel( root.append(panelHandle.root, heightResizer.root, content); drainagePanel.load(projectId); + /* ── 리사이저 드래그 출처 추적(2026-08-05 진동·복귀 버그 수정) ────────────── + * 메인 패널을 끄는 중엔 grow(자동 확장)가 포인터와 싸우면 안 되고, 서브패널을 끄는 + * 중엔 캐스케이드의 임시 축소(인라인)가 드래그 변수 값을 덮으면 안 된다. */ + let mainPanelDragging = false; + let subPanelDragging = false; + heightResizer.root.addEventListener("pointerdown", () => { + mainPanelDragging = true; + }); + [massHaul.overlay, tableOverlay.overlay].forEach((overlay) => + overlay.querySelector(".ui-resizer")?.addEventListener("pointerdown", () => { + subPanelDragging = true; + }), + ); + const clearDragFlags = (): void => { + mainPanelDragging = false; + subPanelDragging = false; + }; + window.addEventListener("pointerup", clearDragFlags); + window.addEventListener("pointercancel", clearDragFlags); + let detail: SectionDetailResponse | null = null; let selectedStationId: string | null = null; let stationInterval: number | undefined; @@ -476,14 +496,22 @@ export function createRouteProfilePanel( * draw()(전체 재구성)와 리사이즈 중 경량 동기화가 **같은 계산**을 쓴다 — 끌 때는 * 이 함수만 프레임마다 돌리고, 무거운 차트·테이블 재구성은 손을 뗀 뒤 한 번만 한다. */ - function applyHeightCascade(): { chartHeight: number } { + function applyHeightCascade(allowGrow: boolean): { chartHeight: number } { const available = Math.max(120, body.clientHeight); const massOpen = !massHaul.overlay.hidden; const tableOpen = tableOverlay.isOpen(); + // 서브패널 리사이저를 끄는 중이면 그 높이가 **사용자 의도**다 — 임시 축소(인라인)로 + // 덮어쓰면 드래그 값이 안 먹다가 나중에 한 번에 튄다("이전 배치로 복귀" 증상의 원인). + // 이때는 인라인을 걷어 변수(드래그 값)가 그대로 보이게 하고, 자리가 모자라면 + // 아래 deficit 처리로 메인 패널을 키운다. + if (subPanelDragging) { + massHaul.overlay.style.height = ""; + tableOverlay.overlay.style.height = ""; + } let massHeight = massOpen ? massHaul.overlay.offsetHeight : 0; let tableOverlayHeight = tableOpen ? tableOverlay.overlay.offsetHeight : 0; const budget = Math.max(0, available - MIN_CHART_HEIGHT - MASSHAUL_HANDLE_GUTTER_PX); - if (massHeight + tableOverlayHeight > budget) { + if (!subPanelDragging && massHeight + tableOverlayHeight > budget) { // ② 같이 줄이기 — 줄일 수 있는 여유분에 비례해 축소, 각자 최소 높이 하한. const massMin = massOpen ? MASSHAUL_MIN_HEIGHT : 0; const tableMin = tableOpen ? TABLE_OVERLAY_MIN_HEIGHT : 0; @@ -496,7 +524,7 @@ export function createRouteProfilePanel( // 패널을 다시 키우면 원래 높이로 돌아온다. if (massOpen) massHaul.overlay.style.height = `${massHeight}px`; if (tableOpen) tableOverlay.overlay.style.height = `${tableOverlayHeight}px`; - } else { + } else if (!subPanelDragging && massHeight + tableOverlayHeight <= budget) { // 공간 충분 — 임시 축소 해제(저장된 변수 높이로 복귀). massHaul.overlay.style.height = ""; tableOverlay.overlay.style.height = ""; @@ -506,10 +534,12 @@ export function createRouteProfilePanel( // 접힌 유토곡선 손잡이는 바닥 고정(순서상 테이블 아래에서 나옴), 접힌 테이블 손잡이는 // 열린 유토곡선 위 경계(TableOverlay.syncHandlePosition). tableOverlay.setBottomOffset(massHeight); - // ③' 최소까지 줄여도 모자라면(서브패널이 갑자기 펼쳐진 경우) 메인 패널을 키운다. + // ③' 최소까지 줄여도 모자라면(서브패널 펼침·서브패널 드래그로 자리가 부족한 경우) + // 메인 패널을 키운다. 단 **메인 패널을 끄는 중에는 절대 안 된다** — 포인터가 정한 + // 높이를 되돌려 서로 밀고 당기는 진동("반복적으로 돌아가려는" 증상)이 생긴다. const deficit = MIN_CHART_HEIGHT + massHeight + tableOverlayHeight + MASSHAUL_HANDLE_GUTTER_PX - available; - if (deficit > 1) { + if (deficit > 1 && allowGrow && !mainPanelDragging) { const grown = Math.min(root.offsetHeight + deficit, Math.round(window.innerHeight * 0.92)); if (grown > root.offsetHeight + 1) { root.style.setProperty("--b05-profile-height", `${grown}px`); @@ -528,7 +558,8 @@ export function createRouteProfilePanel( * SVG는 잠깐 세로 스케일되지만, 손을 떼면(디바운스) 전체 재그리기가 정확히 다시 그린다. */ function syncHeightsLight(): void { if (!detail) return; - const { chartHeight } = applyHeightCascade(); + // 서브패널을 끄는 중엔 즉시 메인 패널을 밀어 올려야(grow) 드래그가 자연스럽다. + const { chartHeight } = applyHeightCascade(subPanelDragging); const canvas = body.firstElementChild as HTMLElement | null; if (!canvas || !canvas.classList.contains("b05-profile__canvas")) return; canvas.style.height = `${chartHeight}px`; @@ -572,7 +603,7 @@ export function createRouteProfilePanel( const x = chainageMapper(longitudinal, width, originOffset); // 그래프 40% : 테이블 60% (상단 정보 라인은 본문 밖이라 애초에 빠져 있다). // 가로 스크롤바를 `overflow-x: scroll`로 항상 띄우므로 clientHeight에서 이미 빠져 있다. - const { chartHeight } = applyHeightCascade(); + const { chartHeight } = applyHeightCascade(true); const tableHeight = tableOverlay.contentHeight(); const table = @@ -884,6 +915,8 @@ export function createRouteProfilePanel( window.clearTimeout(crossPreviewTimer); resizeObserver.disconnect(); heightResizer.dispose(); + window.removeEventListener("pointerup", clearDragFlags); + window.removeEventListener("pointercancel", clearDragFlags); }, }; }