diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 289199e3c..8cf1ac684 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -14,7 +14,7 @@ import { createDesignSync } from "../../B06_Section/B06_Section_UI_Page_Design_Sync"; import { leaveForDashboard } from "../b_missing_data_guard"; -import { readByKey, stateKey, writeByKey } from "../b_page_state"; +import { stateKey, writeByKey } from "../b_page_state"; import { showToast } from "@ui/ui_template_elements"; import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations"; import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures"; @@ -82,6 +82,10 @@ import { setPipeSectionsSource, } from "./design_structure_pipe_calc"; import { pipeRevetChoiceBridge } from "../../B06_Section/B06_Section_UI_Page_Link_Session"; +import { + DISPLAY_HALF_WIDTH_DEFAULT_M, + displayHalfWidth, +} from "../../B06_Section/B06_Section_Display_Width"; import { bindDesignView, bindPipeOptions, @@ -91,10 +95,6 @@ import { } from "./design_shared"; import "@util/common_util_mass_haul.css"; -/** 횡단면도 **표시** 반폭 기본값(m) — 2026-08-19 사용자 지시로 20 → 12. 계산·보유 샘플 폭은 - * 백엔드 기본(SECTION_CROSS_HALF_WIDTH_M = 20m) 그대로라 12는 표시 범위만 좁힌다. */ -const DISPLAY_HALF_WIDTH_DEFAULT_M = 10; - export async function mountCrossView( center: HTMLElement, host: DesignPageHost, @@ -635,23 +635,10 @@ export async function mountCrossView( // 확정 이력의 표준 횡단면 설정값 복원(진행 중 세션 편집값이 있으면 패널이 무시). if (storedOptions?.standard_cross_section) standardPanel?.applyStored(storedOptions.standard_cross_section); - /* 입력칸의 기준값은 **표시 기본 10m**. ⚠ `data.options.cross_half_width_m`은 종단·횡단을 - * 만들 때 쓴 **샘플링(계산) 반폭**이지 사용자가 고른 표시 폭이 아니다 — 그 값으로 덮으면 - * 표시 기본값이 언제나 20m로 되돌아온다(2026-08-19 사용자 보고의 원인). 사용자가 실제로 - * 고른 표시 폭만 되살린다: ① 측점 개별 표시 반폭 저장값의 대표값 ② 세션의 마지막 조절값. */ - const savedDisplayWidths = sectionDetail.cross_sections - .map((section) => section.design?.display_half_width_m) - .filter((width): width is number => typeof width === "number" && width > 0); - if (savedDisplayWidths.length) { - // 측점마다 다를 수 있다 — 전역 입력칸에는 가장 넓은 값을 올린다(잘림 방지). - crossHalfWidthField.input.value = Math.max(...savedDisplayWidths).toFixed(1); - } - const sessionDisplayKey = displaySessionKey(); - if (sessionDisplayKey) { - const sessionDisplay = Number(readByKey(sessionDisplayKey)); - if (Number.isFinite(sessionDisplay) && sessionDisplay > 0) - crossHalfWidthField.input.value = sessionDisplay.toFixed(1); - } + // 입력칸 = 보기 반폭 한 벌(세션 > 측점 저장 최댓값 > 기본 · B05 우측 판도 같이 읽음 · 393-4) + const shownWidth = displayHalfWidth(projectId, currentRouteId, sectionDetail.cross_sections); + if (shownWidth !== DISPLAY_HALF_WIDTH_DEFAULT_M) + crossHalfWidthField.input.value = shownWidth.toFixed(1); if (storedOptions?.station_interval_m && storedOptions.station_interval_m > 0) stationInterval = storedOptions.station_interval_m; // 종단 보기에서 고친 관 옵션(세월교 · BOX 스펙)을 캐시에 얹는 창구 — 담아 둔 것부터(24-3b). diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 14276f371..d693bcd98 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -116,15 +116,14 @@ function appliedHalfWidth(projectId: string): number | undefined { return Number.isFinite(value) && value > 0 ? value : undefined; } -function createPane(title: string, empty: string): CrossPane { +function createPane(title: string, tag: string, empty: string): CrossPane { const root = document.createElement("section"); root.className = "b05-routeedit__cross"; + root.setAttribute("aria-label", title); + // 제목 행 없음(393-4) — 측점은 그래프 왼쪽 위 작은 글(「10+0」 · 「이전 10+0」) root.innerHTML = ` -
- ${title} - -
+
${empty}
`; const station = root.querySelector(".b05-routeedit__cross-station")!; const foot = root.querySelector(".b05-routeedit__cross-foot")!; @@ -137,16 +136,10 @@ function createPane(title: string, empty: string): CrossPane { let source: RouteCrossSource | null = null; let chrome = 0; let interval = 20; - function drawCard(preview: CrossPreviewResponse, from: RouteCrossSource): void { + function drawCard(from: RouteCrossSource): void { const plotHeight = (): number => Math.max(120, (box.clientHeight || 150) - chrome); // 테두리 안쪽 const build = (): HTMLElement => { - const card = routeCrossCard( - from, - interval, - preview.cross_half_width_m, - size.width || 240, - plotHeight(), - ); + const card = routeCrossCard(from, interval, size.width || 240, plotHeight()); box.replaceChildren(card); return card; }; @@ -164,7 +157,7 @@ function createPane(title: string, empty: string): CrossPane { const rect = box.getBoundingClientRect(); size = { width: Math.round(rect.width), height: Math.round(rect.height) }; observer.observe(box); // 창에 붙은 뒤에 지켜본다 — 붙기 전에는 isConnected 가 거짓이다. - if (source) return drawCard(preview, source); + if (source) return drawCard(source); const plot = buildCrossSvg(preview, size.width || 240, size.height || 150); box.replaceChildren(plot.svg); // 확대·이동은 **공용 조각**이 맡는다 — B06 횡단 카드와 한 코드다(2026-09-12 지시 ③). @@ -204,7 +197,7 @@ function createPane(title: string, empty: string): CrossPane { } // 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물 // 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다. - station.textContent = formatStation(preview.chainage_m, intervalM); + station.textContent = `${tag}${formatStation(preview.chainage_m, intervalM)}`; draw(preview); foot.textContent = source ? "" : summarizeCross(preview); // 카드 = 그래프만(393-3) }, @@ -245,8 +238,12 @@ export function pipelineCrossAt( } export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWindow { - const current = createPane("횡단", "측점 눈금을 누르면 그 측점 횡단이 뜹니다."); - const previous = createPane("이전 횡단", "노선을 고치면 고치기 전 횡단이 여기 남습니다."); + const current = createPane("횡단", "", "측점 눈금을 누르면 그 측점 횡단이 뜹니다."); + const previous = createPane( + "이전 횡단", + "이전 ", + "노선을 고치면 고치기 전 횡단이 여기 남습니다.", + ); params.side.append(current.root, previous.root); const interval = (): number => params.request().station_interval_m; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts index 29b7ab62f..7bb1d1e87 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts @@ -8,6 +8,7 @@ * · 단면 사본(station_id 앞머리 · design 사본) — B06 고름 · 줌 기억 · 면적 원본을 안 건드림 * · 구조물 자리 · 암 경계 = 정본만 읽는 제어기(`Structure_Layouts` · 세월교 · 박스 조정값) · 누름 막음 * · 그래프만(393-3) — 카드 머리 · 줌 단추 · 발 줄 걷음 · 그래프가 판 칸을 다 씀 + * · 가로 범위 = B06 「횡단 반폭」 보기값 한 벌(`Display_Width` · 393-4) — B06 에서 바꾸면 다음 그림부터 * ========================================================================== */ import "../B06_Section/B06_Section_UI_Style_Cross_Controls.css"; @@ -16,6 +17,8 @@ import type { DesignPipeline } from "../A00_Common/design/logic/design_pipeline" import { nearestWithin } from "../A00_Common/design/design_selection"; import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import { peekSectionDetail } from "../B06_Section/B06_Section_Section_Store"; +import { displayHalfWidth } from "../B06_Section/B06_Section_Display_Width"; +import { pageRouteId } from "../A00_Common/design/design_page_route"; import { extraWalls, inletStructure, @@ -35,6 +38,8 @@ export interface RouteCrossSource { section: CrossSection; designZ: number | undefined; link: CulvertLink | undefined; + /** 보기 반폭(m) — B06 입력칸과 같은 값 · 그릴 때마다 읽음(B06 에서 바꾸면 다음 그림부터). */ + halfWidthM: () => number; } const none = (): void => undefined; @@ -72,6 +77,7 @@ export function routeCrossSource( link: found.culvert ? undefined : culvertLinkFor(found, sections, (at) => designElevationAt(profiles, at) ?? null), + halfWidthM: () => displayHalfWidth(projectId, pageRouteId(projectId), sections), }; } @@ -79,7 +85,6 @@ export function routeCrossSource( export function routeCrossCard( source: RouteCrossSource, intervalM: number, - halfWidthM: number | undefined, widthPx: number, plotHeightPx: number, ): HTMLElement { @@ -112,7 +117,7 @@ export function routeCrossCard( 1, none, intervalM, - halfWidthM, + source.halfWidthM(), widthPx, plotHeightPx, source.designZ, diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index 9bf9c4b2a..06b19ceee 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -208,6 +208,11 @@ display: flex; } +/* 판 머리 「횡단」 행 걷음(393-4) — 그 자리까지 그림 · 이름은 손잡이에 */ +.b05-routeedit__side-wrap .ui-side-panels__head { + display: none; +} + .b05-routeedit__side { display: flex; flex: 1 1 auto; @@ -218,6 +223,7 @@ } .b05-routeedit__cross { + position: relative; display: flex; flex: 1 1 0; min-height: 0; @@ -230,16 +236,22 @@ background: var(--color-surface-raised); } -.b05-routeedit__cross-head { - display: flex; - flex: none; - align-items: baseline; - gap: var(--spacing-8); +/* 측점 = 그래프 왼쪽 위 작은 글(393-4 · 제목 행 없음) */ +.b05-routeedit__cross-station { + position: absolute; + top: calc(var(--spacing-12) + 4px); + left: calc(var(--spacing-12) + 6px); + z-index: 1; + padding: 0 4px; + border-radius: var(--radius-4, 4px); + background: color-mix(in srgb, var(--color-surface-raised) 80%, transparent); + color: var(--color-text-secondary); + font-size: var(--text-caption); + pointer-events: none; } -.b05-routeedit__cross-station { - color: var(--color-text-secondary); - font-size: inherit; /* 44-2 — 제목(횡단)과 같은 크기 */ +.b05-routeedit__cross-station:empty { + display: none; } /* 횡단 판 — 캔버스 그림이 아니라 **SVG 선**이다(2026-09-12 사용자 지시 ③). diff --git a/B06_Section/B06_Section_Display_Width.ts b/B06_Section/B06_Section_Display_Width.ts new file mode 100644 index 000000000..c09d62e1b --- /dev/null +++ b/B06_Section/B06_Section_Display_Width.ts @@ -0,0 +1,29 @@ +/* ============================================================================= + * B06_Section_Display_Width.ts + * 횡단 보기 반폭(보기값 · m) 한 벌 — B06 좌측 「횡단 반폭」 입력칸 · B05 평면 우측 횡단 판이 같이 읽음(393-4). + * + * 세션 조절값([보기 반폭 적용] · 노선 단위) > 측점 저장 표시 반폭 최댓값 > 표시 기본 10m(PLAN 79-4). + * ⚠ `data.options.cross_half_width_m` 은 샘플링(계산) 반폭 — 보기값 아님. + * ========================================================================== */ + +import { readStateRaw } from "../A00_Common/b_page_state"; +import type { CrossSection } from "./B06_Section_Api_Fetch"; + +/** 표시 기본 반폭(m) — 계산 기본(20m)과 다름(2026-08-19 사용자 지시). */ +export const DISPLAY_HALF_WIDTH_DEFAULT_M = 10; + +export function displayHalfWidth( + projectId: string, + routeId: number | null, + sections: readonly CrossSection[], +): number { + const session = Number( + routeId === null ? NaN : readStateRaw("cross-display", projectId, routeId), + ); + if (Number.isFinite(session) && session > 0) return session; + // 측점마다 다를 수 있음 — 가장 넓은 값(잘림 방지) + const saved = sections + .map((section) => section.design?.display_half_width_m) + .filter((width): width is number => typeof width === "number" && width > 0); + return saved.length ? Math.max(...saved) : DISPLAY_HALF_WIDTH_DEFAULT_M; +} diff --git a/resources/tester/helper_b05_routeedit_cross_width.cjs b/resources/tester/helper_b05_routeedit_cross_width.cjs index c27f0dfd8..1a655ee86 100644 --- a/resources/tester/helper_b05_routeedit_cross_width.cjs +++ b/resources/tester/helper_b05_routeedit_cross_width.cjs @@ -47,6 +47,7 @@ const fakeEl = () => ({ innerHTML: "", querySelector: () => fakeEl(), replaceChildren() {}, + setAttribute() {}, append() {}, getBoundingClientRect: () => ({ width: 300, height: 200 }), }); @@ -221,6 +222,20 @@ async function run() { const before = shownCount; pick.selectDesign("p-1", { chainageM: null, structureId: null }, { from: "escape" }); out.cleared = { watched: cross.watched(), redrawn: shownCount > before }; + + // 393-4 — 보기 반폭 한 벌(B06 입력칸 · B05 판) = 세션 조절값 > 측점 저장 최댓값 > 기본 10 + const { displayHalfWidth } = loadTs( + path.join(ROOT, "B06_Section", "B06_Section_Display_Width.ts"), + ); + const saved = [12, 14, null].map((w) => ({ design: { display_half_width_m: w } })); + out.halfWidth = { + none: displayHalfWidth("p-9", 5, []), + saved: displayHalfWidth("p-9", 5, saved), + }; + session.setItem("aislo:draft:cross-display:p-9:5", "18"); + out.halfWidth.session = displayHalfWidth("p-9", 5, saved); + out.halfWidth.otherRoute = displayHalfWidth("p-9", 6, []); + out.halfWidth.noRoute = displayHalfWidth("p-9", null, saved); return out; } run().then((out) => console.log(JSON.stringify(out))); diff --git a/resources/tester/test_b05_routeedit_cross_width.py b/resources/tester/test_b05_routeedit_cross_width.py index c29a984ea..7b7744f6d 100644 --- a/resources/tester/test_b05_routeedit_cross_width.py +++ b/resources/tester/test_b05_routeedit_cross_width.py @@ -123,3 +123,42 @@ def test_393_3_판_칸은_그래프만(): assert ".b05-routeedit__cross-box:has(> .b06-cross-card) {" in css assert ".b05-routeedit__cross-foot:empty {\n display: none;" in css assert "footer button" not in css + + +def test_393_4_보기_반폭_한_벌(results): + # 세션 조절값([보기 반폭 적용]) > 측점 저장 표시 반폭 최댓값 > 기본 10 · 노선 단위 + assert results["halfWidth"] == { + "none": 10, + "saved": 14, + "session": 18, + "otherRoute": 10, + "noRoute": 14, + } + + +def test_393_4_제목_행_없음_반폭은_B06_값_같이_읽음(): + root = os.path.dirname(os.path.dirname(HERE)) + + def read(*parts): + with open(os.path.join(root, *parts), encoding="utf-8") as file: + return file.read() + + cross = read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts") + pane = cross.split("function createPane(", 1)[1].split("const station", 1)[0] + assert "cross-head" not in pane and "cross-title" not in pane + assert '
' in pane + assert "station.textContent = `${tag}${formatStation(" in cross + assert '"이전 횡단",\n "이전 ",' in cross + css = read("B05_Profile", "B05_Profile_UI_Style_RouteEdit.css") + assert ".b05-routeedit__side-wrap .ui-side-panels__head {\n display: none;" in css + assert "cross-head" not in css + # 반폭 = B06 입력칸과 같은 함수(사본 상수 없음) · 그릴 때마다 읽음 + card = read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts") + assert ( + "halfWidthM: () => displayHalfWidth(projectId, pageRouteId(projectId), sections)," in card + ) + assert "source.halfWidthM()," in card + assert "DISPLAY_HALF_WIDTH" not in card + b06 = read("A00_Common", "design", "design_view_cross.ts") + assert "displayHalfWidth(projectId, currentRouteId, sectionDetail.cross_sections)" in b06 + assert "const DISPLAY_HALF_WIDTH_DEFAULT_M" not in b06