From 205bc275cd9d1edf17f036f89dd39b7c6bd9a4e1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 20:00:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(E04):=20=ED=99=94=EB=A9=B4=20=EC=97=B4?= =?UTF-8?q?=EC=87=A0=EC=97=90=20=EC=A1=B0=EA=B1=B4=20=C2=B7=20=EA=B1=B4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=9A=B0=EA=B8=B0=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=ED=91=9C=EC=8B=9C=20=C2=B7=20=EC=97=B4=20?= =?UTF-8?q?=EB=95=8C=20=EC=A0=80=EC=9E=A5=20=EC=84=A4=EC=A0=95=20=EB=90=98?= =?UTF-8?q?=EC=82=B4=EB=A6=BC=20=C2=B7=20[=EB=8B=A4=EC=8B=9C=20=ED=95=B4?= =?UTF-8?q?=EC=84=9D]=20=EB=BA=8C=20(PLAN=20564-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구간 목록 = 구간마다 건기 · 우기 안전율 둘 다 · 고른 조건 결과를 엶 - 캐시가 없으면 저장된 결과의 탐색 설정으로 칸을 채움 - [다시 해석] 뺌 — 엔진이 바뀌면 지문(엔진 파일 해시)이 바뀌어 [해석]이 다시 셈 - [기본값]이 조건을 건기로 되돌리지 않음 Co-Authored-By: Claude Opus 5.5 --- E04_Stab3D/E04_Stab3D_UI_Page.ts | 84 ++++++++++++++++----------- E04_Stab3D/E04_Stab3D_UI_Settings.ts | 4 +- ui_template/ui_template_locale_e04.ts | 3 +- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/E04_Stab3D/E04_Stab3D_UI_Page.ts b/E04_Stab3D/E04_Stab3D_UI_Page.ts index e0a3dbbff..ef7c04817 100644 --- a/E04_Stab3D/E04_Stab3D_UI_Page.ts +++ b/E04_Stab3D/E04_Stab3D_UI_Page.ts @@ -4,7 +4,8 @@ * 좌 = 구간 목록 · 탐색 설정(기본 · 고급 접힘) · [해석] · 진행률 · [끊기] * 가운데 = 평면 지도(셀별 최소 안전율 · 임계 체적) · [3D 보기](지표 + 노선면 + 임계 활동면) * 우 = 구간 결과(3D 최소 · 2D 최소(같은 구간 E03 측점 · 3D 와 같은 조건 안전율 최소) · 3D/2D 비 · 체적 · 방법) - * 계산 = 서버 단독(POST …/stab/analyze3d · 뒤에서) · 화면은 값 · 그림만. 탐색 설정 = sessionStorage. + * 계산 = 서버 단독(POST …/stab/analyze3d · 뒤에서) · 화면은 값 · 그림만. 탐색 설정 = sessionStorage + * · 없으면 저장된 결과의 설정 · 결과 열쇠 = 구간 + 조건(건기 · 우기 따로 남음). * ========================================================================== */ import "./E04_Stab3D_UI_Style.css"; @@ -44,6 +45,7 @@ interface SavedSummary { f_min: number; volume: number; method: string; + condition: string; stale: boolean; } @@ -67,7 +69,10 @@ interface Progress { } const cacheKey = (projectId: string): string => `e04_settings:${projectId}`; -const sectionKey = (start: number, end: number): string => `${start.toFixed(2)}_${end.toFixed(2)}`; +const CONDITIONS = ["dry", "wet"] as const; +const rangeKey = (start: number, end: number): string => `${start.toFixed(2)}_${end.toFixed(2)}`; +const sectionKey = (start: number, end: number, condition: string): string => + `${rangeKey(start, end)}_${condition}`; async function api(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { @@ -218,16 +223,11 @@ export function renderE04Stab3D(root: HTMLElement): void { const analyzeButton = createButton({ label: t("E04_Analyze"), variant: "filled", - onClick: () => void analyze(false), + onClick: () => void analyze(), }); - const reanalyzeButton = createButton({ - label: t("E04_Reanalyze"), - variant: "ghost", - onClick: () => void analyze(true), - }); - analyzeButton.disabled = reanalyzeButton.disabled = true; + analyzeButton.disabled = true; const actions = el("div", { className: "ui-sidebar-actions" }); - actions.append(reanalyzeButton, analyzeButton); + actions.append(analyzeButton); const readOnly = note(t("E04_ReadOnly"), "e04-hint"); readOnly.hidden = true; panel.append( @@ -252,40 +252,46 @@ export function renderE04Stab3D(root: HTMLElement): void { let shownKey = ""; settings.onChange(() => { remember(); - const { start_m, end_m } = settings.read(); + const { start_m, end_m, condition } = settings.read(); map.setRange(start_m, end_m); paintSections(); - if (shownKey !== sectionKey(start_m, end_m)) void openResult(); + if (shownKey !== sectionKey(start_m, end_m, condition)) void openResult(); }); function paintButtons(): void { const editable = Boolean(overview?.editable); - analyzeButton.disabled = reanalyzeButton.disabled = !editable || running || !overview; + analyzeButton.disabled = !editable || running || !overview; readOnly.hidden = !overview || editable; } function paintSections(): void { if (!overview) return; - const { start_m, end_m } = settings.read(); + const { start_m, end_m, condition } = settings.read(); const savedOf = new Map(overview.saved.map((s) => [s.key, s])); - const custom = overview.saved.filter( - (s) => !overview!.sections.some((x) => sectionKey(x.start_m, x.end_m) === s.key), - ); - const items = [ - ...overview.sections, - ...custom.map((s) => ({ start_m: s.start_m, end_m: s.end_m })), - ]; + const items = new Map(overview.sections.map((x) => [rangeKey(x.start_m, x.end_m), x])); + for (const s of overview.saved) { + const key = rangeKey(s.start_m, s.end_m); + if (!items.has(key)) items.set(key, { start_m: s.start_m, end_m: s.end_m }); + } sectionList.replaceChildren( - ...items.map((section) => { - const key = sectionKey(section.start_m, section.end_m); - const saved = savedOf.get(key); + ...[...items].map(([key, section]) => { + 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`, - trailing: saved - ? `Fs ${saved.f_min.toFixed(2)}${saved.stale ? ` · ${t("E04_Stale")}` : ""}` - : "", + trailing: byCondition + .flatMap((x) => + x + ? [ + `${t(`E04_Short_${x.condition}` as LocaleKey)} ${x.f_min.toFixed(2)}${ + x.stale ? ` ${t("E04_Stale")}` : "" + }`, + ] + : [], + ) + .join(" · "), stripe: saved ? stepColor(FS_STEPS, saved.f_min) : "transparent", - active: key === sectionKey(start_m, end_m), + active: key === rangeKey(start_m, end_m), onClick: () => settings.setRange(section.start_m, section.end_m), }); row.classList.add("e04-section"); @@ -364,15 +370,19 @@ export function renderE04Stab3D(root: HTMLElement): void { resultBody.replaceChildren(list, ...notes); } + const currentKey = (): string => { + const { start_m, end_m, condition } = settings.read(); + return sectionKey(start_m, end_m, condition); + }; + async function openResult(): Promise { - const { start_m, end_m } = settings.read(); - const key = sectionKey(start_m, end_m); + const key = currentKey(); shownKey = key; const saved = overview?.saved.some((s) => s.key === key); result = saved ? await api(`/projects/${projectId}/stab/result3d?key=${key}`).catch(() => null) : null; - if (key !== sectionKey(settings.read().start_m, settings.read().end_m)) return; // 그새 바뀜 + if (key !== currentKey()) return; // 그새 바뀜 map.setResult(result); if (mode === "3d") view3d.show(result, overview?.line ?? [], layer); paintResult(); @@ -405,13 +415,13 @@ export function renderE04Stab3D(root: HTMLElement): void { } } - async function analyze(force: boolean): Promise { + async function analyze(): Promise { const values = settings.read(); const { start_m, end_m, condition, ...search } = values; try { const body = await api<{ status: string }>(`/projects/${projectId}/stab/analyze3d`, { method: "POST", - body: JSON.stringify({ start_m, end_m, condition, search, force }), + body: JSON.stringify({ start_m, end_m, condition, search }), }); if (body.status === "cached") { showToast(t("E04_Cached"), "info"); @@ -446,7 +456,7 @@ export function renderE04Stab3D(root: HTMLElement): void { ...ov.defaults, start_m: first?.start_m ?? 0, end_m: first?.end_m ?? 100, - condition: "dry", + condition: ov.saved[0]?.condition ?? "dry", }, ); map.setRoute(ov.line, ov.stations); @@ -455,6 +465,12 @@ export function renderE04Stab3D(root: HTMLElement): void { paintSections(); stations2d = await fetch2d(projectId); await openResult(); + // 캐시가 없으면 저장된 결과의 탐색 설정을 되살림 + const opened = result as Result3d | null; // openResult 가 바꿈(흐름 좁힘 풀기) + if (!cached && opened?.settings) { + settings.write(opened.settings as Partial); + remember(); + } const p = await api(`/projects/${projectId}/stab/progress3d`).catch(() => null); if (p?.running) void poll(); })(); diff --git a/E04_Stab3D/E04_Stab3D_UI_Settings.ts b/E04_Stab3D/E04_Stab3D_UI_Settings.ts index 215201303..e27573cfd 100644 --- a/E04_Stab3D/E04_Stab3D_UI_Settings.ts +++ b/E04_Stab3D/E04_Stab3D_UI_Settings.ts @@ -98,8 +98,8 @@ export function buildSettingsCards() { variant: "ghost", size: "sm", onClick: () => { - const range = { start_m: read().start_m, end_m: read().end_m }; - write({ ...read(), ...defaults, ...range, condition: "dry" }); + const { start_m, end_m, condition } = read(); // 구간 · 조건은 그대로 + write({ ...read(), ...defaults, start_m, end_m, condition }); changed(); }, }), diff --git a/ui_template/ui_template_locale_e04.ts b/ui_template/ui_template_locale_e04.ts index 49c898651..73d26b435 100644 --- a/ui_template/ui_template_locale_e04.ts +++ b/ui_template/ui_template_locale_e04.ts @@ -39,7 +39,6 @@ export const ui_locales_e04 = { E04_Condition_wet: ["우기 (지표까지 포화)", "Wet (saturated to surface)"], E04_Defaults: ["기본값", "Defaults"], E04_Analyze: ["해석", "Analyze"], - E04_Reanalyze: ["다시 해석", "Re-analyze"], E04_Cancel: ["끊기", "Stop"], E04_Running: ["해석 중", "Analyzing"], E04_Cancelled: ["해석을 끊었습니다.", "Analysis stopped."], @@ -48,6 +47,8 @@ export const ui_locales_e04 = { E04_ReadOnly: ["보기만 허용된 프로젝트입니다.", "View-only project."], E04_Saved: ["결과 있음", "Saved"], E04_Stale: ["옛 결과", "Outdated"], + E04_Short_dry: ["건기", "Dry"], + E04_Short_wet: ["우기", "Wet"], /* --- 가운데 지도 · 3D --- */ E04_View_Plan: ["평면 지도", "Plan map"],