feat(564-3): E02 [해석 시작] · 진행 막대 · 단계 줄 · [끊기] — [저장] 뺌 · 갔다 와도 진행 이어 봄
- [저장] 자리에 [해석 시작](POST /stab/run = 검사 · 저장 → 체인) · 잠금판은 저장 순간만 - 진행 막대(ui_template_progress) · 단계 줄 다섯(ui_template_list_row · 상태 띠 · 걸린 초) · [끊기] - 열 때 GET /stab/run/progress — 도는 중이면 이어서 봄 · 끝난 결과면 단계 줄만 - locale_e 글 · 저장 안 됨 글 = 「해석 전」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
* E02_StabGround_UI_Page.ts
|
||||
* E02 지반 조건(요구서 345 4-3 · 요구 347-2 · 3 · 4 · PLAN 532-3) — B05~B09 틀(group_workflow).
|
||||
* GET /api/projects/{id}/stab/ground → 작업본(없으면 초기값) · 기본값 · 고칠 수 있는지.
|
||||
* 조작 = sessionStorage 캐시만(자동저장 없음) · [저장] = PUT(서버가 다시 검사 · 측점은 서버 값) ·
|
||||
* [초기화] = POST …/reset(초기값 덮어씀). 검사 = logic/E02_StabGround_Check(파이썬 짝).
|
||||
* 조작 = sessionStorage 캐시만(자동저장 없음) · [해석 시작] = POST …/stab/run(서버가 다시 검사 ·
|
||||
* 저장 → 2D · 3D · 암반 · 광역 · 보고서 체인 · PLAN 564-3) · 진행 = GET …/stab/run/progress(다른
|
||||
* 페이지에 갔다 와도 이어 보임) · [끊기] · [초기화] = POST …/reset(초기값 덮어씀).
|
||||
* 검사 = logic/E02_StabGround_Check(파이썬 짝).
|
||||
* 칸 위 마우스 = 틀린 까닭 · 기본값 · 근거(지식DB 07 · KDS 11 70 05).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -18,6 +20,8 @@ import {
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createListRow } from "@ui/ui_template_list_row";
|
||||
import { createProgressBar } from "@ui/ui_template_progress";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
@@ -38,8 +42,33 @@ interface GroundResponse {
|
||||
missing_stations?: number[];
|
||||
}
|
||||
|
||||
interface RunStep {
|
||||
key: string;
|
||||
state: string;
|
||||
seconds: number | null;
|
||||
}
|
||||
|
||||
/** 체인 진행(`E02_StabGround_Chain` 진행 꼴) */
|
||||
interface RunProgress {
|
||||
running: boolean;
|
||||
percent: number;
|
||||
steps: RunStep[];
|
||||
message: string | null;
|
||||
error: string | null;
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
const cacheKey = (projectId: string): string => `e02_ground_cache:${projectId}`;
|
||||
|
||||
/** 단계 줄 왼쪽 띠 색 — 기다림 = 없음 */
|
||||
const STEP_TONE: Record<string, string> = {
|
||||
run: "var(--color-accent)",
|
||||
done: "var(--color-success)",
|
||||
skip: "var(--color-success)",
|
||||
error: "var(--color-danger)",
|
||||
cancel: "var(--color-warning)",
|
||||
};
|
||||
|
||||
/** 검사 열쇠 → 범위 열쇠(limits) */
|
||||
function limitKey(path: string): string {
|
||||
const last = path.split(".").pop() ?? "";
|
||||
@@ -56,13 +85,13 @@ function errorText(error: CheckError, defaults: GroundDefaults): string {
|
||||
return error.code === "range" && bound ? `${text} (${bound[0]} ~ ${bound[1]})` : text;
|
||||
}
|
||||
|
||||
async function request(path: string, init?: RequestInit): Promise<GroundResponse> {
|
||||
async function request<T = GroundResponse>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...init,
|
||||
});
|
||||
const body = (await response.json().catch(() => ({}))) as GroundResponse & { detail?: unknown };
|
||||
const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown };
|
||||
if (!response.ok) {
|
||||
const detail = body.detail;
|
||||
if (typeof detail === "string") throw new Error(detail);
|
||||
@@ -76,29 +105,95 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const main = el("div", { className: "e02-page" });
|
||||
const status = el("div", { className: "e02-status" });
|
||||
const saveButton = createButton({
|
||||
label: t("Common_Btn_Save"),
|
||||
const runButton = createButton({
|
||||
label: t("E02_Run"),
|
||||
variant: "filled",
|
||||
onClick: () => void save(),
|
||||
onClick: () => void runChain(),
|
||||
});
|
||||
const resetButton = createButton({
|
||||
label: t("Common_Btn_Reset"),
|
||||
variant: "danger-ghost",
|
||||
onClick: () => void reset(),
|
||||
});
|
||||
saveButton.disabled = resetButton.disabled = true; // 읽어 온 뒤 켬
|
||||
runButton.disabled = resetButton.disabled = true; // 읽어 온 뒤 켬
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(resetButton, saveButton);
|
||||
actions.append(resetButton, runButton);
|
||||
const progressBar = createProgressBar();
|
||||
const progressText = createText("");
|
||||
const stepList = el("div", { className: "e02-run__steps" });
|
||||
const cancelButton = createButton({
|
||||
label: t("E02_Run_Cancel"),
|
||||
variant: "danger-ghost",
|
||||
size: "sm",
|
||||
onClick: () => void cancelChain(),
|
||||
});
|
||||
const runBox = el("div", { className: "e02-run" });
|
||||
runBox.append(progressBar.root, progressText, stepList, cancelButton);
|
||||
runBox.hidden = true;
|
||||
const panel = el("div", { className: "ui-sidebar-panel e02-panel" });
|
||||
panel.append(hook(createText(t("E02_Note")), "e02-hint"), status, actions);
|
||||
panel.append(
|
||||
hook(createText(t("E02_Note")), "e02-hint"),
|
||||
hook(createText(t("E02_Run_Hint")), "e02-hint"),
|
||||
status,
|
||||
runBox,
|
||||
actions,
|
||||
);
|
||||
mountGroupStep(root, ROUTES.E02_STAB_GROUND, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId) return void main.append(hook(createText(t("E02_NoProject")), "e02-empty"));
|
||||
|
||||
let view: E02View | null = null;
|
||||
let refreshStations = (): void => {};
|
||||
let dirty = false;
|
||||
let running = false;
|
||||
let missing: number[] = [];
|
||||
|
||||
const paintButtons = (): void => {
|
||||
runButton.disabled = resetButton.disabled = !view?.editable || running;
|
||||
};
|
||||
|
||||
const paintRun = (p: RunProgress): void => {
|
||||
running = p.running;
|
||||
runBox.hidden = !p.running && p.steps.every((s) => s.state === "wait");
|
||||
progressBar.set(p.percent / 100, `${p.percent}%`);
|
||||
progressText.textContent = p.running ? `${t("E02_Run_Running")} · ${p.message ?? ""}` : "";
|
||||
stepList.replaceChildren(
|
||||
...p.steps.map((s) =>
|
||||
createListRow({
|
||||
label: t(`E02_Run_Step_${s.key}` as LocaleKey),
|
||||
trailing: [
|
||||
t(`E02_Run_State_${s.state}` as LocaleKey),
|
||||
s.seconds !== null && s.state !== "wait" ? `${s.seconds} s` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
stripe: STEP_TONE[s.state] ?? "transparent",
|
||||
active: s.state === "run",
|
||||
size: 28,
|
||||
}),
|
||||
),
|
||||
);
|
||||
cancelButton.hidden = !p.running;
|
||||
paintButtons();
|
||||
};
|
||||
|
||||
/** 체인이 끝날 때까지 — 다른 페이지로 가면 멈춤(체인은 서버에서 계속 · 다시 오면 이어 봄) */
|
||||
async function poll(): Promise<void> {
|
||||
while (root.isConnected) {
|
||||
const p = await request<RunProgress>(`/projects/${projectId}/stab/run/progress`).catch(
|
||||
() => null,
|
||||
);
|
||||
if (!p) return;
|
||||
paintRun(p);
|
||||
if (!p.running) {
|
||||
if (p.error) showToast(p.error, "error");
|
||||
else if (p.cancelled) showToast(t("E02_Run_Cancelled"), "info");
|
||||
else showToast(t("E02_Run_Done"), "success");
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
const paintStatus = (errors: CheckError[]): void => {
|
||||
const lines: string[] = [];
|
||||
if (view && !view.editable) lines.push(t("E02_ReadOnly"));
|
||||
@@ -158,12 +253,13 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
view.ground = ground;
|
||||
view.defaults = body.defaults;
|
||||
view.editable = body.editable ?? false;
|
||||
saveButton.disabled = resetButton.disabled = !view.editable;
|
||||
paintButtons();
|
||||
render();
|
||||
};
|
||||
|
||||
async function save(): Promise<void> {
|
||||
if (!view?.editable) return;
|
||||
/** [해석 시작] — 서버가 검사 · 저장(잠금판은 이 순간만) → 체인은 뒤에서 */
|
||||
async function runChain(): Promise<void> {
|
||||
if (!view?.editable || running) return;
|
||||
const errors = mark();
|
||||
if (errors.length) {
|
||||
showToast(
|
||||
@@ -174,16 +270,30 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
}
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const body = await request(`/projects/${projectId}/stab/ground`, {
|
||||
method: "PUT",
|
||||
const body = await request<GroundResponse & RunProgress>(`/projects/${projectId}/stab/run`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ground: view.ground }),
|
||||
});
|
||||
adopt({ ...body, defaults: view.defaults, editable: view.editable }, false);
|
||||
showToast(t("E02_Saved"), "success");
|
||||
adopt({ ground: body.ground, defaults: view.defaults, editable: view.editable }, false);
|
||||
paintRun(body);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
||||
return;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
void poll();
|
||||
}
|
||||
|
||||
async function cancelChain(): Promise<void> {
|
||||
cancelButton.disabled = true;
|
||||
try {
|
||||
await request(`/projects/${projectId}/stab/run/cancel`, { method: "POST" });
|
||||
showToast(t("E02_Run_Stopping"), "info");
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
cancelButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +356,9 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
},
|
||||
};
|
||||
adopt(body, true);
|
||||
const p = await request<RunProgress>(`/projects/${projectId}/stab/run/progress`);
|
||||
paintRun(p);
|
||||
if (p.running) void poll();
|
||||
} catch (error) {
|
||||
main.replaceChildren(
|
||||
hook(
|
||||
|
||||
@@ -121,3 +121,20 @@
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* [해석 시작] 진행 — 막대 · 단계 줄 · [끊기] */
|
||||
.e02-run {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
margin: var(--spacing-8) 0;
|
||||
}
|
||||
|
||||
.e02-run[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.e02-run__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,10 @@ export const ui_locales_e = {
|
||||
E02_Col_Points: ["점 (offset · 깊이)", "Points (offset · depth)"],
|
||||
|
||||
/* --- 상태 · 알림 --- */
|
||||
E02_Unsaved: ["저장 안 된 변경이 있습니다.", "There are unsaved changes."],
|
||||
E02_Unsaved: [
|
||||
"해석 전 — 바뀐 값은 [해석 시작] 때 저장됩니다.",
|
||||
"Not analyzed — changes are saved on [Run analysis].",
|
||||
],
|
||||
E02_ReadOnly: ["보기만 허용된 프로젝트입니다.", "This project is view-only."],
|
||||
E02_Saved: ["저장했습니다.", "Saved."],
|
||||
E02_ResetDone: ["초기값으로 되돌렸습니다.", "Restored to initial values."],
|
||||
@@ -127,6 +130,35 @@ export const ui_locales_e = {
|
||||
E02_UserLayer: ["사용자 지층 — 출처 없음", "User layer — no source"],
|
||||
E02_NoProject: ["프로젝트를 먼저 고르세요.", "Pick a project first."],
|
||||
|
||||
/* --- [해석 시작] 체인(PLAN 564-3) --- */
|
||||
E02_Run: ["해석 시작", "Run analysis"],
|
||||
E02_Run_Hint: [
|
||||
"[해석 시작] = 지반 값 저장 → 2D · 3D(모든 구간 · 건기 · 우기) · 암반 · 광역 · 보고서를 차례로 셈. 입력이 같은 단계는 건너뜀 · 1.2 km 약 25~30 분 · 다른 페이지로 가도 계속 돎.",
|
||||
"[Run analysis] = save ground values → 2D · 3D (all sections · dry · wet) · rock · area · report in turn. Unchanged steps are skipped · about 25–30 min per 1.2 km · keeps running on other pages.",
|
||||
],
|
||||
E02_Run_Running: ["해석 중", "Running"],
|
||||
E02_Run_Cancel: ["끊기", "Stop"],
|
||||
E02_Run_Stopping: [
|
||||
"끊는 중 — 지금 조각이 끝나면 멈춥니다.",
|
||||
"Stopping — halts after the current piece.",
|
||||
],
|
||||
E02_Run_Done: ["해석을 마쳤습니다.", "Analysis finished."],
|
||||
E02_Run_Cancelled: [
|
||||
"해석을 끊었습니다 — 끝난 단계는 저장됨.",
|
||||
"Analysis stopped — finished steps are saved.",
|
||||
],
|
||||
E02_Run_Step_2d: ["2D 측점 해석", "2D station analysis"],
|
||||
E02_Run_Step_3d: ["3D 구간 해석", "3D section analysis"],
|
||||
E02_Run_Step_rock: ["암반 해석", "Rock analysis"],
|
||||
E02_Run_Step_area: ["광역 위험", "Area hazard"],
|
||||
E02_Run_Step_report: ["보고서", "Report"],
|
||||
E02_Run_State_wait: ["기다림", "Waiting"],
|
||||
E02_Run_State_run: ["도는 중", "Running"],
|
||||
E02_Run_State_done: ["끝남", "Done"],
|
||||
E02_Run_State_skip: ["최신 · 건너뜀", "Up to date · skipped"],
|
||||
E02_Run_State_error: ["실패", "Failed"],
|
||||
E02_Run_State_cancel: ["끊음", "Stopped"],
|
||||
|
||||
/* --- 입력 검사(코드 = 파이썬 check_ground 와 같은 낱말) --- */
|
||||
E02_Err_number: ["숫자를 넣으세요", "Enter a number"],
|
||||
E02_Err_range: ["범위 밖", "Out of range"],
|
||||
|
||||
Reference in New Issue
Block a user