fix(B06): 선택 반영을 두 카드로 좁히고 그래프 세로 스크롤 제거
- 선택이 바뀔 때마다 draw()로 모든 카드를 다시 만들어 카드별 휠 줌·팬이 통째로 초기화됐다. 이전·현재 선택 카드 두 장만 교체하고 상단 패널만 다시 그린다. 선택된 카드 안에서 값 칸을 토글할 때는 원래부터 재렌더가 없다(DOM 클래스만 바뀐다). - 접기 손잡이에 전역 캐럿(▸)과 삼각형 아이콘(▼)이 겹쳐 보이던 문제: 같은 특정도로 지우려 해 로드 순서에서 밀렸다. 자식 결합자로 특정도를 올려 지운다. - 그래프 상자 세로 스크롤 제거: 축소 시 두 그래프를 각자 비율로 줄이다 하한에 걸리면 합이 배분 몫을 넘었다. 몫을 먼저 잡고 유토곡선 몫을 종단도 하한이 남도록 클램프해 합이 항상 몫과 같게 했다. 한 축만 auto면 나머지 축도 auto로 올라가므로 overflow-y: hidden도 건다. - 패널 위 휠은 끝에 닿아도 기본 동작을 넘기지 않는다(아래 횡단도 목록이 대신 스크롤되던 현상). 범례 줄이 먼저 먹지 않도록 캡처 단계에서 잡는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,17 +99,24 @@ function chartHeights(panelHeightPx: number): { long: number; mass: number } {
|
||||
if (!Number.isFinite(panelHeightPx) || panelHeightPx <= 0) {
|
||||
return { long: LONG_HEIGHT, mass: MASS_HAUL_HEIGHT };
|
||||
}
|
||||
if (panelHeightPx >= BASE_PANEL_HEIGHT) {
|
||||
return {
|
||||
long: LONG_HEIGHT,
|
||||
mass: Math.max(MASS_HAUL_MIN_HEIGHT, panelHeightPx - PANEL_CHROME_PX - LONG_HEIGHT),
|
||||
};
|
||||
// 두 그래프가 나눠 가질 실제 몫. **합이 이 값을 넘으면 안 된다** — 넘는 만큼 그래프 상자에
|
||||
// 세로 스크롤이 생긴다(2026-08-02 사용자 지적). 예전에는 축소 시 각자 비율로 줄이면서
|
||||
// 하한(110/90)에 걸리면 합이 몫을 넘었다.
|
||||
const available = Math.max(
|
||||
panelHeightPx - PANEL_CHROME_PX,
|
||||
MIN_LONG_HEIGHT + MASS_HAUL_MIN_HEIGHT,
|
||||
);
|
||||
// 기본 높이 이상이면 종단도는 고정, 늘어난 몫은 유토곡선이 전부 흡수한다.
|
||||
if (available >= LONG_HEIGHT + MASS_HAUL_HEIGHT) {
|
||||
return { long: LONG_HEIGHT, mass: available - LONG_HEIGHT };
|
||||
}
|
||||
const ratio = panelHeightPx / BASE_PANEL_HEIGHT;
|
||||
return {
|
||||
long: Math.max(MIN_LONG_HEIGHT, Math.round(LONG_HEIGHT * ratio)),
|
||||
mass: Math.max(MASS_HAUL_MIN_HEIGHT, Math.round(MASS_HAUL_HEIGHT * ratio)),
|
||||
};
|
||||
const ratio = available / (LONG_HEIGHT + MASS_HAUL_HEIGHT);
|
||||
// 유토곡선 몫을 먼저 잡되 종단도 하한을 남겨 둔다 — 그래야 두 하한이 동시에 걸려도 합이 넘지 않는다.
|
||||
const mass = Math.min(
|
||||
Math.max(MASS_HAUL_MIN_HEIGHT, Math.round(MASS_HAUL_HEIGHT * ratio)),
|
||||
available - MIN_LONG_HEIGHT,
|
||||
);
|
||||
return { long: available - mass, mass };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,22 +233,23 @@ export function createSectionView(
|
||||
panel.append(panelResizer.root);
|
||||
|
||||
/**
|
||||
* 패널 위에서 굴린 휠은 페이지가 아니라 **그래프를 좌우로** 민다(2026-08-02 사용자 지시).
|
||||
* 가로로 더 밀 곳이 없으면 기본 동작(세로 스크롤)을 그대로 넘긴다.
|
||||
* 패널 **어디에서든** 굴린 휠은 페이지가 아니라 그래프를 좌우로 민다(2026-08-02 사용자 지시).
|
||||
*
|
||||
* 끝에 닿았다고 기본 동작을 넘기면 아래 횡단도 목록이 대신 스크롤돼 화면이 튄다 — 패널 위에
|
||||
* 커서가 있는 동안은 세로 스크롤을 통째로 막는다. 캡처 단계에서 잡는 이유는 패널 안의 다른
|
||||
* 가로 스크롤 상자(범례 줄)가 먼저 먹어 버리지 않게 하기 위해서다.
|
||||
*/
|
||||
panel.addEventListener(
|
||||
"wheel",
|
||||
(event) => {
|
||||
if (event.ctrlKey) return; // 브라우저 확대 제스처는 건드리지 않는다
|
||||
event.preventDefault();
|
||||
const room = chartWrap.scrollWidth - chartWrap.clientWidth;
|
||||
if (room <= 1) return;
|
||||
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
|
||||
if (!delta) return;
|
||||
const before = chartWrap.scrollLeft;
|
||||
chartWrap.scrollLeft = Math.min(Math.max(before + delta, 0), room);
|
||||
if (chartWrap.scrollLeft !== before) event.preventDefault();
|
||||
if (room <= 1 || !delta) return;
|
||||
chartWrap.scrollLeft = Math.min(Math.max(chartWrap.scrollLeft + delta, 0), room);
|
||||
},
|
||||
{ passive: false },
|
||||
{ passive: false, capture: true },
|
||||
);
|
||||
|
||||
const panelHeight = (): number => {
|
||||
@@ -273,13 +281,37 @@ export function createSectionView(
|
||||
document.getElementById(`cross-${stationId}`)?.scrollIntoView({ behavior, block: "nearest" });
|
||||
};
|
||||
|
||||
/** 측점 id로 카드 하나만 새로 만들어 교체한다(행 높이는 draw가 정해 둔 값을 그대로 쓴다). */
|
||||
const rebuildCard = (stationId: string | null): void => {
|
||||
if (!stationId || !currentDetail) return;
|
||||
const section = currentDetail.cross_sections.find(
|
||||
(candidate) => candidate.station_id === stationId,
|
||||
);
|
||||
const existing = section && document.getElementById(`cross-${stationId}`);
|
||||
if (section && existing) {
|
||||
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(stationId)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 선택이 바뀐 흔적만 화면에 반영한다 — **전체 재렌더 금지**.
|
||||
* `draw()`로 모든 카드를 다시 만들면 사용자가 카드마다 맞춰 둔 휠 줌·팬(viewBox)이 전부
|
||||
* 초기화된다(2026-08-02 사용자 지적). 선택 표시가 바뀌는 카드는 이전·현재 둘뿐이다.
|
||||
*/
|
||||
const applySelection = (previousStationId: string | null): void => {
|
||||
rebuildCard(previousStationId);
|
||||
if (selectedStationId !== previousStationId) rebuildCard(selectedStationId);
|
||||
drawPanel();
|
||||
};
|
||||
|
||||
/** 같은 측점을 다시 고르면 선택을 푼다(2026-08-02 사용자 지시) — 카드·측점선 어느 쪽이든. */
|
||||
const selectStation = (stationId: string, scroll: boolean): void => {
|
||||
const previous = selectedStationId;
|
||||
const deselect = selectedStationId === stationId;
|
||||
selectedStationId = deselect ? null : stationId;
|
||||
// 측점이 바뀌면 면적 강조는 따라가지 않는다(다른 측점의 강조를 물려받으면 오해를 부른다).
|
||||
activeAreaKey = null;
|
||||
draw();
|
||||
applySelection(previous);
|
||||
if (scroll && !deselect) revealCard(stationId, "smooth");
|
||||
};
|
||||
|
||||
@@ -294,18 +326,20 @@ export function createSectionView(
|
||||
activeAreaKey = key;
|
||||
return;
|
||||
}
|
||||
const previous = selectedStationId;
|
||||
selectedStationId = stationId;
|
||||
activeAreaKey = key;
|
||||
draw();
|
||||
applySelection(previous);
|
||||
revealCard(stationId, "smooth");
|
||||
};
|
||||
|
||||
/** 카드 바깥(그리드 빈 자리)을 누르면 선택을 푼다(2026-08-02 사용자 지시). */
|
||||
const clearSelection = (): void => {
|
||||
if (!selectedStationId && !activeAreaKey) return;
|
||||
const previous = selectedStationId;
|
||||
selectedStationId = null;
|
||||
activeAreaKey = null;
|
||||
draw();
|
||||
applySelection(previous);
|
||||
};
|
||||
|
||||
const buildCrossCard = (section: CrossSection, forcedHeightPx?: number): HTMLElement =>
|
||||
|
||||
@@ -115,7 +115,9 @@
|
||||
border-radius: 0 0 var(--radius-buttons) var(--radius-buttons);
|
||||
}
|
||||
|
||||
.b06-section__panel-toggle::after {
|
||||
/* 전역 `.ui-collapsible__title::after` 캐럿을 지운다. 같은 특정도로 쓰면 로드 순서에 따라
|
||||
전역이 이겨 캐럿(▸)과 삼각형 아이콘(▼)이 **둘 다** 보인다 — 자식 결합자로 특정도를 올린다. */
|
||||
.b06-section__panel > .b06-section__panel-toggle::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@@ -128,6 +130,9 @@
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: auto;
|
||||
/* 한 축만 auto로 두면 브라우저가 나머지 축도 auto로 올려 세로 스크롤이 생긴다.
|
||||
높이는 `chartHeights()`가 정확히 맞추므로 잘릴 것도 없다. */
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.b06-section__chart {
|
||||
|
||||
Reference in New Issue
Block a user