- b_project_switch: currentProjectId(탭 세션 먼저 · 새 탭 = 마지막 값) · setCurrentProject · forgetProject · leaveProject(그 프로젝트 세션 칸 · 준비 표식 · 3D 보관함 몫) · reloadIfOtherProject(다른 프로젝트 화면 = 새로 엶) · guardProjectLeave · askProjectLeave([취소 · 폐기 · 저장] 한 번) · linkRouter(라우터가 이어 줌) · 화면 모듈은 부를 때만 들임
- b_asset_cache purgeProject · b_page_state 등록표 D02 · E 일곱 칸(옛 키 한 번 옮김) · router isProjectRoute
- 가드 helper: design_leave = 공용 가드 위 껍질(B05 고침 지문에 D · E 칸 안 섞임) · group_workflow { dirty, save, discard } · b_workflow_nav · b_page_scaffold · overlay · design_store · design_page 직접 읽기 → currentProjectId
- 시험 test_401_project_isolation 새로(탭 둘 흉내 · 떠남 · 보관함 · 가드 · 직접 읽기 한 곳) · 290_1 시드에 D02 · E 옛 키
- 낡은 시험 고침: 298_3 · 346_4 · 384_s · 520_3 헬퍼 = 공용 가드 · 탭 프로젝트 흉내 더함 · 298_3 탭 닫기 듣개 자리 b_project_switch 로 · 302_1 라우터 판별 글 isProjectRoute 꼴로 · 343_3 헬퍼 가짜 IndexedDB 내보냄
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* group_workflow.ts
|
|
* D 예정노선 · E 안정성평가 단계 묶음(요구 345 · PLAN 529-1) — B05~B09 틀(`createWorkflowLayout`)을
|
|
* 단계 목록 · 아이콘 · 주소 인자로 그대로 씀. 빈 단계 = 틀 · 좌측 패널([?] [<] [>]) · 단계 막대만.
|
|
* 페이지 길 = 각 폴더 `draft/<폴더>_Route.ts` · 본문이 생기면 그 폴더 페이지로 갈아 끼움.
|
|
* 떠남(560-4) = `beforeLeave` 가 true 일 때만 — 단계 아이콘 · [<] [>] 는 기다린 뒤 이동 ·
|
|
* 머리 메뉴 · 뒤로 가기는 같은 함수를 `setLeaveGuard` 로(false = 머묾).
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
API_BASE_URL,
|
|
PLAN_STEP_ROUTES,
|
|
STAB_STEP_ROUTES,
|
|
type RoutePath,
|
|
} from "@config/config_frontend";
|
|
import { el, hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
|
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
|
import { guardProjectLeave, setCurrentProject, type ProjectLeaveGuard } from "./b_project_switch";
|
|
import { navigateTo, setLeaveGuard, type PageRenderer } from "./router";
|
|
|
|
interface GroupFlow {
|
|
routes: readonly RoutePath[];
|
|
labels: readonly LocaleKey[];
|
|
icons: readonly string[];
|
|
}
|
|
|
|
const FLOWS: readonly GroupFlow[] = [
|
|
{
|
|
routes: PLAN_STEP_ROUTES,
|
|
labels: ["DE_Step_PlanBase", "DE_Step_PlanAnalysis", "DE_Step_PlanConfirm"],
|
|
icons: ["📍", "🧭", "✅"],
|
|
},
|
|
{
|
|
routes: STAB_STEP_ROUTES,
|
|
labels: [
|
|
"DE_Step_StabGround",
|
|
"DE_Step_StabSection",
|
|
"DE_Step_Stab3D",
|
|
"DE_Step_StabRock",
|
|
"DE_Step_StabArea",
|
|
"DE_Step_StabReport",
|
|
],
|
|
icons: ["🪨", "📐", "🧊", "⛰️", "🗺️", "📄"],
|
|
},
|
|
];
|
|
|
|
/** 종류별 단계 묶음(대시보드 표 단계 막대 · 528-2) — ROAD · 모르는 종류 = 없음(B 막대 그대로) */
|
|
export function groupFlowOf(
|
|
kind: string | null | undefined,
|
|
): { routes: readonly RoutePath[]; steps: string[]; icons: readonly string[] } | null {
|
|
const flow = { PLAN: FLOWS[0], STABILITY: FLOWS[1] }[kind ?? ""];
|
|
return flow ? { ...flow, steps: flow.labels.map((key) => t(key)) } : null;
|
|
}
|
|
|
|
/** 단계 틀을 세움 — mainContent · leftPanel 이 없으면 빈 단계 안내 · [?] 줄만 */
|
|
export function mountGroupStep(
|
|
root: HTMLElement,
|
|
route: RoutePath,
|
|
parts: {
|
|
mainContent?: HTMLElement;
|
|
leftPanel?: HTMLElement;
|
|
/** 옛 떠남(저장 등 · false = 머묾) — 페이지가 dirty · save 로 옮기면 지움 */
|
|
beforeLeave?: () => Promise<boolean>;
|
|
} & Partial<ProjectLeaveGuard> = {},
|
|
): void {
|
|
const flow = FLOWS.find((f) => f.routes.includes(route));
|
|
if (!flow) throw new Error(`단계 묶음 밖 주소: ${route}`);
|
|
const active = flow.routes.indexOf(route);
|
|
const leave = parts.beforeLeave;
|
|
if (leave) setLeaveGuard(async () => !root.isConnected || (await leave()));
|
|
// 떠남 셋(401) — 단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로 모두 라우터 물음 한 자리([취소 · 폐기 · 저장])
|
|
const { dirty, save, discard, message } = parts;
|
|
if (dirty && save) guardProjectLeave(root, { dirty, save, discard, message });
|
|
const layout = createWorkflowLayout({
|
|
title: t(flow.labels[active]),
|
|
steps: flow.labels.map((key) => t(key)),
|
|
icons: flow.icons,
|
|
activeStep: active,
|
|
routes: flow.routes,
|
|
leftPanel: parts.leftPanel,
|
|
mainContent:
|
|
parts.mainContent ?? el("p", { className: "app-placeholder", text: t("DE_Step_Empty") }),
|
|
onStepClick: (_index, next) => {
|
|
if (!leave) return navigateTo(next as RoutePath);
|
|
void leave().then((ok) => ok && navigateTo(next as RoutePath));
|
|
},
|
|
});
|
|
root.append(layout.root);
|
|
}
|
|
|
|
/** 빈 단계 페이지(길 선언의 load 가 부름) */
|
|
export const groupStepPage =
|
|
(route: RoutePath): PageRenderer =>
|
|
(root) =>
|
|
mountGroupStep(root, route);
|
|
|
|
/** 등록 화면 [등록](D01 · E01) — POST → 현재 프로젝트로 기억 → 첫 단계로 · 실패 = 서버 까닭 알림 */
|
|
export async function createGroupProject(
|
|
path: string,
|
|
body: object,
|
|
firstStep: RoutePath,
|
|
): Promise<void> {
|
|
showLoadingOverlay();
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = (await response.json().catch(() => ({}))) as {
|
|
project_id?: string;
|
|
detail?: unknown;
|
|
};
|
|
if (!response.ok || !data.project_id) {
|
|
const detail = typeof data.detail === "string" ? data.detail : `HTTP ${response.status}`;
|
|
throw new Error(detail);
|
|
}
|
|
setCurrentProject(data.project_id);
|
|
showToast(t("DE_Reg_Success"), "success");
|
|
navigateTo(firstStep);
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|