feat(393-4): B05 우측 횡단 판 제목 행 걷음 · 측점은 그래프 왼쪽 위 작은 글 · 가로 범위 = B06 「횡단 반폭」 보기값 한 벌(B06_Section_Display_Width — B06 입력칸 · B05 판 같이 읽음 · 그릴 때마다)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-17 20:02:22 +09:00
co-authored by Claude Opus 5.5
parent 1c0f1af4f0
commit bd78a41c90
7 changed files with 133 additions and 49 deletions
+9 -22
View File
@@ -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).
+14 -17
View File
@@ -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 = `
<div class="b05-routeedit__cross-head">
<strong class="b05-routeedit__cross-title">${title}</strong>
<span class="b05-routeedit__cross-station"></span>
</div>
<div class="b05-routeedit__cross-box"></div>
<span class="b05-routeedit__cross-station"></span>
<div class="b05-routeedit__cross-foot">${empty}</div>`;
const station = root.querySelector<HTMLElement>(".b05-routeedit__cross-station")!;
const foot = root.querySelector<HTMLElement>(".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;
@@ -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,
+20 -8
View File
@@ -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 사용자 지시 ③).
+29
View File
@@ -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;
}
@@ -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)));
@@ -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 '<div class="b05-routeedit__cross-box"></div>' 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