feat(B05/ui): 유토곡선 스크롤바·구분선·선택 토글·패널 시인성(21~25)
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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개를 넘으면 내부 스크롤이 생긴다 — 선택(=수정 중) 항목이 스크롤 밖에
|
||||
// 있으면 안 보이므로, 렌더 직후 그 항목만 보이는 위치로 데려온다.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user