From 05887b6a7e518dd76b0caa05f9b05e65aab70ca6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 4 Aug 2026 19:34:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05/ui):=20=EC=9C=A0=ED=86=A0=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EB=B0=94=C2=B7=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=EC=84=A0=C2=B7=EC=84=A0=ED=83=9D=20=ED=86=A0=EA=B8=80?= =?UTF-8?q?=C2=B7=ED=8C=A8=EB=84=90=20=EC=8B=9C=EC=9D=B8=EC=84=B1(21~25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 21: 유토곡선 높이를 스크롤러 clientHeight 기준으로 — 스크롤바가 Y축 라벨을 덮던 문제 해소, overflow-x scroll 고정으로 높이 안정화 - 22: 절토·성토 요약 줄을 종단 열 안으로 — 배수유역도 좌측 구분선 25px 끊김 해소 - 23: 선택 토글 해제 — 종단·유토곡선 측점 재클릭/빈 곳 클릭, 사이드바 구조물 재클릭 시 해제(null 전파로 3D·그래프·목록·유역 강조 동기 해제) - 24: 좌측 제목 패널(B04~B07 공용) 우측 가장자리 강조색 라인 - 25: 접힌 손잡이 강조색 배경 틴트 — 닫힌 패널이 있음을 상시 표시 Playwright: 재클릭 1→0, 빈곳 클릭 0, 스크롤러 226=곡선 226, 구분선 간극 1px, 접힘 틴트 확인, 콘솔 오류 0. typecheck 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_wf2_Route_UI_IrregularStations.ts | 5 ++- .../B05_wf2_Route_UI_Profile_MassHaul.ts | 22 ++++++++++--- .../B05_wf2_Route_UI_Profile_Panel.ts | 33 ++++++++++++++++--- .../B05_wf2_Route_UI_Style_MassHaul.css | 6 ++-- ui_template/ui_template_overlay.css | 17 ++++++++++ 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts index 19d38990..773228f1 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts @@ -165,6 +165,8 @@ export function createIrregularStationsSection( remainderField.value = target ? String(target.remainder) : ""; structureField.value = target?.structure ?? ""; syncButtons(); + // 선택 강조(is-selected)가 클릭 즉시 따라오도록 목록을 다시 그린다(토글 해제 포함). + renderList(); callbacks.onSelect(target ?? null); } @@ -188,7 +190,8 @@ export function createIrregularStationsSection( const info = document.createElement("span"); info.textContent = station.structure || "(구조물 미입력)"; item.append(name, info); - item.addEventListener("click", () => loadForm(station)); + // 같은 항목 재클릭 = 선택 해제(2026-08-04 사용자 지시) — 그래프·3D 강조도 함께 풀린다. + item.addEventListener("click", () => loadForm(station.id === editingId ? null : station)); list.append(item); // 목록이 5개를 넘으면 내부 스크롤이 생긴다 — 선택(=수정 중) 항목이 스크롤 밖에 // 있으면 안 보이므로, 렌더 직후 그 항목만 보이는 위치로 데려온다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts index e3e47c9d..010a0768 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts @@ -118,6 +118,8 @@ export interface RouteMassHaulDrawParams { widthPx: number; selectedStationId: string | null; onSelectStation: (stationId: string) => void; + /** 측점선이 아닌 빈 곳 클릭 — 선택 해제(2026-08-04 사용자 지시). */ + onClearSelection?: () => void; } export interface RouteMassHaulPanel { @@ -192,6 +194,13 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa }); overlay.append(resizer.root, bar, scroll, legendLayer); + // 측점선이 아닌 빈 곳 클릭 = 선택 해제 — 종단 그래프와 같은 규칙(2026-08-04 사용자 지시). + let clearSelection: (() => void) | undefined; + scroll.addEventListener("click", (event) => { + if ((event.target as HTMLElement).closest(".b06-chart__station")) return; + clearSelection?.(); + }); + // 종단 그래프 영역과 같은 문법 — 세로 휠을 가로 이동으로 돌린다(2026-08-04 사용자 지시). // scrollLeft 동기화 덕에 위 종단 그래프도 함께 움직인다. Shift+휠은 브라우저 기본 그대로. scroll.addEventListener( @@ -274,6 +283,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa } function draw(params: RouteMassHaulDrawParams): void { + clearSelection = params.onClearSelection; overlay.hidden = !open; syncHandlePosition(); if (!open) return; @@ -304,10 +314,14 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa const summarySeries = banded ?? series[0]; bar.append(createMassHaulSummary(summarySeries, haulPlan)); - // 곡선 몫 = 오버레이에서 요약 막대를 뺀 나머지. 첫 렌더(레이아웃 전)에는 0이 나올 수 - // 있어 기본 높이로 시작한다 — Panel이 다음 프레임에 한 번 더 그려 실측으로 수렴한다. - const overlayHeight = overlay.clientHeight || OVERLAY_DEFAULT_HEIGHT; - const chartHeight = Math.max(overlayHeight - bar.offsetHeight, MASS_HAUL_MIN_HEIGHT); + // 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체 + // 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고). + // 첫 렌더(레이아웃 전)에는 0이 나올 수 있어 기본 높이로 시작한다 — Panel이 다음 + // 프레임에 한 번 더 그려 실측으로 수렴한다. + const scrollHeight = + scroll.clientHeight || + (overlay.clientHeight || OVERLAY_DEFAULT_HEIGHT) - bar.offsetHeight - 12; + const chartHeight = Math.max(scrollHeight, MASS_HAUL_MIN_HEIGHT); let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null; const chart = createMassHaulChart( 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 49a8499a..b6a658af 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -218,7 +218,8 @@ export interface RouteProfilePanelCallbacks { export function createRouteProfilePanel( projectId: string, - onSelectStation: (stationId: string) => void, + /** 측점 선택 변경 — 같은 측점 재선택·빈 곳 클릭이면 null(해제)로 알린다. */ + onSelectStation: (stationId: string | null) => void, /** [초기선 복원] 클릭 시 함께 실행(비정규 측점 등 다른 조작값도 초기화하려고 Page가 넘긴다). */ onResetAll?: () => void, callbacks?: RouteProfilePanelCallbacks, @@ -243,7 +244,9 @@ export function createRouteProfilePanel( progress.root.hidden = true; const bodyWrap = document.createElement("div"); bodyWrap.className = "b05-route-profile__body-wrap"; - bodyWrap.append(body, progress.root); + // 절토·성토 요약 줄은 **종단 쪽 열 안**에 둔다 — 패널 전체 폭에 걸치면 우측 배수유역도 + // 구분선이 그 높이만큼 끊겨 보인다(2026-08-04 사용자 보고). + bodyWrap.append(balanceBar, body, progress.root); // 유토곡선 — 패널 바닥에 붙는 오버레이 서브패널(2026-08-04 사용자 확정). // 종단도·테이블 배치는 건드리지 않고 그 위를 덮으며, 위 경계 리사이저로 높이를 조절한다. const massHaul = createRouteMassHaulPanel(() => draw()); @@ -269,7 +272,7 @@ export function createRouteProfilePanel( max: () => (root.parentElement?.clientHeight ?? window.innerHeight) * MAX_PANEL_HEIGHT_RATIO, storageKey: HEIGHT_KEY, }); - root.append(panelHandle.root, heightResizer.root, balanceBar, content); + root.append(panelHandle.root, heightResizer.root, content); drainagePanel.load(projectId); let detail: SectionDetailResponse | null = null; @@ -292,6 +295,17 @@ export function createRouteProfilePanel( /** 처음 그릴 때 잡힌 도면 테이블 높이(px). 패널을 끌어도 이 값을 지킨다. */ let fixedTableHeight = Number(sessionStorage.getItem(TABLE_HEIGHT_KEY)) || 0; + /** + * 측점 선택 — 같은 측점 재선택이면 해제한다(2026-08-04 사용자 지시). + * 그래프(종단·유토곡선) 클릭이 모두 이 경로를 탄다. 해제는 null로 Page에 알린다. + */ + function selectStation(stationId: string | null): void { + const next = stationId !== null && stationId === selectedStationId ? null : stationId; + selectedStationId = next; + draw(); + onSelectStation(next); + } + function renderBalance(): void { balanceBar.replaceChildren(); if (!alignment) { @@ -501,6 +515,12 @@ export function createRouteProfilePanel( const chartWrap = document.createElement("div"); chartWrap.className = "b05-profile__chart"; chartWrap.style.height = `${chartHeight}px`; + // 측점선이 아닌 빈 곳을 누르면 선택 해제(2026-08-04 사용자 지시). + // 측점 마커·편집 버튼 클릭은 각자 처리하므로 여기까지 안 온다(closest·stopPropagation). + chartWrap.addEventListener("click", (event) => { + if ((event.target as HTMLElement).closest(".b06-chart__station")) return; + if (selectedStationId !== null) selectStation(null); + }); const designProfiles = alignment ? [toDesignProfile(alignment, longitudinal.design_profiles?.[0])] : (longitudinal.design_profiles ?? []); @@ -522,7 +542,7 @@ export function createRouteProfilePanel( selectedStationId, 1, undefined, - onSelectStation, + selectStation, stationInterval, width, chartHeight, @@ -608,7 +628,10 @@ export function createRouteProfilePanel( stationInterval: stationIntervalM ?? 1, widthPx: width, selectedStationId, - onSelectStation, + onSelectStation: selectStation, + onClearSelection: () => { + if (selectedStationId !== null) selectStation(null); + }, }); } } diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css b/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css index 0d2c1d26..8b89ed4f 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css @@ -44,12 +44,14 @@ background: var(--color-surface); } -/* 곡선 가로 스크롤러 — 종단 스크롤러와 scrollLeft를 동기화한다(JS). 세로는 잘라낸다. */ +/* 곡선 가로 스크롤러 — 종단 스크롤러와 scrollLeft를 동기화한다(JS). 세로는 잘라낸다. + 스크롤바는 항상 표시(scroll) — auto면 생겼다 없어질 때 곡선 높이가 널뛰고, + clientHeight 기반 높이 계산도 함께 흔들린다. */ .b05-profile__masshaul-scroll { position: relative; flex: 1 1 auto; min-height: 0; - overflow-x: auto; + overflow-x: scroll; overflow-y: hidden; } diff --git a/ui_template/ui_template_overlay.css b/ui_template/ui_template_overlay.css index d642a56c..2a0cc335 100644 --- a/ui_template/ui_template_overlay.css +++ b/ui_template/ui_template_overlay.css @@ -28,6 +28,13 @@ border-top: 0; border-bottom: 0; border-left: 0; + /* 크기 조절은 안 되지만 조절 패널들과 같은 강조색 가장자리(2026-08-04 사용자 지시) + — 리사이저 상시 표시선과 같은 색·기조로 경계가 구분되게 한다. */ + border-right-color: color-mix( + in srgb, + var(--panel-handle-border, var(--color-border)) 75%, + transparent + ); border-radius: 0; transition: transform var(--transition-base); } @@ -105,6 +112,16 @@ transition: transform var(--transition-fast); } +/* 닫힌 패널의 손잡이 — 접힌 내용이 있음을 강조색 배경 틴트로 알린다(2026-08-04 제안 적용). + 열려 있으면 평상 배경이라, 틴트가 있는 손잡이 = "펼칠 것이 있다"로 읽힌다. */ +.ui-workflow-overlay__handle[aria-expanded="false"] { + background: color-mix( + in srgb, + var(--panel-handle-border, var(--color-primary)) 20%, + var(--color-surface-raised) + ); +} + /* 이름표 달린 손잡이 — 화살표+텍스트를 나란히 담도록 고정 폭을 푼다. 화살표(삼각형)는 flex 압축에서 제외해 이름표 유무와 무관하게 같은 크기를 유지한다. */ .ui-workflow-overlay__handle.is-labeled {