diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts index a6d169fe..10e9f558 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts @@ -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 => diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css index 7654b50c..179df812 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css @@ -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 {