From 6bcd6d1209566924a0af37b613443ff9a289cb6a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 4 Aug 2026 19:53:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05/B06):=20=ED=95=98=EB=8B=A8=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=C2=B7=EC=A0=91=ED=9E=98=20=EA=B1=B0=ED=84=B0=C2=B7B06?= =?UTF-8?q?=20sticky=20Y=EC=B6=95=C2=B7=EB=B0=B0=EA=B4=80=20=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=20=EC=83=81=EC=8B=9C=20=ED=91=9C=EA=B8=B0(27~30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 27: 종단 X라벨↔버튼 간격 70%로(bottomInset 19→15) - 28: 사이드 패널 접힘 시 본문 24px 거터 — 손잡이가 Y축 라벨을 덮지 않음(B04~B07 공용) - 29: B06 종단·유토곡선 가로 스크롤 고정 Y축 — buildStickyYAxis 재사용, yaxis CSS 정의처를 MassHaul css로 이동(B05·B06 공용). 앵커를 첫 자식으로 넣고 유토축 inner를 종단 높이만큼 내려 자기 구간만 덮음. 스크롤 600px 블리드 테스트 통과 - 30: 배관 측점 곡선 L·R 항상 표기(R 필수), 이웃 곡선 셀 근접 시 배관 셀만 우측 비킴, 선택 오버레이는 한 열 원위치 유지 Playwright: sticky 앵커 2개(x 고정), 거터 24px, gap 7px, 곡선 셀 8, 오류 0. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_wf2_Route_UI_Profile_Panel.ts | 5 ++- .../B05_wf2_Route_UI_Profile_Table.ts | 38 +++++++++++++++---- B05_wf2_Route/B05_wf2_Route_UI_Style.css | 32 +--------------- .../B05_wf2_Route_UI_Style_MassHaul.css | 33 ++++++++++++++++ .../B06_wf3_ProfileCross_UI_Section_View.ts | 22 +++++++++++ ui_template/ui_template_workflow_layout.css | 6 +++ 6 files changed, 97 insertions(+), 39 deletions(-) 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 b6a658af..3346a19a 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -561,8 +561,9 @@ export function createRouteProfilePanel( if (target) callbacks?.onStructureMove?.(target.chainage_m, toChainage, target); }, // 계획고 편집 ▼ 버튼(바닥 2px + 높이 17px)과 측점 라벨이 겹치지 않게 X축·라벨을 - // 버튼 높이만큼 올린다(2026-08-04 사용자 지시). B06은 편집 버튼이 없어 0 유지. - 19, + // 올린다. 19px는 라벨-버튼 사이가 너무 벌어져 70% 수준(15px)으로 줄였다 + // (2026-08-04 사용자 지시). B06은 편집 버튼이 없어 0 유지. + 15, ), ); // 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts index eccec14c..d482bdd4 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -18,7 +18,11 @@ import type { ProfileAlignment, } from "./B05_wf2_Route_UI_Profile_Alignment"; import { stationLabel } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common"; -import { irregularStationId, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations"; +import { + irregularStationId, + isPipeStation, + type IrregularStation, +} from "./B05_wf2_Route_UI_IrregularStations"; export interface ProfileTableOptions { alignment: ProfileAlignment; @@ -297,6 +301,8 @@ function buildCurveRows( centers: number[], /** 구조물 측점 chainage 키(소수 3자리) — 이 열의 곡선 입력은 기본 표기하지 않는다. */ structureStationKeys: ReadonlySet, + /** 배관 구조물 chainage 키 — R이 필수라 곡선 L·R은 **항상** 표기한다(2026-08-04 지시). */ + pipeStationKeys: ReadonlySet, ): HTMLElement[] { const { alignment, onCurveRadiusChange } = options; const lengthRow = createRow( @@ -313,13 +319,25 @@ function buildCurveRows( alignment.curves.map((curve) => [curve.chainage_m.toFixed(3), curve]), ); + // 곡선 값이 든 셀들의 오른쪽 끝 — 배관 곡선 셀이 이웃 곡선 구간과 근접해 겹치면 + // 배관 쪽만 오른쪽으로 비켜 세운다(선택 하이라이트 오버레이는 원위치·한 열 그대로). + let lastContentRight = Number.NEGATIVE_INFINITY; alignment.stations.forEach((station, index) => { const lengthCell = element("span", "b05-profile-table__curve"); const radiusCell = element("span", "b05-profile-table__curve"); + const key = station.chainage_m.toFixed(3); + const isPipe = pipeStationKeys.has(key); // 구조물 측점 열은 곡선 입력도 기본 표기하지 않는다(빈 칸으로 격자만 유지). - const curve = structureStationKeys.has(station.chainage_m.toFixed(3)) - ? undefined - : curveByChainage.get(station.chainage_m.toFixed(3)); + // 예외: 배관 측점은 R이 필수 입력이라 곡선 L·R을 항상 보인다. + const curve = structureStationKeys.has(key) && !isPipe ? undefined : curveByChainage.get(key); + let center = centers[index]; + if (curve) { + const half = options.cellWidth / 2; + if (isPipe && center - half < lastContentRight + 2) { + center = lastContentRight + 2 + half; + } + lastContentRight = center + half; + } if (curve) { // L·R은 L = R × |대수차| 로 연동된다. 둘 다 입력 가능하고, 어느 쪽을 고쳐도 R로 환산해 // 같은 파이프라인(onCurveRadiusChange)을 태운다. 재계산 후 마지막에 입력한 값이 반영된다. @@ -343,8 +361,8 @@ function buildCurveRows( if (curve.omitted) cell.classList.add("is-omitted"); }); } - placeCell(lengthRow, centers[index], lengthCell); - placeCell(radiusRow, centers[index], radiusCell); + placeCell(lengthRow, center, lengthCell); + placeCell(radiusRow, center, radiusCell); }); function curveInput( @@ -530,6 +548,12 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { const structureStationKeys = new Set( (options.irregularStations ?? []).map((entry) => entry.chainage_m.toFixed(3)), ); + // 배관 구조물은 R이 필수라 곡선 행만은 기본 표기 예외다(2026-08-04 사용자 지시). + const pipeStationKeys = new Set( + (options.irregularStations ?? []) + .filter((entry) => isPipeStation(entry)) + .map((entry) => entry.chainage_m.toFixed(3)), + ); const selectedIrregularEntry = options.irregularStations?.find( (entry) => irregularStationId(entry.id) === options.selectedStationId, ); @@ -560,7 +584,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { }); table.append(row); }); - table.append(...buildCurveRows(options, centers, structureStationKeys)); + table.append(...buildCurveRows(options, centers, structureStationKeys, pipeStationKeys)); // 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다. // 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index 1982d631..cdd5a6e7 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -120,36 +120,8 @@ min-height: 0; } -/* 가로 스크롤에도 고정되는 Y축(표고 눈금). 0크기 sticky 앵커에 축을 절대배치해 레이아웃 불변. */ -.b05-profile__yaxis { - position: sticky; - left: 0; - z-index: 3; - width: 0; - height: 0; -} - -.b05-profile__yaxis-inner { - position: absolute; - left: 0; - top: 0; - box-sizing: border-box; - /* 자기 그래프 영역 밖으로 눈금이 새면 아래 그래프의 축처럼 읽힌다. */ - overflow: hidden; - /* SVG 배경(b06-chart__bg)과 같은 색으로 스크롤되는 그래프를 가려 값 누출을 막는다. */ - background: var(--color-surface-raised); - border-right: 1px solid var(--color-text-secondary); - pointer-events: none; -} - -.b05-profile__yaxis-tick { - position: absolute; - right: 6px; - color: var(--color-text-secondary); - font-size: 10px; - white-space: nowrap; - transform: translateY(-50%); -} +/* 가로 스크롤 고정 Y축(.b05-profile__yaxis*)의 정의처는 + B05_wf2_Route_UI_Style_MassHaul.css다 — buildStickyYAxis를 쓰는 B05·B06이 함께 얻는다. */ .b05-route-profile.is-collapsed .b05-route-profile__body-wrap, .b05-route-profile.is-collapsed .b05-route-profile__body, 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 8b89ed4f..6d02934d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style_MassHaul.css @@ -6,6 +6,39 @@ * 여기서는 종단면 패널 안에서 그것들이 놓이는 **자리**만 잡는다. * ========================================================================== */ +/* 가로 스크롤에도 고정되는 Y축(눈금 오버레이) — buildStickyYAxis(이 파일을 import하는 + _UI_Profile_MassHaul.ts)가 만든다. B05 종단·유토곡선과 B06 종단·유토곡선이 함께 쓰므로 + 여기가 정의처다. 0크기 sticky 앵커에 축을 절대배치해 레이아웃 불변. */ +.b05-profile__yaxis { + position: sticky; + left: 0; + z-index: 3; + width: 0; + height: 0; +} + +.b05-profile__yaxis-inner { + position: absolute; + left: 0; + top: 0; + box-sizing: border-box; + /* 자기 그래프 영역 밖으로 눈금이 새면 아래 그래프의 축처럼 읽힌다. */ + overflow: hidden; + /* SVG 배경(b06-chart__bg)과 같은 색으로 스크롤되는 그래프를 가려 값 누출을 막는다. */ + background: var(--color-surface-raised); + border-right: 1px solid var(--color-text-secondary); + pointer-events: none; +} + +.b05-profile__yaxis-tick { + position: absolute; + right: 6px; + color: var(--color-text-secondary); + font-size: 10px; + white-space: nowrap; + transform: translateY(-50%); +} + /* 본문 칸을 세로로 쌓아 그래프 스크롤러 아래에 손잡이가 붙게 한다. 오버레이의 절대배치 기준이기도 하다. */ .b05-route-profile__body-wrap { 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 fcaa9d4d..fd42a043 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts @@ -15,6 +15,8 @@ import { createPanelResizer } from "@ui/ui_template_resizer"; import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; +// 가로 스크롤 고정 Y축 — B05와 같은 오버레이를 쓴다(정의처: B05 MassHaul 모듈 + 그 CSS). +import { buildStickyYAxis } from "../B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul"; import type { CrossSection, EarthworkConversion, @@ -470,6 +472,10 @@ export function createSectionView( // 종단도 높이가 줄면 Y스케일도 그 높이로 다시 잡아야 표고가 잘리지 않는다. cachedYScale = calculateYScale(detail, heights.long); + // Y축 눈금을 렌더러에서 받아 가로 스크롤 고정 오버레이로 얹는다(2026-08-04 사용자 + // 지시 — 테이블 행 이름표처럼 스크롤해도 계속 보이게, B05와 같은 방식). + let longAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null; + let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null; const nodes: Element[] = [ unwrapChart( createLongitudinalProfile( @@ -483,6 +489,10 @@ export function createSectionView( heights.long, minWidth, detail.longitudinal.design_profiles ?? [], + 0, + (axis) => { + longAxis = axis; + }, ), ), ]; @@ -522,10 +532,22 @@ export function createSectionView( minWidth, (stationId) => selectStation(stationId, true), haulPlan, + (axis) => { + massAxis = axis; + }, ), ); } chartWrap.replaceChildren(...nodes); + // 고정 Y축 오버레이 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너 + // 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다). 유토곡선 축은 종단 높이만큼 + // 안쪽(inner)을 내려 자기 그래프 구간만 덮는다 — 불투명 배경이 지나간 눈금 노출을 막는다. + if (massAxis) { + const overlay = buildStickyYAxis(massAxis, heights.mass); + (overlay.firstElementChild as HTMLElement).style.top = `${heights.long}px`; + chartWrap.prepend(overlay); + } + if (longAxis) chartWrap.prepend(buildStickyYAxis(longAxis, heights.long)); panel.querySelector(".b06-masshaul__legend")?.remove(); panel.querySelector(".b06-masshaul__summary")?.remove(); diff --git a/ui_template/ui_template_workflow_layout.css b/ui_template/ui_template_workflow_layout.css index bdd50474..de496981 100644 --- a/ui_template/ui_template_workflow_layout.css +++ b/ui_template/ui_template_workflow_layout.css @@ -117,6 +117,12 @@ padding-left: min(var(--wf-left-panel-width), calc(100vw - var(--spacing-48))); } +/* 접힘 시에는 남은 손잡이(24px) 폭만큼 거터를 남긴다 — 손잡이가 그래프 Y축 라벨 등 + 본문 글자를 덮지 않게(2026-08-04 사용자 보고: 531.4m 라벨 가려짐). */ +.ui-workflow-layout:not(.is-options-open) .ui-workflow-layout__body { + padding-left: var(--spacing-24); +} + @media (max-width: 860px) { .ui-workflow-layout.is-options-open .ui-workflow-layout__body { padding-left: 0;