feat(E04): 화면 열쇠에 조건 · 건기 · 우기 결과 함께 표시 · 열 때 저장 설정 되살림 · [다시 해석] 뺌 (PLAN 564-2)
- 구간 목록 = 구간마다 건기 · 우기 안전율 둘 다 · 고른 조건 결과를 엶 - 캐시가 없으면 저장된 결과의 탐색 설정으로 칸을 채움 - [다시 해석] 뺌 — 엔진이 바뀌면 지문(엔진 파일 해시)이 바뀌어 [해석]이 다시 셈 - [기본값]이 조건을 건기로 되돌리지 않음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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<void> {
|
||||
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<Result3d>(`/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<void> {
|
||||
async function analyze(): Promise<void> {
|
||||
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<Settings>);
|
||||
remember();
|
||||
}
|
||||
const p = await api<Progress>(`/projects/${projectId}/stab/progress3d`).catch(() => null);
|
||||
if (p?.running) void poll();
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user