diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts index b17abb83..98856d5d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -168,11 +168,9 @@ export async function renderB05Route(root: HTMLElement): Promise { crossSampleInterval: next.route_params?.cross_sample_interval_m ?? undefined, longSampleInterval: next.route_params?.long_sample_interval_m ?? undefined, terrainType: options.terrain_type as RoutePanelValues["terrainType"] | undefined, - mainDirection: options.main_direction as RoutePanelValues["mainDirection"] | undefined, maxGradePct: next.route_params?.max_grade_pct ?? undefined, minVerticalRadius: next.route_params?.min_vertical_radius_m ?? undefined, minTangentLength: next.route_params?.min_tangent_length_m ?? undefined, - balanceSegmentLength: next.route_params?.balance_segment_length_m ?? undefined, startElevationOffset: next.route_params?.start_elevation_offset_m ?? undefined, endElevationOffset: next.route_params?.end_elevation_offset_m ?? undefined, }); @@ -290,11 +288,9 @@ export async function renderB05Route(root: HTMLElement): Promise { cross_sample_interval_m: values.crossSampleInterval, long_sample_interval_m: values.longSampleInterval, terrain_type: values.terrainType, - main_direction: values.mainDirection, max_grade_pct: values.maxGradePct, min_vertical_radius_m: values.minVerticalRadius, min_tangent_length_m: values.minTangentLength, - balance_segment_length_m: values.balanceSegmentLength, start_elevation_offset_m: values.startElevationOffset, end_elevation_offset_m: values.endElevationOffset, }); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts index 73bd9dd9..83a01a7d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts @@ -17,11 +17,9 @@ export interface RoutePanelValues { crossSampleInterval: number | null; longSampleInterval: number | null; terrainType: "normal" | "special"; - mainDirection: "auto" | "ascending" | "descending" | "none"; maxGradePct: number | null; minVerticalRadius: number | null; minTangentLength: number | null; - balanceSegmentLength: number | null; startElevationOffset: number | null; endElevationOffset: number | null; } @@ -263,21 +261,11 @@ export function createRoutePanel(callbacks: PanelCallbacks) { const terrainLabel = document.createElement("label"); terrainLabel.className = "b05-route__field"; terrainLabel.append(document.createTextNode("지형 구분"), terrainType); - // 역기울기(5%) 상한을 어느 방향에 적용할지 결정한다. 계곡 횡단·능선 통과처럼 - // 주 진행방향이 없는 노선에 역기울기를 걸면 지형을 따라갈 수 없어진다. - const mainDirection = document.createElement("select"); - mainDirection.innerHTML = - '' + - '' + - '' + - ''; - const directionLabel = document.createElement("label"); - directionLabel.className = "b05-route__field"; - directionLabel.append(document.createTextNode("주 진행방향"), mainDirection); + // 역기울기(5%) 상한 방향은 서버가 지반 형상에서 자동 판정(main_direction="auto")하므로 + // 수동 선택 UI는 두지 않는다. 노선 균형 구역 길이도 자동 산출 기본값(전체 1구역)에 맡긴다. const maxGradePct = numberField("최대 종단기울기 (%)"); const minVerticalRadius = numberField("종단곡선 최소 반경 (m)"); const minTangentLength = numberField("최소 직선 길이 (m)"); - const balanceSegmentLength = numberField("균형 구역 길이 (m, 비우면 전체)"); const startElevationOffset = numberField("시점 계획고 조정 (m)"); const endElevationOffset = numberField("종점 계획고 조정 (m)"); const criteriaNote = document.createElement("p"); @@ -297,14 +285,23 @@ export function createRoutePanel(callbacks: PanelCallbacks) { gradeHelp.innerHTML = "계획선이란?

공사 후 노면이 될 높이입니다. 직선과 종단곡선만으로 구성되며, " + "절토량과 성토량이 균형을 이루도록(적분값 0) 자동 산출됩니다.

"; - gradeLine.body.append( - terrainLabel, - directionLabel, - balanceSegmentLength.wrapper, - criteriaNote, - gradeAdvanced, - gradeHelp, - ); + gradeLine.body.append(terrainLabel, criteriaNote, gradeAdvanced, gradeHelp); + + // 비정규 측점(구조물 설치용) — 사용자가 판단해 X+XX 위치에 구조물 정보를 적어 두는 컨테이너. + // 지금은 자유 텍스트 초안 입력만 받는다(구조물 형식·옵션 미확정). 추후 선택+값 입력으로 대체 예정. + // 값을 재탐색 트리거(inputElements)에 넣지 않아 텍스트를 고쳐도 "재탐색 필요"가 뜨지 않는다. + const irregular = section("비정규 측점 (구조물)"); + const irregularStations = document.createElement("textarea"); + irregularStations.className = "b05-route__textarea"; + irregularStations.rows = 4; + irregularStations.placeholder = + "예)\n0+15 배수구조물\n2+18 옹벽\n(측점 위치 + 구조물, 한 줄에 하나)"; + const irregularHelp = document.createElement("p"); + irregularHelp.className = "b05-route__note"; + irregularHelp.textContent = + "구조물 설치가 필요한 지점을 측점(X+XX) 위치로 적어 두는 초안 입력입니다. " + + "지금은 자유 텍스트만 받고, 이후 구조물 선택·값 입력으로 발전시킵니다."; + irregular.body.append(irregularStations, irregularHelp); /** 등급·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */ function syncCriteria(): void { @@ -352,11 +349,9 @@ export function createRoutePanel(callbacks: PanelCallbacks) { crossSampleInterval, longSampleInterval, terrainType, - mainDirection, maxGradePct, minVerticalRadius, minTangentLength, - balanceSegmentLength, startElevationOffset, endElevationOffset, ]; @@ -368,6 +363,7 @@ export function createRoutePanel(callbacks: PanelCallbacks) { conditions.root, sectionOptions.root, gradeLine.root, + irregular.root, result.root, actionRow, ); @@ -375,6 +371,8 @@ export function createRoutePanel(callbacks: PanelCallbacks) { return { root, viewControls, + /** 비정규 측점(구조물) 초안 텍스트. 아직 백엔드로 보내지 않는다(형식 확정 전 임시 보관용). */ + irregularStationsText: () => irregularStations.value, values(): RoutePanelValues { return { contourInterval: Number(contourInterval.value) || 1, @@ -391,11 +389,9 @@ export function createRoutePanel(callbacks: PanelCallbacks) { crossSampleInterval: parseOptional(crossSampleInterval), longSampleInterval: parseOptional(longSampleInterval), terrainType: terrainType.value as RoutePanelValues["terrainType"], - mainDirection: mainDirection.value as RoutePanelValues["mainDirection"], maxGradePct: parseOptional(maxGradePct), minVerticalRadius: parseOptional(minVerticalRadius), minTangentLength: parseOptional(minTangentLength), - balanceSegmentLength: parseOptional(balanceSegmentLength), startElevationOffset: parseOptional(startElevationOffset), endElevationOffset: parseOptional(endElevationOffset), }; @@ -417,13 +413,10 @@ export function createRoutePanel(callbacks: PanelCallbacks) { if (values.longSampleInterval != null) longSampleInterval.value = String(values.longSampleInterval); if (values.terrainType) terrainType.value = values.terrainType; - if (values.mainDirection) mainDirection.value = values.mainDirection; if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct); if (values.minVerticalRadius != null) minVerticalRadius.value = String(values.minVerticalRadius); if (values.minTangentLength != null) minTangentLength.value = String(values.minTangentLength); - if (values.balanceSegmentLength != null) - balanceSegmentLength.value = String(values.balanceSegmentLength); if (values.startElevationOffset != null) startElevationOffset.value = String(values.startElevationOffset); if (values.endElevationOffset != null) 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 919c8235..de12a2cf 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -128,26 +128,19 @@ function normalizedLongitudinal(data: LongitudinalSection): LongitudinalSection const CELL_GAP_PX = 2; /** - * **측점 간격 기본값(px)** — 측점 한 칸의 고정 폭. - * - * 목표 글자 크기(12px)로 7자리 값(`3000.00`)이 잘리지 않는 크기다. 브라우저 폭에 맞춰 - * 늘였다 줄였다 하지 않고 이 값을 항상 유지하며, 남는 가로는 스크롤로 훑는다. 규칙 측점은 - * 정확히 이 간격으로 찍히고, 셀 폭·글자 크기도 이 값에서 나온다. - * - * 다음 세션의 **비정규 측점**(`+18`, `+15` 등)은 이 간격을 한 칸으로 보고 그 안에서 - * chainage 비례로 자리 잡는다 — 값 자체는 chainage×(간격/측점간격)로 매핑되므로 규칙 칸을 - * 넓히면 비정규 측점이 겹칠 여유도 함께 늘어난다. 그래서 기본값을 넉넉히 잡아 둔다. + * 측점 한 칸의 **기준 폭(px)** — 목표 글자 크기(12px)로 7자리 값(`3000.00`)이 잘리지 않는 크기. + * 실제 기본 간격은 여기에 배수(`PROFILE_SPACING_MULTIPLIER`)를 곱한 값을 쓴다. */ const STATION_SPACING_PX = tableCellWidthFor(TABLE_TARGET_FONT_PX) + CELL_GAP_PX; /** - * 0측점·종점을 좌우 축 프레임 안쪽으로 반 칸 밀어넣는 여백(px). + * **측점 간격 기본 배수**. 기준 폭의 1.5배를 한 측점 칸의 기본 간격으로 삼는다. * - * 0측점을 이름표 열(labelWidth = LONG_PAD.left) 바로 위에 두면 셀의 왼쪽 절반이 이름표에 - * 가려진다. 반 칸을 밀어 0측점 셀 전체가 이름표 오른쪽으로 나오게 하고, 종점도 대칭으로 - * 오른쪽 끝에서 반 칸 띄운다. 그래프도 같은 오프셋(`originOffsetPx`)을 받아 X축을 맞춘다. + * 이 배수로 펼친 폭이 **최소 폭**이다 — 브라우저가 이보다 넓으면 폭맞춤으로 늘리고, 좁으면 + * 이 간격을 유지한 채 스크롤로 훑는다. 넉넉한 기본값은 다음 세션의 **비정규 측점**(`+18` 등)이 + * 규칙 칸 안에서 chainage 비례로 자리 잡을 여유도 함께 확보한다. */ -const PROFILE_ORIGIN_OFFSET_PX = STATION_SPACING_PX / 2; +const PROFILE_SPACING_MULTIPLIER = 1.5; /** 유효 표고 샘플 기준의 노선 최대 chainage(m). 그래프·테이블이 같은 값을 써야 X축이 맞물린다. */ function maxChainageOf(data: LongitudinalSection): number { @@ -168,17 +161,38 @@ function chainageMapper( return (chainage: number) => LONG_PAD.left + originOffset + (chainage / maxChainage) * plotWidth; } +interface ProfileLayout { + /** 캔버스 폭(px). 화면이 넓으면 폭맞춤으로, 좁으면 최소 폭으로. */ + width: number; + /** 0측점·종점을 축 프레임 안으로 반 칸씩 들여쓰는 여백(px) — 그래프·테이블 공통. */ + originOffset: number; + /** 이웃 측점과 겹치지 않는 테이블 셀 폭(px). 실제 측점 간격에 맞춰 함께 늘어난다. */ + cellWidth: number; +} + /** - * 측점 간격을 `STATION_SPACING_PX`로 고정했을 때의 캔버스 폭. + * 측점 간격 기본값(기준 폭 × 1.5)으로 노선을 펼치되, 화면이 더 넓으면 폭맞춤으로 늘린다. * - * 브라우저 폭에 맞춰 늘리지 않는다 — 노선 전체를 `STATION_SPACING_PX / 측점간격(m)`의 - * 고정 배율(px/m)로 펼치고, 화면보다 길면 스크롤로 훑는다. 이렇게 해야 측점 간격·셀 폭· - * 글자 크기가 브라우저 크기와 무관하게 일정하게 유지된다. + * 매핑은 `x(c) = LONG_PAD.left + halfCell + c·pxPerMeter`이고, 좌우로 반 칸(halfCell)씩 띄워 + * 0측점 셀이 이름표 열 밖으로, 종점 셀이 오른쪽 끝 밖으로 나오게 한다. 좌우 여백을 합치면 + * 한 칸(측점간격)이므로 `width = pads + (maxChainage + interval)·pxPerMeter`가 되고, 이를 뒤집어 + * pxPerMeter를 구하면 halfCell·셀 폭이 실제 간격과 항상 맞물린다. */ -function fixedProfileWidth(data: LongitudinalSection, stationIntervalM: number): number { - const pxPerMeter = STATION_SPACING_PX / Math.max(stationIntervalM, 1e-6); - // 좌우 반 칸씩(= STATION_SPACING_PX) 여백을 더해 0측점·종점 셀이 프레임에 붙지 않게 한다. - return LONG_PAD.left + LONG_PAD.right + STATION_SPACING_PX + maxChainageOf(data) * pxPerMeter; +function computeProfileLayout( + data: LongitudinalSection, + stationIntervalM: number, + availableWidth: number, +): ProfileLayout { + const maxChainage = maxChainageOf(data); + const interval = Math.max(stationIntervalM, 1e-6); + const framePad = LONG_PAD.left + LONG_PAD.right; + const minSpacing = STATION_SPACING_PX * PROFILE_SPACING_MULTIPLIER; + // 기본 배수로 펼친 최소 폭 (좌우 반 칸 = 한 칸 여백 포함). + const minWidth = framePad + ((maxChainage + interval) / interval) * minSpacing; + const width = Math.max(minWidth, availableWidth); + const pxPerMeter = (width - framePad) / (maxChainage + interval); + const spacing = interval * pxPerMeter; + return { width, originOffset: spacing / 2, cellWidth: Math.max(1, spacing - CELL_GAP_PX) }; } export function createRouteProfilePanel( @@ -311,17 +325,22 @@ export function createRouteProfilePanel( renderBalance(); const longitudinal = detail.longitudinal; - // 계획선(편집 가능) 상태에서는 측점 간격을 고정한다. 그 외(구버전·플레인 뷰)는 - // 예전처럼 화면 폭에 맞춰 펼친다. + const availableWidth = Math.max(1, body.clientWidth - 15); + // 계획선(편집 가능) 상태에서는 측점 간격 기본값(기준×1.5)으로 펼치되 화면이 넓으면 + // 폭맞춤으로 늘린다. 그 외(구버전·플레인 뷰)는 예전처럼 화면 폭에 맞춰 펼친다. const stationIntervalM = stationInterval ?? alignment?.policy.station_interval_m; - const originOffset = alignment ? PROFILE_ORIGIN_OFFSET_PX : 0; - const width = + const layout = alignment && stationIntervalM - ? fixedProfileWidth(longitudinal, stationIntervalM) - : Math.max( - Math.max(1, body.clientWidth - 15), - longitudinalMinimumWidth(longitudinal, stationInterval), - ); + ? computeProfileLayout(longitudinal, stationIntervalM, availableWidth) + : { + width: Math.max( + availableWidth, + longitudinalMinimumWidth(longitudinal, stationInterval), + ), + originOffset: 0, + cellWidth: STATION_SPACING_PX - CELL_GAP_PX, + }; + const { width, originOffset } = layout; const canvas = document.createElement("div"); canvas.className = "b05-profile__canvas"; canvas.style.width = `${width}px`; @@ -341,8 +360,8 @@ export function createRouteProfilePanel( stationInterval: stationInterval ?? alignment.policy.station_interval_m, width, height: tableHeight, - // 측점 간격을 고정했으므로 셀 폭도 고정값으로 둔다(브라우저 폭과 무관하게 유지). - cellWidth: STATION_SPACING_PX - CELL_GAP_PX, + // 셀 폭은 실제 측점 간격에 맞춰 함께 늘어난다(폭맞춤 시 값이 넓게 퍼진다). + cellWidth: layout.cellWidth, // 이름표 열을 그래프 좌측 여백과 같은 폭으로 맞춰야 그래프 시작점이 가려지지 않는다. labelWidth: LONG_PAD.left, rowCount: TABLE_ROW_COUNT, 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 1e9d7092..c37135a7 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -95,6 +95,29 @@ function placeCell(row: HTMLElement, centerX: number, node: HTMLElement): void { row.append(node); } +/** + * 각 측점 셀의 중심 x. 규칙 측점은 측점 수직선 위(`x(chainage)`)에 그대로 두되, + * **종점(마지막 측점)**이 앞 측점과 겹치면 오른쪽으로 민다. 종점이라 위치가 수직선에서 + * 살짝 벗어나도 사용자가 종점 값임을 인지하는 데 무리가 없다. 캔버스 오른쪽 끝을 넘지 않게 + * 가용 여백 안에서만 민다. (곡선/구배 행이 아니라 측점 값 셀·곡선 셀에만 적용한다.) + */ +function stationCellCenters( + stations: ReadonlyArray<{ chainage_m: number }>, + x: (chainageM: number) => number, + cellWidth: number, + width: number, +): number[] { + const centers = stations.map((station) => x(station.chainage_m)); + const last = centers.length - 1; + if (last >= 1) { + const cleared = centers[last - 1] + cellWidth; + if (centers[last] < cleared) { + centers[last] = Math.min(cleared, width - cellWidth / 2 - 2); + } + } + return centers; +} + /** * 행 하나. 이름표는 sticky로 좌측에 고정되며 **그래프의 좌측 여백과 같은 폭**을 쓴다. * 단위는 이름표를 좁게 유지하려고 툴팁으로 뺐다(넓히면 그래프 시작점을 가린다). @@ -206,8 +229,8 @@ function curveTitle(curve: AlignmentCurve): string { * 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄 * 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다. */ -function buildCurveRows(options: ProfileTableOptions): HTMLElement[] { - const { alignment, x, onCurveRadiusChange } = options; +function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLElement[] { + const { alignment, onCurveRadiusChange } = options; const lengthRow = createRow( "b05-profile-table__row--curve is-group-start", "곡선 L", @@ -218,7 +241,7 @@ function buildCurveRows(options: ProfileTableOptions): HTMLElement[] { alignment.curves.map((curve) => [curve.chainage_m.toFixed(3), curve]), ); - alignment.stations.forEach((station) => { + alignment.stations.forEach((station, index) => { const lengthCell = element("span", "b05-profile-table__curve"); const radiusCell = element("span", "b05-profile-table__curve"); const curve = curveByChainage.get(station.chainage_m.toFixed(3)); @@ -244,8 +267,8 @@ function buildCurveRows(options: ProfileTableOptions): HTMLElement[] { if (curve.omitted) cell.classList.add("is-omitted"); }); } - placeCell(lengthRow, x(station.chainage_m), lengthCell); - placeCell(radiusRow, x(station.chainage_m), radiusCell); + placeCell(lengthRow, centers[index], lengthCell); + placeCell(radiusRow, centers[index], radiusCell); }); return [lengthRow, radiusRow]; } @@ -254,6 +277,9 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { const { alignment, stationInterval, width, height, cellWidth, labelWidth, rowCount, x } = options; const table = element("div", "b05-profile-table"); const fontSize = fitFontSize(height / Math.max(rowCount, 1), cellWidth); + // 종점이 앞 측점과 겹치면 오른쪽으로 민 셀 중심(측점 값·곡선 셀 공용). 구배 블록은 구간을 + // 덮는 넓은 범위라 겹치지 않으므로 x를 그대로 쓴다. + const centers = stationCellCenters(alignment.stations, x, cellWidth, width); // 이름표는 "누가거리"(한글 4자)가 들어가야 하므로 값 글자보다 크지 않게 제한한다. const labelFontSize = Math.min(fontSize, Math.floor((labelWidth - 10) / 4)); table.style.width = `${width}px`; @@ -271,12 +297,12 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { spec.unit, ); // 값이 없는 측점(절토고/성토고 중 한쪽)도 빈 칸을 만들어야 세로 구분선이 끊기지 않는다. - alignment.stations.forEach((station, stationIndex) => { + alignment.stations.forEach((_station, stationIndex) => { const cell = element("span", "b05-profile-table__cell", spec.cell(stationIndex)); - placeCell(row, x(station.chainage_m), cell); + placeCell(row, centers[stationIndex], cell); }); table.append(row); }); - table.append(...buildCurveRows(options)); + table.append(...buildCurveRows(options, centers)); return table; } diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index 8683f956..b737a34b 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -66,33 +66,8 @@ padding-right: 15px; } -/* 종단면도는 노선 전체 길이만큼 가로로 길어 스크롤바가 유일한 이동 수단이다. - 전역 스크롤바(8px, thumb 불투명도 12%)는 가로로 쓰기엔 너무 옅어 잡히지 않으므로 - 이 패널에서만 더 두껍고 대비가 분명한 막대로 덮어쓴다. */ -.b05-route-profile__body { - scrollbar-color: var(--color-text-muted, var(--color-plum-velvet)) var(--color-surface); - scrollbar-width: auto; -} - -.b05-route-profile__body::-webkit-scrollbar { - height: 12px; -} - -.b05-route-profile__body::-webkit-scrollbar-track { - border-top: 1px solid var(--color-border); - background-color: var(--color-surface); -} - -.b05-route-profile__body::-webkit-scrollbar-thumb { - border: 2px solid transparent; - border-radius: var(--radius-pills); - background-color: color-mix(in srgb, var(--color-text) 45%, transparent); - background-clip: padding-box; -} - -.b05-route-profile__body::-webkit-scrollbar-thumb:hover { - background-color: color-mix(in srgb, var(--color-text) 65%, transparent); -} +/* 가로 스크롤바는 전역 pill 스크롤바(테마 `*::-webkit-scrollbar`, 8px)를 그대로 쓴다 — + 좌측 사이드바와 동일한 형태로 통일. (예전엔 이 패널만 12px 두꺼운 막대로 덮어썼다.) */ /* 그래프와 테이블을 같은 폭으로 쌓아 X축이 저절로 맞물리게 한다. */ .b05-profile__canvas { @@ -122,13 +97,18 @@ height: 100%; } -/* 축 제목과 측점 라벨은 아래 도면 테이블(측점 행)이 그대로 담고 있어 중복이다. - 지우면 그래프 상·하단이 비어 편집 버튼이 가려지지 않고, 좌측 여백도 이름표 열로 쓸 수 있다. */ -.b05-route-profile .b06-chart__axis-label, -.b05-route-profile .b06-chart__station-label { +/* X·Y축 제목은 아래 도면 테이블이 그대로 담고 있어 중복이라 숨긴다. */ +.b05-route-profile .b06-chart__axis-label { display: none; } +/* 측점 이름(세로선 항목)은 그래프에서도 보여야 어느 측점의 선인지 바로 읽힌다. + 다만 하단 편집 버튼(is-down: bottom 2px, 높이 15px)과 겹치지 않도록 라벨을 살짝 + 위로 올려 하단 여백 위쪽에 앉힌다(하단 버튼 밴드와 분리). */ +.b05-route-profile .b06-chart__station-label { + transform: translateY(-9px); +} + .b05-route__viewport canvas { display: block; width: 100%; @@ -233,7 +213,8 @@ .b05-route__field input, .b05-route__field select, -.b05-route__panel-section > select { +.b05-route__panel-section > select, +.b05-route__textarea { width: 100%; min-width: 0; padding: var(--spacing-8); @@ -243,6 +224,14 @@ color: var(--color-text-body); } +/* 비정규 측점 초안 입력 — 세로 리사이즈만 허용(가로는 컨테이너 폭 고정). */ +.b05-route__textarea { + box-sizing: border-box; + resize: vertical; + font: inherit; + line-height: 1.4; +} + .b05-route__check { display: flex; align-items: center;