From 15c2d81b49bb9207886513478750d2cceabc3760 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 13:56:01 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B06):=20=ED=9A=A1=EB=8B=A8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EA=BB=8D=EB=8D=B0=EA=B8=B0=C2=B7=EB=B0=98=ED=8F=AD?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=20=EB=B6=84=EB=A6=AC=20(700=EC=A4=84=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_UI_Cross_View` 784줄 → 698줄. 도면과 무관한 제목행·하단 정보행을 `_UI_Cross_Card_Chrome` 으로 옮기고, 실효 표시 반폭 계산은 순수 계산이라 `_UI_Cross_View_Metrics` 로, 측점별 줌·팬 상태 맵은 임자인 `_UI_Cross_View_Zoom` 으로 옮겼다. 계산·판정 규칙은 그대로다. 검증 — 공용 브라우저 B06 실측: 카드 65·제목행 65·하단행 65·단면유형 pill 65·사면 미교차 경고 13, 첫 카드 제목행 `0+0.0 / 0.0m / 토사·리핑암·발파암 / 편측 성토 / BP` 로 종전과 동일. pytest 383 passed·17 skipped, typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Card_Chrome.ts | 102 +++++++++++++++++ B06_Section/B06_Section_UI_Cross_View.ts | 106 ++---------------- .../B06_Section_UI_Cross_View_Metrics.ts | 27 +++++ B06_Section/B06_Section_UI_Cross_View_Zoom.ts | 3 + 4 files changed, 142 insertions(+), 96 deletions(-) create mode 100644 B06_Section/B06_Section_UI_Cross_Card_Chrome.ts diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts new file mode 100644 index 00000000..4d57f093 --- /dev/null +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -0,0 +1,102 @@ +/* ============================================================================= + * B06_Section_UI_Cross_Card_Chrome.ts + * 횡단 카드의 **껍데기**(제목행·하단 정보행)만 떼어 낸 조립기 (2026-09-03 · 700줄 제한). + * + * 카드 본체(`_UI_Cross_View`)는 SVG 도면과 조작을 맡고, 도면과 무관한 머리·꼬리는 여기서 + * 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다. + * ========================================================================== */ + +import type { CrossSection } from "./B06_Section_Api_Fetch"; +import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design"; +import { + buildDesignControls, + buildRockBoundaryControl, + sectionModeLabel, +} from "./B06_Section_UI_Cross_Design"; +import { type DesignChangeHandler, L, stationLabel } from "./B06_Section_UI_Section_Common"; + +/** + * 카드 제목행을 만들어 카드에 붙인다(설계 조작이 있으면 조작 바까지). + * + * 1행 구조(E-2/E-3): 측점 위치표기 → 지반유형 → 단면유형 pill → 구조물 → 측점정보. + */ +export function appendCardHeader( + card: HTMLElement, + section: CrossSection, + stationInterval: number, + onDesignChange?: DesignChangeHandler, +): void { + const header = document.createElement("header"); + const title = document.createElement("div"); + title.className = "b06-cross-card__title"; + const label = document.createElement("strong"); + label.textContent = stationLabel(section.chainage_m, stationInterval); + const chainage = document.createElement("span"); + chainage.textContent = `${section.chainage_m.toFixed(1)}m`; + title.append(label, chainage); + + // 단면유형 pill(자동 판정, 읽기 전용) — 구조물 pill과 동일 표기, 좌측 배치(E-3). + const modePill = document.createElement("span"); + modePill.className = "b06-cross-card__mode"; + modePill.textContent = sectionModeLabel(section.design?.section_mode); + modePill.title = L("B06_Design_Mode_Legend"); + + const meta = document.createElement("div"); + meta.className = "b06-cross-card__meta"; + // 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지 + // 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정). + if (section.design?.slope_unclosed) { + const openSlope = document.createElement("span"); + openSlope.className = "b06-cross-card__warning"; + openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`; + openSlope.title = L("B06_Cross_SlopeUnclosed_Tip"); + meta.append(openSlope); + } + if (section.structure) { + const structure = document.createElement("span"); + structure.className = "b06-cross-card__structure"; + structure.textContent = section.structure; + structure.title = `구조물: ${section.structure}`; + meta.append(structure); + } + // 측점 종류는 시·종점(BP/EP)만 적는다 — "일반측점"은 대다수라 정보가 없다(2026-08-02 사용자 지시). + if (section.kind === "bp" || section.kind === "ep") { + const kind = document.createElement("span"); + kind.textContent = L( + section.kind === "ep" ? "B06_Profile_View_Kind_EP" : "B06_Profile_View_Kind_BP", + ); + meta.append(kind); + } + + // 단면유형 pill은 지반유형 우측·우측 맞춤으로 배치(1번). 좌측 구분선은 지반유형 세그먼트에 준다(3번). + modePill.classList.add("b06-cross-card__mode--right"); + header.append(title); + if (onDesignChange) { + const controls = buildDesignControls(section, onDesignChange); + controls.groundSegment.classList.add("b06-cross-card__ground"); + header.append(controls.groundSegment, modePill, meta); + card.append(header, controls.bar); + return; + } + header.append(modePill, meta); + card.append(header); +} + +/** 하단 정보행 — 중심고와 (암 지반일 때) 암 경계선 제어. */ +export function appendCardFooter( + card: HTMLElement, + section: CrossSection, + rockBoundary?: RockBoundaryControl, +): void { + const footer = document.createElement("footer"); + const center = document.createElement("span"); + center.textContent = `${L("B06_Profile_View_CenterElevation")} ${section.center_z?.toFixed(2) ?? "-"}m`; + footer.append(center); + // 암 경계선 제어는 하단 정보 행 가운데(2026-08-02) — 암 지반만. + if (rockBoundary && section.design?.geometry_preset === "rock") { + const rockControl = buildRockBoundaryControl(section, rockBoundary); + rockControl.classList.add("b06-cross-card__rockb"); + footer.append(rockControl); + } + card.append(footer); +} diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 941785d2..fe7595aa 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -23,11 +23,9 @@ import { appendCrossDesignOverlay, appendPavementOverlay, appendRockBoundaryOverlay, - buildDesignControls, - buildRockBoundaryControl, - sectionModeLabel, type RockBoundaryControl, } from "./B06_Section_UI_Cross_Design"; +import { appendCardFooter, appendCardHeader } from "./B06_Section_UI_Cross_Card_Chrome"; import { appendCulvertOverlay } from "./B06_Section_UI_Cross_Culvert"; import type { CulvertDesignTrim } from "./B06_Section_UI_Cross_Culvert"; import { appendBoxOverlay, computeBoxLayout } from "./B06_Section_UI_Cross_Box"; @@ -35,7 +33,7 @@ import type { BoxControl } from "./B06_Section_UI_Cross_Box_Panel"; import { createBodyWiring } from "./B06_Section_UI_Cross_View_Bodies"; import { appendFordOverlay, computeFordLayout } from "./B06_Section_UI_Cross_Ford"; import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel"; -import { computeCardCulvert, culvertRequiredHalfWidth } from "./B06_Section_UI_Cross_Culvert_Wire"; +import { computeCardCulvert } from "./B06_Section_UI_Cross_Culvert_Wire"; import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire"; import type { ExtraWallControl, @@ -47,8 +45,8 @@ import type { import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel"; import { adoptAdjustPanel, releaseAdjustPanel } from "./B06_Section_UI_Adjust_Dock"; import { culvertCardState, structurePanelDeps } from "./B06_Section_UI_Cross_View_Structure"; -import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom"; -import type { CrossWidthActions, ZoomPanState } from "./B06_Section_UI_Cross_View_Zoom"; +import { attachZoomPan, buildZoomControls, cardZoomStates } from "./B06_Section_UI_Cross_View_Zoom"; +import type { CrossWidthActions } from "./B06_Section_UI_Cross_View_Zoom"; import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit"; import { createCrossAxes, IDENTITY_VIEW } from "./B06_Section_UI_Cross_Axes"; import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert"; @@ -60,12 +58,11 @@ import { type DesignChangeHandler, emptyView, L, - stationLabel, svgElement, svgText, validElevation, } from "./B06_Section_UI_Section_Common"; -import { crossPlotMetrics } from "./B06_Section_UI_Cross_View_Metrics"; +import { crossPlotMetrics, effectiveCardHalfWidth } from "./B06_Section_UI_Cross_View_Metrics"; export { crossCardNaturalHeight } from "./B06_Section_UI_Cross_View_Metrics"; /** @@ -113,9 +110,6 @@ export type { StructureSpanControl, } from "./B06_Section_UI_Cross_Culvert_Wire"; -/** 측점별 줌·팬 상태 — 카드 재생성에도 배율 유지(2026-08-22 사용자 ③). */ -const cardZoomStates = new Map(); - export function createCrossSectionCard( section: CrossSection, selected: boolean, @@ -154,24 +148,10 @@ export function createCrossSectionCard( // 선택·조정은 연다 — 연동을 풀어 이 측점 위치를 따로 잡을 수 있어야 한다 // (2026-08-24 사용자). 길이·전/후 같은 구간 값은 소유 측점 한 곳에서 관리한다. const isLinkedCulvert = !section.culvert && !!culvertLink; - // 실효 표시 반폭 — 개별값 > 전역값(2026-08-06). 절·성토선이 원지반과 만나는 지점 - // (교차점)이 반폭 밖이면 **이 카드만** 자동 줌아웃한다(2026-08-22, 판정 기준 개편 - // 2026-08-23: 배수관용 5m 사면 규칙 대신 실제 교차점 = toeFitHalfWidth). - // 보유 샘플 밖까지는 넓히지 않는다 — 지반이 없어 빈 화면이 될 뿐이고, 그 경우 - // [보기 반폭 적용]이 필요한 폭으로 백엔드 재생성을 건다. - const baseHalfWidth = stationWidth?.widthFor(section) ?? crossHalfWidth; - const sampledExtent = Math.max( - 0, - ...section.samples.map((sample) => Math.abs(sample.offset_m ?? 0)), + const effectiveHalfWidth = effectiveCardHalfWidth( + section, + stationWidth?.widthFor(section) ?? crossHalfWidth, ); - const requiredHalfWidth = Math.min( - Math.max(culvertRequiredHalfWidth(section) ?? 0, toeFitHalfWidth(section) ?? 0), - sampledExtent, - ); - const effectiveHalfWidth = - baseHalfWidth !== undefined && requiredHalfWidth > 0 - ? Math.max(baseHalfWidth, requiredHalfWidth) - : baseHalfWidth; const card: CrossCardElement = document.createElement("article"); card.id = `cross-${section.station_id}`; card.className = `b06-cross-card${selected ? " b06-cross-card--selected" : ""}`; @@ -291,63 +271,7 @@ export function createCrossSectionCard( onAreaSelect?.(section.station_id, activeArea); }; - // 제목행 1행 구조(E-2/E-3): 측점 위치표기 → 단면유형 pill → 지반유형 → 구조물 → 측점정보. - const header = document.createElement("header"); - const title = document.createElement("div"); - title.className = "b06-cross-card__title"; - const label = document.createElement("strong"); - label.textContent = stationLabel(section.chainage_m, stationInterval); - const chainage = document.createElement("span"); - chainage.textContent = `${section.chainage_m.toFixed(1)}m`; - title.append(label, chainage); - - // 단면유형 pill(자동 판정, 읽기 전용) — 구조물 pill과 동일 표기, 좌측 배치(E-3). - const modePill = document.createElement("span"); - modePill.className = "b06-cross-card__mode"; - modePill.textContent = sectionModeLabel(section.design?.section_mode); - modePill.title = L("B06_Design_Mode_Legend"); - - const meta = document.createElement("div"); - meta.className = "b06-cross-card__meta"; - // 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지 - // 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정). - if (section.design?.slope_unclosed) { - const openSlope = document.createElement("span"); - openSlope.className = "b06-cross-card__warning"; - openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`; - openSlope.title = L("B06_Cross_SlopeUnclosed_Tip"); - meta.append(openSlope); - } - if (section.structure) { - const structure = document.createElement("span"); - structure.className = "b06-cross-card__structure"; - structure.textContent = section.structure; - structure.title = `구조물: ${section.structure}`; - meta.append(structure); - } - // 측점 종류는 시·종점(BP/EP)만 적는다 — "일반측점"은 대다수라 정보가 없다(2026-08-02 사용자 지시). - if (section.kind === "bp" || section.kind === "ep") { - const kind = document.createElement("span"); - kind.textContent = L( - section.kind === "ep" ? "B06_Profile_View_Kind_EP" : "B06_Profile_View_Kind_BP", - ); - meta.append(kind); - } - - // 단면유형 pill은 지반유형 우측·우측 맞춤으로 배치(1번). 좌측 구분선은 지반유형 세그먼트에 준다(3번). - modePill.classList.add("b06-cross-card__mode--right"); - header.append(title); - if (onDesignChange) { - // 제목행: 측점 라벨 → (구분선) 지반유형 → 단면유형 pill(우측) → 구조물·kind (D-6/E-2/1·3번). - const controls = buildDesignControls(section, onDesignChange); - controls.groundSegment.classList.add("b06-cross-card__ground"); - header.append(controls.groundSegment, modePill, meta); - card.append(header); - card.append(controls.bar); - } else { - header.append(modePill, meta); - card.append(header); - } + appendCardHeader(card, section, stationInterval, onDesignChange); const metrics = crossPlotMetrics( section, @@ -769,16 +693,6 @@ export function createCrossSectionCard( card.append(chartWrap); } - const footer = document.createElement("footer"); - const center = document.createElement("span"); - center.textContent = `${L("B06_Profile_View_CenterElevation")} ${section.center_z?.toFixed(2) ?? "-"}m`; - footer.append(center); - // 암 경계선 제어는 하단 정보 행 가운데(2026-08-02) — 암 지반만. - if (rockBoundary && section.design?.geometry_preset === "rock") { - const rockControl = buildRockBoundaryControl(section, rockBoundary); - rockControl.classList.add("b06-cross-card__rockb"); - footer.append(rockControl); - } - card.append(footer); + appendCardFooter(card, section, rockBoundary); return card; } diff --git a/B06_Section/B06_Section_UI_Cross_View_Metrics.ts b/B06_Section/B06_Section_UI_Cross_View_Metrics.ts index 04cdd36b..29721ee5 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Metrics.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Metrics.ts @@ -1,5 +1,7 @@ import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import { CROSS_HEIGHT, CROSS_PAD, validElevation } from "./B06_Section_UI_Section_Common"; +import { culvertRequiredHalfWidth } from "./B06_Section_UI_Cross_Culvert_Wire"; +import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit"; const AREA_OVERLAY_HEADROOM_PX = 58; @@ -73,3 +75,28 @@ export function crossCardNaturalHeight( ?.heightPx ?? CROSS_HEIGHT ); } + +/** + * 카드가 실제로 그릴 표시 반폭 — 개별값 > 전역값(2026-08-06). + * + * 절·성토선이 원지반과 만나는 지점(교차점)이 반폭 밖이면 **이 카드만** 자동 줌아웃한다 + * (2026-08-22, 판정 기준 개편 2026-08-23: 배수관용 5m 사면 규칙 대신 실제 교차점). + * 보유 샘플 밖까지는 넓히지 않는다 — 지반이 없어 빈 화면이 될 뿐이고, 그 경우 + * [보기 반폭 적용]이 필요한 폭으로 백엔드 재생성을 건다. + */ +export function effectiveCardHalfWidth( + section: CrossSection, + baseHalfWidth: number | undefined, +): number | undefined { + const sampledExtent = Math.max( + 0, + ...section.samples.map((sample) => Math.abs(sample.offset_m ?? 0)), + ); + const requiredHalfWidth = Math.min( + Math.max(culvertRequiredHalfWidth(section) ?? 0, toeFitHalfWidth(section) ?? 0), + sampledExtent, + ); + return baseHalfWidth !== undefined && requiredHalfWidth > 0 + ? Math.max(baseHalfWidth, requiredHalfWidth) + : baseHalfWidth; +} diff --git a/B06_Section/B06_Section_UI_Cross_View_Zoom.ts b/B06_Section/B06_Section_UI_Cross_View_Zoom.ts index 271cd768..9ccb9de7 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Zoom.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Zoom.ts @@ -53,6 +53,9 @@ export interface ContentBounds { height: number; } +/** 측점별 줌·팬 상태 — 카드를 다시 만들어도 배율이 살아남는다(2026-08-22 사용자 ③). */ +export const cardZoomStates = new Map(); + export function attachZoomPan( svg: SVGSVGElement, plotLayer: SVGGElement,