fix(B05): 하단 패널 리사이즈 진동·서브패널 드래그 값 복귀 버그
원인 두 가지(2026-08-05 사용자 보고 분석):
1. 캐스케이드의 임시 축소(인라인 height)가 서브패널 자체 리사이저의 변수 값을
덮어씀 — 드래그 값이 화면에 안 먹다가 인라인이 걷힐 때 한 번에 튐
('이전 배치로 복귀' 증상). → 서브패널 드래그 중엔 인라인을 걷고 임시 축소를
중단, 자리가 모자라면 메인 패널을 즉시 키워 밀어올림
2. 자동 확장(grow)이 메인 패널 드래그 중에도 개입해 포인터가 정한 높이를
되돌림 — 서로 밀고 당기는 진동('반복적으로 돌아가려는' 증상).
→ 드래그 출처 추적(pointerdown/up 플래그): 메인 드래그 중 grow 금지,
경량 동기화는 서브패널 드래그일 때만 grow 허용
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user