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:
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 사용자 지시 ③).
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user