From ac6adcac8847a3acbe3c7454e45a4a2e1a0fe109 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 25 Jul 2026 14:21:12 +0900 Subject: [PATCH] 260725_5 --- B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts | 18 +++++++++++++++++- .../B05_wf2_Route_UI_Profile_Panel.ts | 5 +++++ .../B06_wf3_ProfileCross_UI_Cross_Design.ts | 17 +++++++++++++++++ .../B06_wf3_ProfileCross_UI_Page.ts | 14 +++++++++----- .../B06_wf3_ProfileCross_UI_Standard_Panel.ts | 5 +++-- .../B06_wf3_ProfileCross_UI_Style.css | 15 +++++++++++---- .../B06_wf3_ProfileCross_UI_Style_Cross.css | 6 ++++++ ui_template/ui_template_locale.ts | 4 ++-- 8 files changed, 70 insertions(+), 14 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts index 655fb2b3..621b7449 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Edit.ts @@ -160,6 +160,8 @@ export interface EditOverlayOptions { onStation: (chainageM: number, delta: number) => void; onSegment: (segment: AlignmentSegment, delta: number) => void; onResetStation: (chainageM: number) => void; + /** 구간 평행이동 편집을 자동 선형으로 원복(양 끝 오프셋 삭제). */ + onResetSegment: (segment: AlignmentSegment) => void; } function overlayButton( @@ -213,7 +215,8 @@ function planElevationAtSample(alignment: ProfileAlignment, chainageM: number): /** 그래프 영역 위에 겹치는 편집 버튼 층을 만든다 (선 자체는 가리지 않는다). */ export function createEditOverlay(options: EditOverlayOptions): HTMLElement { - const { alignment, width, x, step, onStation, onSegment, onResetStation } = options; + const { alignment, width, x, step, onStation, onSegment, onResetStation, onResetSegment } = + options; const irregular = options.irregularStations ?? []; const layer = document.createElement("div"); layer.className = "b05-profile-edit"; @@ -305,6 +308,19 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement { ); down.style.left = `${center - BUTTON_HALF_PX}px`; layer.append(up, down); + + // 구간 양 끝 중 하나라도 편집됐으면 원복 ↺ 노출(측점 ↺와 동일 스타일·연산). + if (edited.has(chainageKey(segment.from_m)) || edited.has(chainageKey(segment.to_m))) { + const reset = overlayButton( + repeater, + "is-segment is-reset", + "↺", + `${label} — 자동 선형으로 원복`, + () => onResetSegment(segment), + ); + reset.style.left = `${center - BUTTON_HALF_PX}px`; + layer.append(reset); + } }); return layer; diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index e9b6e0f1..2202d33d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -499,6 +499,11 @@ export function createRouteProfilePanel( onSegment: (segment, delta) => base && applyEdits(shiftSegment(base, store.edits(), segment, delta)), onResetStation: (chainage) => store.resetStation(chainage), + // 구간 원복 = 양 끝 측점 오프셋 삭제(측점 원복 연산 ×2). + onResetSegment: (segment) => { + store.resetStation(segment.from_m); + store.resetStation(segment.to_m); + }, }), ); } diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Design.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Design.ts index fa215f05..b92c8494 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Design.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Design.ts @@ -408,6 +408,23 @@ export function appendCrossDesignOverlay( polyline.setAttribute("points", points.join(" ")); polyline.setAttribute("class", "b06-chart__design-cross"); svg.append(polyline); + + // 차도·노견 경계 짧은 수직 틱(N-4-2): 노면 단일 기울기라 육안 구분이 안 되는 경계를 + // 표시한다. 노견 바깥 끝(road_edges)은 측구·사면 꺾임으로 이미 구분되므로 제외한다. + const edges = design.carriageway_edges; + if (edges) { + for (const edge of [edges.left, edges.right]) { + const cx = x(edge.offset_m); + const cy = toDisplayY(edge.elevation_m); + const tick = document.createElementNS(SVG_NS, "line"); + tick.setAttribute("x1", String(cx)); + tick.setAttribute("y1", String(cy - 6)); + tick.setAttribute("x2", String(cx)); + tick.setAttribute("y2", String(cy + 6)); + tick.setAttribute("class", "b06-chart__carriageway-tick"); + svg.append(tick); + } + } } /** diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts index 8b89a9d1..d19bc185 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page.ts @@ -8,6 +8,7 @@ import { showToast, } from "@ui/ui_template_elements"; import { createWorkflowLayout } from "@ui/ui_template_workflow_layout"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { fetchWorkflowState, @@ -43,11 +44,12 @@ function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } -function buildGroup(legend: string): HTMLElement { - const group = document.createElement("fieldset"); - group.className = "b06-profile__group"; - const legendElement = document.createElement("legend"); - legendElement.className = "b06-profile__group-legend"; +// B05 사이드패널 접기 컨테이너 템플릿 재사용(N-4-1): 제목 행 클릭 토글, 우측 ▾/▸ 캐럿. +function buildGroup(legend: string, collapsed = false): HTMLElement { + const group = document.createElement("section"); + group.className = `b06-profile__group ui-collapsible${collapsed ? " is-collapsed" : ""}`; + const legendElement = document.createElement("h3"); + legendElement.className = "b06-profile__group-legend ui-collapsible__title"; legendElement.textContent = legend; group.append(legendElement); return group; @@ -99,6 +101,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const leftForm = document.createElement("div"); leftForm.className = "b06-profile__form"; leftForm.append(standardGroup, displayGroup, actionRow); + // 그룹 제목 행 클릭 시 접기/펼치기(N-4-1). 액션 버튼 행은 collapsible 아님. + attachCollapsible(leftForm); // 측점별 최신 요청 시퀀스 — 늦게 도착한 옛 응답을 폐기해 경합을 방지한다. const designRequestSeq = new Map(); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Standard_Panel.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Standard_Panel.ts index df4a19f9..0d6c5c55 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Standard_Panel.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Standard_Panel.ts @@ -179,9 +179,10 @@ export function createStandardPanel( const buildGroup = (key: StandardCrossKey, legendKey: keyof typeof ui_locales): HTMLElement => { const group = state[key]; - const fieldset = document.createElement("fieldset"); + // B05 "기준값 직접 지정"과 동일한 details/summary 패턴, 기본 접힘(N-4-1). + const fieldset = document.createElement("details"); fieldset.className = "b06-std__group"; - const legend = document.createElement("legend"); + const legend = document.createElement("summary"); legend.className = "b06-std__legend"; legend.textContent = L(legendKey); fieldset.append(legend); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css index f6a7ef4c..4b60516b 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css @@ -198,32 +198,39 @@ .b06-diag__label { fill: var(--color-text-body); - font-size: 12px; + font-size: 14px; } .b06-diag__label-sm { fill: var(--color-text-secondary); - font-size: 11px; + font-size: 12px; } +/* 구간 그룹(details/summary) — 상단 "변수 위치 안내" 모식도와 동일한 균일 8px 패딩으로 + 맞춰 접힘 시 좌측·하단 여백을 줄인다(N-4 후속). */ .b06-std__group { display: flex; flex-direction: column; gap: var(--spacing-8); margin: 0; - padding: var(--spacing-8) var(--spacing-16) var(--spacing-16); + padding: var(--spacing-8); border: 1px solid var(--color-border); border-radius: var(--radius-inputs); background-color: var(--color-surface); } .b06-std__legend { - padding: 0 var(--spacing-8); font-size: var(--text-caption); font-weight: var(--font-weight-medium); color: var(--color-text-secondary); } +/* 구간 그룹 접기(N-4-1) — details/summary. 기본 ▶ 마커로 접힘/펼침 표시. */ +.b06-std__group > summary.b06-std__legend { + cursor: pointer; + user-select: none; +} + .b06-std__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css index 7468c881..7babdf8e 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css @@ -483,6 +483,12 @@ stroke-linejoin: round; } +/* 차도·노견 경계 짧은 수직 틱(N-4-2) — 설계선과 같은 계열, 얇게. */ +.b06-chart__carriageway-tick { + stroke: var(--color-royal-amethyst); + stroke-width: 1.2; +} + /* 암 경계선(설계선 복사 + 오프셋): 리핑암·발파암 구간 점선 */ .b06-chart__rock-boundary { fill: none; diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index cac6b76a..0cd2a650 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -833,8 +833,8 @@ export const ui_locales = { "Slope direction is adjustable only for both-cut and both-fill sections.", ], B06_Design_More: ["더보기", "More"], - B06_Design_SlopeDir_Left: ["좌경사", "Slope left"], - B06_Design_SlopeDir_Right: ["우경사", "Slope right"], + B06_Design_SlopeDir_Left: ["좌", "Left"], + B06_Design_SlopeDir_Right: ["우", "Right"], B06_Design_Disabled_RockCut: [ "암(리핑/발파) 지반의 절토 단면에서만 사용할 수 있습니다.", "Available only for rock ground with a cut section.",