From 24acc22a0c103ccda42afbc5035746bc637d4bf5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 24 Jul 2026 18:13:11 +0900 Subject: [PATCH] 260723_8 --- .../B05_wf2_Route_UI_IrregularStations.ts | 2 +- B05_wf2_Route/B05_wf2_Route_UI_Page.ts | 4 +- B05_wf2_Route/B05_wf2_Route_UI_Panel.ts | 29 ++++- .../B05_wf2_Route_UI_Profile_Panel.ts | 9 ++ .../B05_wf2_Route_UI_Profile_Table.ts | 109 ++++++++++++------ B05_wf2_Route/B05_wf2_Route_UI_Style.css | 17 ++- .../B06_wf3_ProfileCross_UI_Longitudinal.ts | 12 +- ui_template/ui_template_locale.ts | 2 +- ui_template/ui_template_theme.css | 5 +- 9 files changed, 136 insertions(+), 53 deletions(-) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts index 5a874318..d756732b 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts @@ -75,7 +75,7 @@ export function createIrregularStationsSection( root.className = "b05-route__panel-section ui-collapsible"; const heading = document.createElement("h3"); heading.className = "ui-collapsible__title"; - heading.textContent = "비정규 측점 (구조물)"; + heading.textContent = "구조물 배치"; const body = document.createElement("div"); body.className = "b05-route__panel-body"; root.append(heading, body); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts index da8ed4d6..15ac226d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -223,6 +223,7 @@ export async function renderB05Route(root: HTMLElement): Promise { onRadiusChange: (radius) => viewer.markers.updateSelected({ radius_m: radius }), onInputChange: markStale, onIrregularChange: (stations) => applyIrregularStations(stations), + onStationDisplayChange: (offset) => profilePanel.setStationDisplay(offset), onIrregularSelect: (station) => { if (selectionSyncing) return; selectionSyncing = true; @@ -309,6 +310,7 @@ export async function renderB05Route(root: HTMLElement): Promise { function renderSections(detail: SectionDetailResponse, routeId?: number): void { currentSectionDetail = detail; profilePanel.render(detail, panel.values().stationInterval ?? undefined, routeId); + profilePanel.setStationDisplay(panel.stationDisplayOffset()); profilePanel.setIrregularStations(irregularStations); renderStationLines(detail); } @@ -512,7 +514,7 @@ export async function renderB05Route(root: HTMLElement): Promise { mainContent.className = "b05-route__main"; mainContent.append(viewer.root, profilePanel.root); const layout = createWorkflowLayout({ - title: "노선 설계", + title: "종단 설계", steps: workflowSteps(), activeStep: 2, leftPanel: panel.root, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts index 6f493ae5..71e28613 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts @@ -62,6 +62,8 @@ interface PanelCallbacks { onIrregularChange: (stations: IrregularStation[]) => void; /** 비정규 측점을 목록에서 선택/해제할 때 해당 측점(또는 null). */ onIrregularSelect: (station: IrregularStation | null) => void; + /** 이어 공사 시작 기준(시작 측점·누가거리 시작)이 바뀔 때. */ + onStationDisplayChange: (offset: { station: number; cumulative: number }) => void; } type WrappedInput = HTMLInputElement & { wrapper: HTMLLabelElement }; @@ -255,7 +257,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { longSampleInterval.wrapper, ); - const gradeLine = section("계획선(시공계획고) 설계"); + const gradeLine = section("종단 설계 기준"); const terrainType = document.createElement("select"); terrainType.innerHTML = ''; @@ -284,6 +286,28 @@ export function createRoutePanel(callbacks: PanelCallbacks) { ); gradeLine.body.append(terrainLabel, criteriaNote, gradeAdvanced); + // 공사 시작점 — 이전 공사에 이어 시공할 때 0측점을 임의 측점/누가거리로 시작 표기한다. + // (내부 chainage는 0기준 유지, 측점 라벨·누가거리 "표시"만 이 값만큼 이동.) 기본값 0/0. + const startBasis = section("공사 시작점"); + const startStation = numberField("시작 측점", "0"); + startStation.step = "1"; + startStation.min = "0"; + const startCumulative = numberField("시작 누가거리 (m)", "0"); + // 2열×2행: 1행 항목명 · 2행 값 입력 / 1열 시작 측점 · 2열 시작 누가거리. + const startRow = document.createElement("div"); + startRow.className = "b05-route__field-row"; + startRow.append(startStation.wrapper, startCumulative.wrapper); + startBasis.body.append(startRow); + const stationDisplayOffset = (): { station: number; cumulative: number } => ({ + station: Math.max(0, Math.round(Number(startStation.value) || 0)), + cumulative: Number(startCumulative.value) || 0, + }); + [startStation, startCumulative].forEach((input) => + input.addEventListener("change", () => + callbacks.onStationDisplayChange(stationDisplayOffset()), + ), + ); + // 비정규 측점(구조물 측점) — 측점번호+잔여거리로 추가/수정/삭제. 목록 변경은 Page로 올려 // 그래프·테이블·3D에 반영한다. chainage 환산 기준인 측점간격은 실시간 조회한다. const irregular = createIrregularStationsSection({ @@ -352,6 +376,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { conditions.root, sectionOptions.root, gradeLine.root, + startBasis.root, irregular.root, result.root, actionRow, @@ -366,6 +391,8 @@ export function createRoutePanel(callbacks: PanelCallbacks) { viewControls, /** 비정규 측점 섹션 API(목록 조회·선택·초기화). 아직 백엔드로 보내지 않는다(프론트 프리뷰). */ irregularStations: irregular as IrregularStationsSection, + /** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */ + stationDisplayOffset, values(): RoutePanelValues { return { contourInterval: Number(contourInterval.value) || 1, 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 a13a130c..e9b6e0f1 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -273,6 +273,8 @@ export function createRouteProfilePanel( let stationInterval: number | undefined; let routeId: number | null = null; let irregularStations: IrregularStation[] = []; + // 이어 공사 시작 기준 — 측점번호·누가거리 표시 오프셋(내부 chainage는 0기준 유지). + let stationDisplay = { station: 0, cumulative: 0 }; let base: AlignmentBase | null = null; let alignment: ProfileAlignment | null = null; let store = createProfileEditStore(null, emptyEdits(), () => rebuild()); @@ -432,6 +434,7 @@ export function createRouteProfilePanel( entry.chainage_m >= 0 && entry.chainage_m <= maxChainageOf(longitudinal) + 1e-6, ), selectedStationId, + stationDisplay, onCurveRadiusChange: (curve, radius) => applyEdits(setCurveRadius(store.edits(), curve, radius ?? 0)), // 값 열 계획고 직접 입력 → 규칙 측점과 동일한 station_offset 파이프라인. @@ -474,6 +477,7 @@ export function createRouteProfilePanel( (axis) => { yAxis = axis; }, + stationDisplay.station, ), ); // 가로 스크롤에도 고정되는 sticky Y축 오버레이(SVG와 같은 눈금·불투명 배경으로 값 누출 차단). @@ -576,6 +580,11 @@ export function createRouteProfilePanel( irregularStations = stations; draw(); }, + /** 이어 공사 시작 기준(측점번호·누가거리 오프셋)을 반영해 측점 라벨·누가거리 표시를 옮긴다. */ + setStationDisplay(next: { station: number; cumulative: number }) { + stationDisplay = next; + draw(); + }, /** * 특정 chainage의 계획고 편집(station_offset·curve_radii)을 지운다. * 비정규 측점을 옮기거나 지울 때 옛 위치에 남는 편집(유령 변화점)을 청소하는 데 쓴다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts index da6ddef3..8da2bf6d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -18,11 +18,7 @@ import type { ProfileAlignment, } from "./B05_wf2_Route_UI_Profile_Alignment"; import { stationLabel } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common"; -import { - irregularLabel, - irregularStationId, - type IrregularStation, -} from "./B05_wf2_Route_UI_IrregularStations"; +import { irregularStationId, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations"; export interface ProfileTableOptions { alignment: ProfileAlignment; @@ -39,8 +35,10 @@ export interface ProfileTableOptions { x: (chainageM: number) => number; /** 규칙 격자 밖 비정규 측점(구조물). 선택된 측점만 값 열로 오버레이한다. */ irregularStations?: IrregularStation[]; - /** 현재 선택된 측점 id. 비정규 측점이면 그 측점의 값 열을 테이블에 겹쳐 보여준다. */ + /** 현재 선택된 측점 id. 규칙/비정규 측점 모두 그 측점의 값 열을 테이블에 겹쳐 보여준다. */ selectedStationId?: string | null; + /** 이어 공사 시작 기준 — 측점번호·누가거리 표시를 이만큼 더한다(내부 chainage는 0기준 유지). */ + stationDisplay?: { station: number; cumulative: number }; onCurveRadiusChange: (curve: AlignmentCurve, radiusM: number | null) => void; /** 임의 chainage의 계획고를 delta만큼 조정(비정규 측점 값 열 직접 입력용). */ onAdjustStation?: (chainageM: number, deltaM: number) => void; @@ -149,7 +147,19 @@ const SEGMENT_ROWS: SegmentRowSpec[] = [ { label: "구배 S", unit: "% 구간 기울기", cell: (segment) => segment.grade_percent.toFixed(2) }, ]; -function buildStationRows(alignment: ProfileAlignment, interval: number): StationRowSpec[] { +/** 이어 공사 시작 기준을 반영한 측점 표기 — 측점번호에 시작 측점을 더한다(잔여거리는 그대로). */ +function displayStationLabel(chainageM: number, interval: number, stationOffset: number): string { + const base = stationLabel(chainageM, interval); // "X+YY.Y" + if (!stationOffset) return base; + const [number, remainder] = base.split("+"); + return `${Number(number) + stationOffset}+${remainder}`; +} + +function buildStationRows( + alignment: ProfileAlignment, + interval: number, + display: { station: number; cumulative: number }, +): StationRowSpec[] { const stations = alignment.stations; return [ { @@ -171,7 +181,11 @@ function buildStationRows(alignment: ProfileAlignment, interval: number): Statio cell: (index) => stations[index].plan_elevation_m.toFixed(2), }, { label: "지반고", unit: "m", cell: (index) => stations[index].ground_elevation_m.toFixed(2) }, - { label: "누가거리", unit: "m", cell: (index) => stations[index].chainage_m.toFixed(2) }, + { + label: "누가거리", + unit: "m", + cell: (index) => (stations[index].chainage_m + display.cumulative).toFixed(2), + }, { label: "거리", unit: "m 전 측점 대비", @@ -180,7 +194,7 @@ function buildStationRows(alignment: ProfileAlignment, interval: number): Statio { label: "측점", unit: "측점번호+잔여거리", - cell: (index) => stationLabel(stations[index].chainage_m, interval), + cell: (index) => displayStationLabel(stations[index].chainage_m, interval, display.station), }, ]; } @@ -292,11 +306,6 @@ function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLEl if (curve.omitted) cell.classList.add("is-omitted"); }); } - // 선택된 규칙 측점 열의 곡선 셀도 함께 하이라이트한다. - if (station.station_id && station.station_id === options.selectedStationId) { - lengthCell.classList.add("is-selected"); - radiusCell.classList.add("is-selected"); - } placeCell(lengthRow, centers[index], lengthCell); placeCell(radiusRow, centers[index], radiusCell); }); @@ -352,26 +361,27 @@ function interpolateSample( type CellValue = { text: string } | { left: string; right: string }; /** - * 선택된 비정규 측점의 **값 열 오버레이**. 규칙 측점 열과 같은 12행 순서로 값(계획고·지반고· - * 누가거리·측점 등, 계획선 샘플 보간)을 쌓아 테이블 위에 겹친다. 구조물 이름은 사이드바 입력에 - * 있으므로 여기엔 넣지 않고, 세로 점선도 두지 않는다. + * 선택된 측점(규칙·비정규 공용)의 **값 열 오버레이**. 12행 순서 그대로 값(계획고·지반고·누가거리· + * 측점·구배·곡선 등)을 쌓아 테이블 위에 겹친다. 규칙 측점을 눌러도 비정규와 **같은 오버레이 방식**으로 + * 눈에 잘 들어오게 한다. `centerX`는 호출부가 정한다 — 종점처럼 셀이 우측 이동된 규칙 측점은 그 이동된 + * 중심을, 비정규 측점은 `x(chainage)`를 넘긴다. */ -function buildIrregularColumn( - station: IrregularStation, +function buildSelectedColumn( + chainage: number, alignment: ProfileAlignment, centerX: number, cellWidth: number, interval: number, + display: { station: number; cumulative: number }, onAdjustStation?: (chainageM: number, deltaM: number) => void, ): HTMLElement { - const plan = interpolateSample(alignment.samples, station.chainage_m, "elevation_m"); - const ground = interpolateSample(alignment.samples, station.chainage_m, "ground_elevation_m"); + const plan = interpolateSample(alignment.samples, chainage, "elevation_m"); + const ground = interpolateSample(alignment.samples, chainage, "ground_elevation_m"); const cut = plan !== null && ground !== null && ground - plan > 0.005 ? ground - plan : null; const fill = plan !== null && ground !== null && plan - ground > 0.005 ? plan - ground : null; // 구배(L/H/S): 이 측점이 구간 안에 있으면 그 구간 값을 그대로(중복이어도) 표기하고, // 변화점이라 좌우 구간이 갈리면 세로 구분선으로 셀을 반 나눠 좌/우 값을 각각 넣는다. - const chainage = station.chainage_m; const inside = alignment.segments.find( (segment) => segment.from_m + 1e-6 < chainage && chainage < segment.to_m - 1e-6, ); @@ -384,6 +394,14 @@ function buildIrregularColumn( return left && right ? { left, right } : { text: left || right || "" }; }; const curve = alignment.curves.find((entry) => Math.abs(entry.chainage_m - chainage) < 0.01); + // 거리: 바로 앞 측점(계획선 측점)까지의 간격. + const previous = alignment.stations + .filter((row) => row.chainage_m < chainage - 1e-6) + .reduce( + (acc, row) => (acc === null ? row.chainage_m : Math.max(acc, row.chainage_m)), + null, + ); + const distance = previous !== null ? chainage - previous : null; // 테이블 행 순서(구배 3 · 측점값 7 · 곡선 2)와 정확히 같게 채운다. 없는 값은 공백. // `plan: true`인 계획고 행만 직접 입력 셀로 만든다(값 열은 오버레이라 실제 12행 격자는 불변). @@ -395,9 +413,9 @@ function buildIrregularColumn( { value: { text: fill !== null ? fill.toFixed(2) : "" }, modifier: "fill" }, // 성토고 { value: { text: plan !== null ? plan.toFixed(2) : "" }, modifier: "plan", plan: true }, // 계획고 { value: { text: ground !== null ? ground.toFixed(2) : "" }, modifier: "" }, // 지반고 - { value: { text: station.chainage_m.toFixed(2) }, modifier: "" }, // 누가거리 - { value: { text: "" }, modifier: "" }, // 거리 - { value: { text: stationLabel(station.chainage_m, interval) }, modifier: "" }, // 측점 + { value: { text: (chainage + display.cumulative).toFixed(2) }, modifier: "" }, // 누가거리 + { value: { text: distance !== null ? distance.toFixed(2) : "" }, modifier: "" }, // 거리 + { value: { text: displayStationLabel(chainage, interval, display.station) }, modifier: "" }, // 측점 { value: { text: curve ? curve.l_m.toFixed(2) : "" }, modifier: "" }, // 곡선 L { value: { text: curve ? curve.r_m.toFixed(1) : "" }, modifier: "" }, // 곡선 R ]; @@ -410,7 +428,7 @@ function buildIrregularColumn( `b05-profile-table__irregular-col-cell${row.modifier ? ` is-${row.modifier}` : ""}`, ); if (row.plan && onAdjustStation && plan !== null) { - // 계획고 직접 입력 → (입력값 − 현재 계획고) delta로 규칙 측점과 동일한 station_offset 반영. + // 계획고 직접 입력 → (입력값 − 현재 계획고) delta로 station_offset 반영(규칙·비정규 공용). const input = document.createElement("input"); input.type = "number"; input.step = "any"; @@ -419,7 +437,7 @@ function buildIrregularColumn( input.title = `계획고 직접 입력 (현재 ${plan.toFixed(2)}m)`; input.addEventListener("change", () => { const target = Number.parseFloat(input.value); - if (Number.isFinite(target)) onAdjustStation(station.chainage_m, target - plan); + if (Number.isFinite(target)) onAdjustStation(chainage, target - plan); }); cell.append(input); } else if ("left" in row.value) { @@ -434,12 +452,15 @@ function buildIrregularColumn( } column.append(cell); }); - column.title = `비정규 측점 ${irregularLabel(station)} · ${station.chainage_m.toFixed(2)}m`; + column.title = `${displayStationLabel(chainage, interval, display.station)} · ${( + chainage + display.cumulative + ).toFixed(2)}m`; return column; } export function createProfileTable(options: ProfileTableOptions): HTMLElement { const { alignment, stationInterval, width, height, cellWidth, labelWidth, rowCount, x } = options; + const display = options.stationDisplay ?? { station: 0, cumulative: 0 }; const table = element("div", "b05-profile-table"); const rowHeight = height / Math.max(rowCount, 1); const fontSize = fitFontSize(rowHeight, cellWidth); @@ -456,36 +477,50 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { table.style.setProperty("--b05-table-label-font", `${Math.max(8, labelFontSize)}px`); table.append(...buildSegmentRows(alignment, x, fontSize, rowHeight)); - buildStationRows(alignment, stationInterval).forEach((spec, index) => { + buildStationRows(alignment, stationInterval, display).forEach((spec, index) => { const row = createRow( `${spec.modifier ? `is-${spec.modifier}` : ""}${index === 0 ? " is-group-start" : ""}`, spec.label, spec.unit, ); // 값이 없는 측점(절토고/성토고 중 한쪽)도 빈 칸을 만들어야 세로 구분선이 끊기지 않는다. - alignment.stations.forEach((station, stationIndex) => { + alignment.stations.forEach((_station, stationIndex) => { const cell = element("span", "b05-profile-table__cell", spec.cell(stationIndex)); - // 선택된 규칙 측점은 그 열의 값 셀을 하이라이트한다(비정규 측점의 값 열 강조와 통일). - if (station.station_id && station.station_id === options.selectedStationId) { - cell.classList.add("is-selected"); - } placeCell(row, centers[stationIndex], cell); }); table.append(row); }); table.append(...buildCurveRows(options, centers)); - // 선택된 비정규 측점만 값 열로 오버레이한다(세로 점선·구조물 태그 없음). + + // 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다. + // 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다. const selectedIrregular = options.irregularStations?.find( (entry) => irregularStationId(entry.id) === options.selectedStationId, ); + const selectedRegularIndex = options.selectedStationId + ? alignment.stations.findIndex((row) => row.station_id === options.selectedStationId) + : -1; if (selectedIrregular) { table.append( - buildIrregularColumn( - selectedIrregular, + buildSelectedColumn( + selectedIrregular.chainage_m, alignment, x(selectedIrregular.chainage_m), cellWidth, stationInterval, + display, + options.onAdjustStation, + ), + ); + } else if (selectedRegularIndex >= 0) { + table.append( + buildSelectedColumn( + alignment.stations[selectedRegularIndex].chainage_m, + alignment, + centers[selectedRegularIndex], + cellWidth, + stationInterval, + display, options.onAdjustStation, ), ); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index 47cdeafa..a1ff6d4e 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -233,6 +233,13 @@ gap: var(--spacing-8); } +/* 2열 그리드 — 각 열은 항목명(위)+값 입력(아래). '공사 시작점' 등 나란한 입력에 쓴다. */ +.b05-route__field-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-8); +} + .b05-route__contour-row .b05-route__field { flex: 1; } @@ -555,16 +562,6 @@ border-right: 1px solid var(--color-border); } -/* 선택된 규칙 측점 열의 값·곡선 셀 하이라이트(비정규 측점 값 열 강조와 같은 색). */ -.b05-profile-table__cell.is-selected, -.b05-profile-table__curve.is-selected { - background: color-mix( - in srgb, - var(--color-royal-amethyst, rgb(109 40 217)) 14%, - var(--color-surface) - ); -} - .b05-profile-table__row.is-cut .b05-profile-table__cell { color: rgb(220 38 38); } diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Longitudinal.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Longitudinal.ts index b459ce01..63024693 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Longitudinal.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Longitudinal.ts @@ -21,6 +21,14 @@ import { type YScaleOptions, } from "./B06_wf3_ProfileCross_UI_Section_Common"; +/** 측점 라벨의 측점번호에 시작 측점 오프셋을 더한다(잔여거리는 그대로). */ +function offsetStationLabel(chainageM: number, interval: number, stationOffset: number): string { + const base = stationLabel(chainageM, interval); + if (!stationOffset) return base; + const [number, remainder] = base.split("+"); + return `${Number(number) + stationOffset}+${remainder}`; +} + export function longitudinalMinimumWidth( data: LongitudinalSection, configuredStationInterval?: number, @@ -96,6 +104,8 @@ export function createLongitudinalProfile( * 오버레이를 그릴 때 SVG와 **같은 Y-스케일**을 공유하려고 쓴다. B06은 넘기지 않는다. */ onYAxis?: (axis: { padLeft: number; ticks: Array<{ y: number; label: string }> }) => void, + /** 이어 공사 시작 측점 오프셋. 측점 라벨의 측점번호에 이만큼 더한다(B05 표시용, 기본 0). */ + stationNumberOffset = 0, ): HTMLElement { const samples = data.samples.filter(validElevation); if (samples.length < 2) return emptyView(L("B06_Profile_View_NoLongitudinal")); @@ -215,7 +225,7 @@ export function createLongitudinalProfile( y2: heightPx - LONG_PAD.bottom + 8, class: `b06-chart__station-line b06-chart__station-line--${selected ? "selected" : station.kind}`, }), - svgText(stationLabel(station.chainage_m, stationInterval), { + svgText(offsetStationLabel(station.chainage_m, stationInterval, stationNumberOffset), { x: stationX, y: heightPx - 23, "text-anchor": "middle", diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index 04adb83f..e7ed99ce 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -725,7 +725,7 @@ export const ui_locales = { B05_Route_Solve_Failed: ["경로 탐색에 실패했습니다.", "Route solve failed."], B05_Route_Confirm_Success: ["경로를 확정했습니다.", "Route confirmed."], B05_Route_Confirm_Failed: ["경로 확정에 실패했습니다.", "Route confirm failed."], - B05_Route_Group_SectionOptions: ["측점·횡단 옵션", "Station & Cross Options"], + B05_Route_Group_SectionOptions: ["측점 및 샘플링 설정", "Station & Sampling Settings"], B05_Route_Field_StationInterval: ["측점 간격(m)", "Station interval (m)"], B05_Route_Field_CrossHalfWidth: ["횡단 반폭(m)", "Cross half-width (m)"], B05_Route_Field_CrossSample: ["횡단 샘플 간격(m)", "Cross sample interval (m)"], diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index d1887762..a10ce886 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -346,8 +346,11 @@ .ui-collapsible__title::after { content: "▾"; flex: 0 0 auto; + margin-block: -0.4em; color: var(--color-text-muted); - font-size: 0.8em; + /* 레이아웃 허용 범위 내에서 최대한 크게(제목 글자의 약 1.7배). 음수 margin으로 행 높이는 유지. */ + font-size: 1.7em; + line-height: 1; } .ui-collapsible.is-collapsed > .ui-collapsible__title::after {