From 53c30db57bd1386e53aa46f0f62f44622d097a0d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 20:07:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(E01=C2=B7E04=C2=B7E06=C2=B7E07):=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EA=B8=80=20=3D=20No.+=EA=B1=B0=EB=A6=AC?= =?UTF-8?q?=20=EC=86=8C=EC=88=98=20=ED=95=9C=20=EC=9E=90=EB=A6=AC=20=C2=B7?= =?UTF-8?q?=20=EA=B0=84=EA=B2=A9=20=3D=20=EC=B8=A1=EC=A0=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EC=A0=95=20(PLAN=20566-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공용 stationLabel · inferStationInterval 로 한 벌(예 61+14.5) · 서버 label(STA. 꼴) 안 읽음 · 화면만 - E07 — 측점 표 · 지도 측점 글 · 셀 툴팁 · 지도 글 솎음이 20 m 고정 대신 추정 간격 - E04 — 구간 목록 · 위치(가까운 측점) · 지도 측점 글 · 툴팁 · 솎음 간격 - E06 — 위험 띠 · 측점 표 · 상세 제목 · 스테레오넷 제목 · 띠 양 끝 글 - E01 빠진 측점 띠 — interval 인자(기본 20 m · E02 · E03 이 측점 간격을 넘기면 그 값) Co-Authored-By: Claude Opus 5.5 --- E01_StabRegister/E01_StabRegister_UI_Missing.ts | 8 +++++--- E04_Stab3D/E04_Stab3D_UI_Map.ts | 13 +++++++------ E04_Stab3D/E04_Stab3D_UI_Page.ts | 10 +++++++--- E06_StabRock/E06_StabRock_UI_Page.ts | 9 +++++++++ E06_StabRock/E06_StabRock_UI_Result.ts | 4 ++-- E07_StabArea/E07_StabArea_UI_Map.ts | 12 +++++++----- E07_StabArea/E07_StabArea_UI_Page.ts | 5 ++--- 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/E01_StabRegister/E01_StabRegister_UI_Missing.ts b/E01_StabRegister/E01_StabRegister_UI_Missing.ts index eadd7b4bc..d711a3ccb 100644 --- a/E01_StabRegister/E01_StabRegister_UI_Missing.ts +++ b/E01_StabRegister/E01_StabRegister_UI_Missing.ts @@ -8,15 +8,17 @@ import { createBanner } from "@ui/ui_template_banner"; import { el } from "@ui/ui_template_elements"; import { t } from "@ui/ui_template_locale"; +import { stationLabel } from "../common_util/common_util_station"; -/** 빠진 측점이 없으면 빈 칸(띠 없음) */ -export function buildMissingBanner(missing: number[] | undefined): HTMLElement { +/** 빠진 측점이 없으면 빈 칸(띠 없음) · 측점 글 = No.+거리 — interval = 측점 간격(부르는 쪽이 + * `inferStationInterval(측점 목록)` 으로 넘김 · 없으면 20 m) */ +export function buildMissingBanner(missing: number[] | undefined, interval = 20): HTMLElement { const list = missing ?? []; if (!list.length) return el("div"); const box = createBanner({ tone: "warning", text: t("E_Missing_Head").replace("{n}", String(list.length)), - details: el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }), + details: el("p", { text: list.map((m) => stationLabel(m, interval, 1)).join(" · ") }), }); box.classList.add("e-missing"); box.dataset.role = "missing-stations"; diff --git a/E04_Stab3D/E04_Stab3D_UI_Map.ts b/E04_Stab3D/E04_Stab3D_UI_Map.ts index 43c0d8435..95d4f978d 100644 --- a/E04_Stab3D/E04_Stab3D_UI_Map.ts +++ b/E04_Stab3D/E04_Stab3D_UI_Map.ts @@ -7,6 +7,7 @@ import { el } from "@ui/ui_template_elements"; import { tooltipBox } from "@ui/ui_template_hover_info"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { t } from "@ui/ui_template_locale"; import { cutCells, footprintCenter } from "./logic/E04_Stab3D_Mesh"; import { @@ -43,8 +44,6 @@ interface View { const RANGE_COLOR = "#2563eb"; // 고른 구간 — 3D 노선 관과 같은 색 · 안전율 색 단계와 겹치지 않음 -const shortLabel = (label: string): string => label.replace(/^STA\./, "").replace(/\.0+$/, ""); - const css = (name: string, fallback: string): string => getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; @@ -60,6 +59,7 @@ export function createPlanMap(): PlanMap { let line: number[][] = []; let stations: StationPoint[] = []; + let interval = 20; // 측점 간격 — setRoute 때 측점 목록에서 추정 let range: [number, number] | null = null; let result: Result3d | null = null; let layer: MapLayer = "fs"; @@ -140,8 +140,8 @@ export function createPlanMap(): PlanMap { ctx.stroke(); } // 측점 — 화면 간격이 좁으면 다섯 개마다 이름 - // 이름 = "STA.0+020.000" → "0+020" · 이름끼리 70 px 이상 떨어지게 1 · 2 · 5 · 10 … 개마다 - const gap = stations.length > 1 ? 20 * view.scale : 999; + // 이름 = No.+거리(예 61+14.5) · 이름끼리 70 px 이상 떨어지게 1 · 2 · 5 · 10 … 개마다 + const gap = stations.length > 1 ? interval * view.scale : 999; const every = [1, 2, 5, 10, 25, 50, 100].find((n) => n * gap >= 70) ?? 100; ctx.font = "11px sans-serif"; ctx.textBaseline = "middle"; @@ -152,7 +152,7 @@ export function createPlanMap(): PlanMap { ctx.beginPath(); ctx.arc(px, py, i % every === 0 ? 3 : 1.5, 0, Math.PI * 2); ctx.fill(); - if (i % every === 0 && s.label) ctx.fillText(shortLabel(s.label), px + 6, py - 8); + if (i % every === 0) ctx.fillText(stationLabel(s.chainage_m, interval, 1), px + 6, py - 8); }); } @@ -311,7 +311,7 @@ export function createPlanMap(): PlanMap { tip.textContent = [ `${t("E04_Layer_FS")} ${f.toFixed(3)}`, `${t("E04_Layer_Volume")} ${v === null ? "-" : `${Math.round(v).toLocaleString()} m³`}`, - near ? `${t("E04_Tip_Station")} ${near.label ?? near.chainage_m.toFixed(1)}` : "", + near ? `${t("E04_Tip_Station")} ${stationLabel(near.chainage_m, interval, 1)}` : "", ] .filter(Boolean) .join("\n"); @@ -351,6 +351,7 @@ export function createPlanMap(): PlanMap { setRoute(nextLine, nextStations) { line = nextLine; stations = nextStations; + interval = inferStationInterval(stations); fit(); draw(); }, diff --git a/E04_Stab3D/E04_Stab3D_UI_Page.ts b/E04_Stab3D/E04_Stab3D_UI_Page.ts index ef7c04817..d855b06a8 100644 --- a/E04_Stab3D/E04_Stab3D_UI_Page.ts +++ b/E04_Stab3D/E04_Stab3D_UI_Page.ts @@ -24,6 +24,7 @@ import { createSegment } from "@ui/ui_template_segment"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { createText } from "@ui/ui_template_text"; import { mountGroupStep } from "../A00_Common/group_workflow"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { createPlanMap, type MapLayer, type StationPoint } from "./E04_Stab3D_UI_Map"; import { createView3D } from "./E04_Stab3D_UI_View3D"; import { buildSettingsCards, type Settings } from "./E04_Stab3D_UI_Settings"; @@ -246,6 +247,9 @@ export function renderE04Stab3D(root: HTMLElement): void { let result: Result3d | null = null; let stations2d: Station2d[] = []; let running = false; + /** 측점 글 = No.+거리(소수 한 자리) · 간격 = 측점 목록 추정 */ + const station = (chainage: number): string => + stationLabel(chainage, inferStationInterval(overview?.stations ?? []), 1); const remember = (): void => sessionStorage.setItem(cacheKey(projectId), JSON.stringify(settings.read())); @@ -278,7 +282,7 @@ export function renderE04Stab3D(root: HTMLElement): void { const byCondition = CONDITIONS.map((c) => savedOf.get(`${key}_${c}`)); const saved = savedOf.get(`${key}_${condition}`); const row = createListRow({ - label: `${section.start_m.toFixed(0)} – ${section.end_m.toFixed(0)} m`, + label: `${station(section.start_m)} – ${station(section.end_m)}`, trailing: byCondition .flatMap((x) => x @@ -313,7 +317,7 @@ export function renderE04Stab3D(root: HTMLElement): void { s.end_m, ); const ratio = ratio3d2d(s.f_min, f2d); - const station = overview?.stations.reduce( + const nearest = overview?.stations.reduce( (best, x) => !best || Math.abs(x.chainage_m - s.chainage_m) < Math.abs(best.chainage_m - s.chainage_m) ? x @@ -335,7 +339,7 @@ export function renderE04Stab3D(root: HTMLElement): void { ["E04_Result_Bishop", s.by_method.bishop.toFixed(3)], ["E04_Result_Janbu", s.by_method.janbu_corrected.toFixed(3)], ["E04_Result_Hovland", s.by_method.hovland.toFixed(3)], - ["E04_Result_Where", station?.label ?? `${s.chainage_m.toFixed(1)} m`], + ["E04_Result_Where", station(nearest?.chainage_m ?? s.chainage_m)], ["E04_Result_Cells", s.cells.toLocaleString()], ["E04_Result_Trials", s.trials.toLocaleString()], ["E04_Result_Time", `${result.elapsed_s.toFixed(1)} s`], diff --git a/E06_StabRock/E06_StabRock_UI_Page.ts b/E06_StabRock/E06_StabRock_UI_Page.ts index ce94a8e73..03840d0e0 100644 --- a/E06_StabRock/E06_StabRock_UI_Page.ts +++ b/E06_StabRock/E06_StabRock_UI_Page.ts @@ -22,6 +22,7 @@ import { } from "@ui/ui_template_elements"; import { t } from "@ui/ui_template_locale"; import { createSidePanels } from "@ui/ui_template_side_panels"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { mountGroupStep } from "../A00_Common/group_workflow"; import { buildJointCards, @@ -158,6 +159,11 @@ export function renderE06StabRock(root: HTMLElement): void { let result: RockResult | null = null; let active: string | null = null; let shown: StationResponse | null = null; + let interval = 20; // 측점 간격 — 처음 GET 의 측점 목록에서 추정 + /** 측점 글 = No.+거리(소수 한 자리) — 서버 label(STA. 꼴) 대신 */ + const relabel = (e: StationEntry): void => { + e.label = stationLabel(e.chainage_m, interval, 1); + }; const invalidCount = (): number => cards.querySelectorAll('[aria-invalid="true"]').length; const paintStatus = (): void => { @@ -241,6 +247,7 @@ export function renderE06StabRock(root: HTMLElement): void { shown = await api( `/projects/${projectId}/stab/rock/result?station=${encodeURIComponent(key)}&side=${sideName}`, ); + relabel(shown.entry); } catch (error) { shown = null; showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error"); @@ -251,6 +258,7 @@ export function renderE06StabRock(root: HTMLElement): void { async function loadResult(): Promise { const body = await api(`/projects/${projectId}/stab/rock/result`); result = body.result; + result?.stations.forEach(relabel); staleBar.hidden = !body.stale; const stations = result?.stations ?? []; const keep = stations.find((e) => entryId(e) === active); @@ -334,6 +342,7 @@ export function renderE06StabRock(root: HTMLElement): void { try { const body = await api(`/projects/${projectId}/stab/rock`); const stations = body.stations ?? []; + interval = inferStationInterval(stations); projection = body.joints.settings.projection; paintProjection(); view = { diff --git a/E06_StabRock/E06_StabRock_UI_Result.ts b/E06_StabRock/E06_StabRock_UI_Result.ts index 5619c350c..0cb7bd2ca 100644 --- a/E06_StabRock/E06_StabRock_UI_Result.ts +++ b/E06_StabRock/E06_StabRock_UI_Result.ts @@ -113,8 +113,8 @@ export function buildBand( }); const ticks = el("div", { className: "e06-band__ticks" }); ticks.append( - el("span", { text: `${lo.toFixed(0)} m` }), - el("span", { text: `${hi.toFixed(0)} m` }), + el("span", { text: list[0].label ?? `${lo.toFixed(0)} m` }), + el("span", { text: list[list.length - 1].label ?? `${hi.toFixed(0)} m` }), ); box.append(legend, strip, ticks); } else box.append(legend, note(t("E06_NoRock"))); diff --git a/E07_StabArea/E07_StabArea_UI_Map.ts b/E07_StabArea/E07_StabArea_UI_Map.ts index 196b02ff4..062e604ca 100644 --- a/E07_StabArea/E07_StabArea_UI_Map.ts +++ b/E07_StabArea/E07_StabArea_UI_Map.ts @@ -7,6 +7,7 @@ import { el } from "@ui/ui_template_elements"; import { tooltipBox } from "@ui/ui_template_hover_info"; import { t, type LocaleKey } from "@ui/ui_template_locale"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { CLASS_KEYS, cellIndex, rasterRgba, type Decoded } from "./logic/E07_StabArea_Raster"; export type MapLayer = "cls" | "fs"; @@ -34,8 +35,6 @@ interface View { scale: number; // px / m } -const shortLabel = (label: string): string => label.replace(/^STA\./, "").replace(/\.0+$/, ""); - const css = (name: string, fallback: string): string => getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; @@ -51,6 +50,7 @@ export function createAreaMap(): AreaMap { let line: number[][] = []; let stations: StationPoint[] = []; + let interval = 20; // 측점 간격 — setRoute 때 측점 목록에서 추정 let band = 100; let data: Decoded | null = null; let layer: MapLayer = "cls"; @@ -112,7 +112,7 @@ export function createAreaMap(): AreaMap { ctx.lineWidth = 2; path(); ctx.stroke(); - const gap = stations.length > 1 ? 20 * view.scale : 999; + const gap = stations.length > 1 ? interval * view.scale : 999; const every = [1, 2, 5, 10, 25, 50, 100].find((n) => n * gap >= 70) ?? 100; ctx.font = "11px sans-serif"; ctx.textBaseline = "middle"; @@ -124,7 +124,8 @@ export function createAreaMap(): AreaMap { ctx.beginPath(); ctx.arc(px, py, on ? 6 : i % every === 0 ? 3 : 1.5, 0, Math.PI * 2); ctx.fill(); - if ((on || i % every === 0) && s.label) ctx.fillText(shortLabel(s.label), px + 7, py - 9); + if (on || i % every === 0) + ctx.fillText(stationLabel(s.chainage_m, interval, 1), px + 7, py - 9); }); } @@ -227,7 +228,7 @@ export function createAreaMap(): AreaMap { t(`E07_Class_${CLASS_KEYS[data!.cls[i] - 1]}` as LocaleKey), `SI ${data!.si[i].toFixed(3)} · Fs ${data!.fs[i].toFixed(3)}`, `${t("E07_Tip_Slope")} ${data!.slope[i].toFixed(1)}°${Number.isNaN(m) ? "" : ` · m ${m.toFixed(2)}`}`, - near ? `${t("E07_Tip_Station")} ${near.label ?? near.chainage_m.toFixed(1)}` : "", + near ? `${t("E07_Tip_Station")} ${stationLabel(near.chainage_m, interval, 1)}` : "", ] .filter(Boolean) .join("\n"); @@ -265,6 +266,7 @@ export function createAreaMap(): AreaMap { setRoute(nextLine, nextStations) { line = nextLine; stations = nextStations; + interval = inferStationInterval(stations); fit(); draw(); }, diff --git a/E07_StabArea/E07_StabArea_UI_Page.ts b/E07_StabArea/E07_StabArea_UI_Page.ts index 32ae7397e..f310f504f 100644 --- a/E07_StabArea/E07_StabArea_UI_Page.ts +++ b/E07_StabArea/E07_StabArea_UI_Page.ts @@ -30,6 +30,7 @@ import { createSegment } from "@ui/ui_template_segment"; import { createTable } from "@ui/ui_template_table"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { createText } from "@ui/ui_template_text"; +import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; import { mountGroupStep } from "../A00_Common/group_workflow"; import { createAreaMap, type MapLayer, type StationPoint } from "./E07_StabArea_UI_Map"; import { @@ -87,8 +88,6 @@ const NUMBER_FIELDS: [string, LocaleKey, "soil" | "rain" | "band"][] = [ ]; const cacheKey = (projectId: string): string => `e07_settings:${projectId}`; -const shortLabel = (label: string | null, ch: number): string => - (label ?? `${ch.toFixed(1)} m`).replace(/^STA\./, "").replace(/\.0+$/, ""); async function api(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { @@ -379,7 +378,7 @@ export function renderE07StabArea(root: HTMLElement): void { { label: t("E07_Col_RiskArea"), align: "end" }, ], result.stations.map((r) => [ - shortLabel(r.label, r.chainage_m), + stationLabel(r.chainage_m, inferStationInterval(overview?.stations ?? []), 1), r.fs_min === undefined ? "-" : r.fs_min.toFixed(2), r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1), r.level ? chipCell(LEVEL_COLORS[r.level], t(`E07_Level_${r.level}` as LocaleKey)) : "",