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}
-
-
+
`;
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