Files
Aislo/A00_Common/group_workflow.ts
T
eomsangdonandClaude Opus 5.5 29512c001b feat(D 예정노선): D02 기준 지정 걷음 · 지점 마킹 = D03 노선분석 · 기준점 둘레 1 km 처음 받기 · 조용히 이어 받기 (PLAN 591-2 · 요구 452 · 453)
- D02 걷음 — 길 · 단계(PLAN 단계 둘 = 노선 분석 · 확정) · [>] · 대시보드 단계 점 · 서버 길 · 폴더 만들기 · 캐시 등록 · 글 · 가이드 폴더
- 등록 [등록] · 받은 예정노선 [보기] = D03 으로 곧바로
- D03 지점 = 긋기에 붙는 점 · 목록 칸(여섯 색 · 「지점 N」 · 🗑) · 핀 끌기 = 그 자리 노선 노드 같이 · [저장] 몸에 실음 · [초기화] = 선만 지우고 지점 남김
- 지도 우클릭 메뉴(부품 상자) — 여기서 긋기 시작(빈 땅 = 새 본선) · 갈래 시작 · 이어 긋기 · 지점 놓기 · 지점 지우기 · 긋기 끝
- 받기 — 처음 = 등록 기준점 둘레 ± 1 km(지점 · 선 없을 때) · 지점 · 선 · 지도 보기 가운데가 받은 범위 밖이면 조용히 이어 받음(단추 · 막는 창 걷음 · 실패만 [다시 받기])
- 좌표계 = 파일 머리 · 없으면 기준점 경도 TM 띠
- 시험: 529_3(D02 지점 API) 지움 → 591_2 로 갈음 · 280_2 · 528 · 530_2 · 547_1 · 548 · 553_4 · 556_2 · 560_4 · 401 · 54_2 = D02 단계 · 페이지 걷음에 맞춤 · 590_1 · 530_2_analyze = 사업연도 · 기준점 한 번에 읽음 · 591_1 D04 씨앗 시험 = 590-1 이 씨앗 줄 걷음에 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:19:34 +09:00

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_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();
}
}