From 505f28a30babe85a2bfbcd8715b87ddb564c2e2d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 24 Jul 2026 17:52:01 +0900 Subject: [PATCH] 260723_7 --- .../B05_wf2_Route_Engine_Sections_Core.py | 8 +++-- .../B05_wf2_Route_UI_IrregularStations.ts | 3 +- B05_wf2_Route/B05_wf2_Route_UI_Panel.ts | 16 ++++----- .../B05_wf2_Route_UI_Profile_Table.ts | 11 +++++- B05_wf2_Route/B05_wf2_Route_UI_Style.css | 34 ++++++------------- .../B06_wf3_ProfileCross_UI_Cross_View.ts | 13 ++++++- .../B06_wf3_ProfileCross_UI_Style.css | 10 ++++++ .../B07_wf4_DesignDetail_UI_Page.ts | 5 ++- ui_template/ui_template_collapsible.ts | 21 ++++++++++++ ui_template/ui_template_theme.css | 27 +++++++++++++++ 10 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 ui_template/ui_template_collapsible.ts diff --git a/B05_wf2_Route/B05_wf2_Route_Engine_Sections_Core.py b/B05_wf2_Route/B05_wf2_Route_Engine_Sections_Core.py index a62c7ca1..61606c40 100644 --- a/B05_wf2_Route/B05_wf2_Route_Engine_Sections_Core.py +++ b/B05_wf2_Route/B05_wf2_Route_Engine_Sections_Core.py @@ -135,6 +135,8 @@ def generate_sections( long_z, long_valid = sampler.sample_xy(long_xy) station_chainage = _chainages(total, options.station_interval_m, options.include_endpoint) + # 규칙(격자) 측점 chainage 집합 — 비정규와 겹칠 때 규칙 측점을 우선 판정하는 데 쓴다. + regular_rounded = {round(float(value), 6) for value in station_chainage} # 비정규 측점(구조물)을 격자 측점 배열에 병합한다. 정렬·중복 제거만 하면 이후 접선·횡단 # 샘플링이 전부 이 배열을 따라 자동으로 확장된다. 어느 측점이 비정규인지는 chainage로 판별. extras = { @@ -183,7 +185,8 @@ def generate_sections( kind = "bp" elif abs(float(value) - total) <= 1e-6: kind = "ep" - elif rounded in extras: + elif rounded in extras and rounded not in regular_rounded: + # 격자와 겹치지 않는 비정규 측점만 irregular. 규칙 격자와 겹치면 규칙 측점 우선. kind = "irregular" else: kind = "regular" @@ -213,7 +216,8 @@ def generate_sections( "azimuth_deg": round(azimuth, 6), "frame": frame, } - if kind == "irregular": + # 구조물은 kind와 무관하게 부착한다(규칙 격자와 겹쳐 regular가 돼도 구조물 정보는 유지). + if rounded in extras: station["structure"] = extras[rounded] stations.append(station) diff --git a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts index 139251a4..5a874318 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts @@ -72,8 +72,9 @@ export function createIrregularStationsSection( callbacks: IrregularStationsCallbacks, ): IrregularStationsSection { const root = document.createElement("section"); - root.className = "b05-route__panel-section"; + root.className = "b05-route__panel-section ui-collapsible"; const heading = document.createElement("h3"); + heading.className = "ui-collapsible__title"; heading.textContent = "비정규 측점 (구조물)"; const body = document.createElement("div"); body.className = "b05-route__panel-body"; diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts index ea3301b8..6f493ae5 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts @@ -5,6 +5,7 @@ import { type IrregularStationsSection, } from "./B05_wf2_Route_UI_IrregularStations"; import { type ButtonVariant, createButton } from "@ui/ui_template_elements"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; export interface RoutePanelValues { @@ -71,8 +72,9 @@ function L(key: keyof typeof ui_locales): string { function section(title: string): { root: HTMLElement; body: HTMLElement } { const root = document.createElement("section"); - root.className = "b05-route__panel-section"; + root.className = "b05-route__panel-section ui-collapsible"; const heading = document.createElement("h3"); + heading.className = "ui-collapsible__title"; heading.textContent = title; const body = document.createElement("div"); body.className = "b05-route__panel-body"; @@ -355,15 +357,9 @@ export function createRoutePanel(callbacks: PanelCallbacks) { actionRow, ); - // 컨테이너 제목(h3) 클릭 시 본문을 접거나 편다(제목은 남긴다). 본문 안의 details 등 별도 - // 접힘 항목은 손대지 않으므로 열려 있으면 그대로 보인다. - root.addEventListener("click", (event) => { - const heading = (event.target as HTMLElement).closest("h3"); - const container = heading?.parentElement; - if (heading && container?.classList.contains("b05-route__panel-section")) { - container.classList.toggle("is-collapsed"); - } - }); + // 컨테이너 제목 행 전체 클릭 시 본문을 접거나 편다(공용 collapsible). 내부 details 등 별도 + // 접힘 항목은 손대지 않는다. + attachCollapsible(root); return { root, 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 5983543d..da6ddef3 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -292,6 +292,11 @@ 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); }); @@ -458,8 +463,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)); + // 선택된 규칙 측점은 그 열의 값 셀을 하이라이트한다(비정규 측점의 값 열 강조와 통일). + if (station.station_id && station.station_id === options.selectedStationId) { + cell.classList.add("is-selected"); + } placeCell(row, centers[stationIndex], cell); }); table.append(row); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index f309da8b..47cdeafa 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -211,33 +211,11 @@ background: var(--color-surface-raised); } +/* 제목 행 클릭 접기/펼치기는 공용 collapsible(ui_template_theme.css)에서 처리한다. */ .b05-route__panel-section h3 { margin: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-8); color: var(--color-text); font-size: var(--text-body-sm); - cursor: pointer; - user-select: none; -} - -/* 접기/펼침 상태를 제목 우측 캐럿으로 표시(이 기능이 있음을 알린다). 펼침 ▾ / 접힘 ▸. */ -.b05-route__panel-section h3::after { - content: "▾"; - flex: 0 0 auto; - color: var(--color-text-muted); - font-size: 0.8em; -} - -.b05-route__panel-section.is-collapsed h3::after { - content: "▸"; -} - -/* 제목 클릭으로 접힌 컨테이너: 본문만 감추고 제목은 남긴다. */ -.b05-route__panel-section.is-collapsed .b05-route__panel-body { - display: none; } .b05-route__panel-body, @@ -577,6 +555,16 @@ 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_Cross_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts index 188c58bf..99911c72 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts @@ -141,7 +141,18 @@ export function createCrossSectionCard( : section.kind === "bp" ? L("B06_Profile_View_Kind_BP") : L("B06_Profile_View_Kind_Station"); - header.append(title, kind); + // kind 라벨(일반측점/BP/EP) 좌측에 구조물 정보를 표기(비정규 측점이 확정 시 실어온 값). + const meta = document.createElement("div"); + meta.className = "b06-cross-card__meta"; + 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); + } + meta.append(kind); + header.append(title, meta); card.append(header); if (onDesignChange) card.append(buildDesignControls(section, onDesignChange)); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css index 85491a8e..cf1dddc8 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css @@ -240,6 +240,16 @@ font-size: var(--text-body-sm); } +/* 비정규 측점의 구조물 라벨 — kind(일반측점/BP/EP) 좌측에 자수정색 pill로 표기. */ +.b06-cross-card__structure { + padding: 1px 6px; + border-radius: var(--radius-pills); + background: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 16%, transparent); + color: var(--color-royal-amethyst, rgb(109 40 217)); + font-size: var(--text-caption); + white-space: nowrap; +} + .b06-cross-card > footer { border-top: 1px solid var(--color-border); } diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts index e99eab8b..69cce9cd 100644 --- a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts @@ -11,6 +11,7 @@ * ========================================================================== */ import "./B07_wf4_DesignDetail_UI_Style.css"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, @@ -136,9 +137,10 @@ function buildDrawingSidePanel( for (const [label, kind, items] of groups) { if (!items.length) continue; const section = document.createElement("section"); - section.className = "b07-drawing-group"; + section.className = "b07-drawing-group ui-collapsible"; section.dataset.kind = kind; const sectionTitle = document.createElement("h3"); + sectionTitle.className = "ui-collapsible__title"; sectionTitle.textContent = `${label} ${items.length}`; section.append(sectionTitle); for (const drawing of items) { @@ -159,6 +161,7 @@ function buildDrawingSidePanel( } panel.append(section); } + attachCollapsible(panel); return panel; } diff --git a/ui_template/ui_template_collapsible.ts b/ui_template/ui_template_collapsible.ts new file mode 100644 index 00000000..c179e683 --- /dev/null +++ b/ui_template/ui_template_collapsible.ts @@ -0,0 +1,21 @@ +/* ============================================================================= + * ui_template_collapsible.ts + * 페이지 공용 "접기 컨테이너" 동작. (B05 컨테이너 템플릿 기준으로 재사용) + * + * 규칙: + * - 컨테이너에 `ui-collapsible`, 그 제목 요소에 `ui-collapsible__title` 클래스를 준다. + * - 제목 **행 전체**를 클릭하면 컨테이너에 `is-collapsed`가 토글되어, 제목을 뺀 나머지 직속 + * 자식이 숨겨진다(펼침 ▾ / 접힘 ▸ 캐럿은 공용 CSS가 그린다). + * - 컨테이너 내부의 별도 접힘 항목(`
` 등)은 건드리지 않는다. + * + * 스타일은 전역 `ui_template_theme.css`에 있어 어떤 페이지에서도 클래스만 붙이면 동작한다. + * ========================================================================== */ + +/** root 아래 `.ui-collapsible__title` 클릭을 위임 처리해 해당 컨테이너를 접거나 편다. */ +export function attachCollapsible(root: HTMLElement): void { + root.addEventListener("click", (event) => { + const title = (event.target as HTMLElement).closest(".ui-collapsible__title"); + const container = title?.closest(".ui-collapsible"); + if (title && container) container.classList.toggle("is-collapsed"); + }); +} diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index df119202..d1887762 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -331,6 +331,33 @@ } } +/* ─── 공용 접기 컨테이너 (ui_template_collapsible.ts) ───────────────────────── + 컨테이너에 `ui-collapsible`, 제목에 `ui-collapsible__title`을 주면 제목 행 전체가 + 클릭 영역이 되고, 접히면 제목을 뺀 직속 자식이 숨겨진다. 펼침 ▾ / 접힘 ▸ 캐럿 표시. */ +.ui-collapsible__title { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-8, 8px); + cursor: pointer; + user-select: none; +} + +.ui-collapsible__title::after { + content: "▾"; + flex: 0 0 auto; + color: var(--color-text-muted); + font-size: 0.8em; +} + +.ui-collapsible.is-collapsed > .ui-collapsible__title::after { + content: "▸"; +} + +.ui-collapsible.is-collapsed > :not(.ui-collapsible__title) { + display: none; +} + html, body { margin: 0;