Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -169,6 +169,27 @@ export async function purgeOtherProjects(projectId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/** 그 프로젝트 몫만 지운다(401 · 떠날 때) — 다른 탭 · 다른 프로젝트 줄은 둠 · 이 탭 준비 표식도 버림. */
|
||||
export async function purgeProject(projectId: string): Promise<void> {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
const stamp = window.sessionStorage.getItem(PRELOADED_SIGNATURE_KEY);
|
||||
if (stamp?.startsWith(`${projectId}|`))
|
||||
window.sessionStorage.removeItem(PRELOADED_SIGNATURE_KEY);
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 표식이 없다. */
|
||||
}
|
||||
await deleteWhere((store) => {
|
||||
const cursorRequest = store.index("projectId").openKeyCursor(IDBKeyRange.only(projectId));
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
if (!cursor) return;
|
||||
store.delete(cursor.primaryKey);
|
||||
cursor.continue();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 보관함에서 바이트만 꺼낸다 — 네트워크를 타지 않는다. 없으면 null.
|
||||
* 주소에 열쇠가 박히는 자료(3D 코리도)는 이걸로 **먼저 보고** 없을 때만 받는다. */
|
||||
export async function readCachedBytes(projectId: string, url: string): Promise<ArrayBuffer | null> {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import {
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
WORKFLOW_STEP_ROUTES,
|
||||
type WorkflowState,
|
||||
} from "./b_workflow_nav";
|
||||
import { currentProjectId } from "./b_project_switch";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
@@ -107,7 +107,7 @@ export async function renderPendingWorkflow(
|
||||
opts: PendingWorkflowOptions,
|
||||
): Promise<void> {
|
||||
injectScaffoldStyles();
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
let workflowState: WorkflowState | undefined;
|
||||
if (projectId) {
|
||||
try {
|
||||
|
||||
@@ -214,6 +214,14 @@ export const STATE_REGISTRY = {
|
||||
"route-draft-hold": { bucket: "draft", scope: "project" },
|
||||
/** D04 연도별 지정 업체 목록(531-3) — [저장] 때 PUT plan/assignees. */
|
||||
"d04-assignees": { bucket: "draft", scope: "project" },
|
||||
/* D02 · E 페이지 캐시(401) — 진입 · 떠남 비우기가 같은 표를 봄 · 옛 키는 한 번 옮김. */
|
||||
"d02-points": { bucket: "draft", scope: "project", legacy: (p) => `d02_points_${p}` },
|
||||
"e02-ground": { bucket: "draft", scope: "project", legacy: (p) => `e02_ground_cache:${p}` },
|
||||
"e03-prob": { bucket: "draft", scope: "project", legacy: (p) => `e03_prob_cache:${p}` },
|
||||
"e04-settings": { bucket: "draft", scope: "project", legacy: (p) => `e04_settings:${p}` },
|
||||
"e05-report": { bucket: "draft", scope: "project", legacy: (p) => `e05_report_cache:${p}` },
|
||||
"e06-rock": { bucket: "draft", scope: "project", legacy: (p) => `e06_rock_cache:${p}` },
|
||||
"e07-settings": { bucket: "draft", scope: "project", legacy: (p) => `e07_settings:${p}` },
|
||||
|
||||
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
|
||||
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
|
||||
|
||||
@@ -1,21 +1,70 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/b_project_switch.ts
|
||||
* 다른 프로젝트에 들어갈 때 **그 밖 프로젝트의 캐시**를 지운다(계획서 290-1 · 요구 183-1).
|
||||
* 프로젝트마다 따로 동작(401) — 탭마다 지금 프로젝트 · 떠남 물음 · 떠날 때 · 들 때 비움.
|
||||
*
|
||||
* 지우는 것 — 이 탭 세션의 다른 프로젝트 칸(초안 · 결과 · 고름 · 옛 키 · 페이지별 칸)과
|
||||
* 3D 지형 보관함(IndexedDB). 남기는 것 — 지금 프로젝트 것 · 계정 취향(pref, localStorage).
|
||||
* 다른 프로젝트에 저장 안 한 고침(초안)이 있으면 확인창 — 「남기기」면 그 초안만 둔다.
|
||||
* 같은 프로젝트로 다시 들어오면(새로고침 포함) 아무것도 안 지운다(탭마다 들어온 표식).
|
||||
* 지금 프로젝트 = 탭 세션 칸 먼저 · 없으면 마지막으로 연 프로젝트(localStorage · 새 탭이 이어받음).
|
||||
* 들 때(`enterProject`) — 다른 프로젝트 세션 칸 · 3D 보관함을 지움(290-1) · 저장 안 한 고침이 있으면 확인창.
|
||||
* 떠날 때(`leaveProject`) — 그 프로젝트 세션 칸 · 준비 표식 · 3D 보관함 몫 · 탭 프로젝트 표식 · 취향은 남김.
|
||||
* 다른 프로젝트 화면을 그리려 하면 새로 엶(`reloadIfOtherProject` · 모듈 메모리 통째 비움).
|
||||
* 떠남 물음(`guardProjectLeave`) — 화면에 붙은 가드를 모아 [취소 · 폐기 · 저장] 한 번.
|
||||
* 화면 모듈은 부를 때만 들임 · 라우터는 스스로 이어 줌(`linkRouter`) — 설계 가게 · 인쇄 묶음이 이 파일을 부름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import { purgeOtherProjects } from "./b_asset_cache";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "../config/config_frontend";
|
||||
import { STATE_REGISTRY, type StateEntry } from "./b_page_state";
|
||||
|
||||
/** 이 탭이 마지막으로 들어온 프로젝트. */
|
||||
const ENTERED_KEY = "aislo:entered-project";
|
||||
|
||||
/** 이 탭의 지금 프로젝트(401) — 탭 세션 칸 먼저 · 없으면 마지막으로 연 프로젝트(새 탭) · 없으면 "". */
|
||||
export function currentProjectId(): string {
|
||||
try {
|
||||
return (
|
||||
window.sessionStorage.getItem(ENTERED_KEY) ||
|
||||
window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) ||
|
||||
""
|
||||
);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** 지금 프로젝트로 적음 — 이 탭 + 새 탭이 이어받을 마지막 값. 다른 프로젝트 캐시 비움은 `enterProject`. */
|
||||
export function setCurrentProject(projectId: string): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(ENTERED_KEY, projectId);
|
||||
window.localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId);
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 기억 못 함. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 지워진 · 권한 없는 프로젝트를 잊음 — 이 탭 표식 · 마지막 값이 그 프로젝트일 때만. */
|
||||
export function forgetProject(projectId: string): void {
|
||||
try {
|
||||
if (window.sessionStorage.getItem(ENTERED_KEY) === projectId)
|
||||
window.sessionStorage.removeItem(ENTERED_KEY);
|
||||
if (window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) === projectId)
|
||||
window.localStorage.removeItem(CURRENT_PROJECT_ID_KEY);
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 잊을 것이 없다. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 이 화면(모듈 메모리)이 담은 프로젝트 — 프로젝트 화면을 그릴 때마다 견줌. */
|
||||
let heldProject = "";
|
||||
|
||||
/** 다른 프로젝트 화면을 그리려 하면 새로 엶(401 · 모듈 메모리 통째 비움) · true = 새로 여는 중. */
|
||||
export function reloadIfOtherProject(): boolean {
|
||||
const projectId = currentProjectId();
|
||||
if (heldProject && projectId && projectId !== heldProject) {
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
if (projectId) heldProject = projectId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const MARK_P = "\u0001";
|
||||
const MARK_R = "\u0002";
|
||||
|
||||
@@ -107,6 +156,7 @@ export async function enterProject(
|
||||
const keep = new Set<string>();
|
||||
for (const other of plan.draftProjects) {
|
||||
const name = await nameOf(other).catch(() => other);
|
||||
const { showConfirmDialog } = await import("@ui/ui_template_elements");
|
||||
const drop = await showConfirmDialog(
|
||||
`「${name}」에 저장 안 한 고침이 있음 — 지우고 열기 / 남기기`,
|
||||
"지우고 열기",
|
||||
@@ -115,7 +165,8 @@ export async function enterProject(
|
||||
if (!drop) keep.add(other);
|
||||
}
|
||||
plan.doomed(keep).forEach((key) => window.sessionStorage.removeItem(key));
|
||||
window.sessionStorage.setItem(ENTERED_KEY, projectId);
|
||||
setCurrentProject(projectId);
|
||||
const { purgeOtherProjects } = await import("./b_asset_cache");
|
||||
await purgeOtherProjects(projectId);
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 캐시를 못 지워도 프로젝트는 연다. */
|
||||
@@ -136,6 +187,7 @@ export async function wipeAccountCache(): Promise<void> {
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 지울 것이 없다. */
|
||||
}
|
||||
const { purgeOtherProjects } = await import("./b_asset_cache");
|
||||
await purgeOtherProjects(""); // 빈 id 보다 큰 열쇠 = 모든 프로젝트
|
||||
}
|
||||
|
||||
@@ -153,3 +205,102 @@ export async function wipeIfOwnerChanged(user: string | number): Promise<boolean
|
||||
await wipeAccountCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트를 떠남(401) — 이 탭 세션의 그 프로젝트 칸 · 준비 표식 · 3D 보관함 몫을 지움.
|
||||
* 탭 프로젝트 표식(뒤로 가기 · 재진입) · 취향 · 입력 설정 · CAD 복구본(localStorage)은 남김.
|
||||
*/
|
||||
export async function leaveProject(projectId: string): Promise<void> {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
// 프로젝트 칸 열쇠는 모두 프로젝트 번호(uuid)를 품음 — 등록표 · 옛 키 · 페이지 칸을 한 줄로.
|
||||
Object.keys(sessionEntries())
|
||||
.filter((key) => key.includes(projectId))
|
||||
.forEach((key) => window.sessionStorage.removeItem(key));
|
||||
} catch {
|
||||
/* 저장소가 막힌 브라우저 — 지울 것이 없다. */
|
||||
}
|
||||
const { purgeProject } = await import("./b_asset_cache");
|
||||
await purgeProject(projectId);
|
||||
}
|
||||
|
||||
/** 페이지가 알리는 떠남 셋(401) — 고침 있나 · 저장(false · 던짐 = 머묾) · 폐기. */
|
||||
export interface ProjectLeaveGuard {
|
||||
dirty: () => boolean;
|
||||
save: () => Promise<unknown>;
|
||||
discard?: () => void | Promise<void>;
|
||||
/** 물음 글 — 없으면 공용 */
|
||||
message?: string;
|
||||
/** 같은 페이지로 보는 주소(B05 ↔ B06) — 그리로 가면 안 묻고 다시 걺 */
|
||||
samePage?: readonly string[];
|
||||
}
|
||||
|
||||
/** 라우터의 떠남 물음 자리 — 라우터가 들 때 이어 줌(라우터 → 이 파일 한 방향 · 페이지 표를 안 끌게). */
|
||||
interface RouterLink {
|
||||
currentRoute: () => string;
|
||||
setLeaveGuard: (guard: (() => Promise<boolean>) | null) => void;
|
||||
}
|
||||
let routerLink: RouterLink | null = null;
|
||||
export function linkRouter(link: RouterLink): void {
|
||||
routerLink = link;
|
||||
}
|
||||
|
||||
const guards: Array<{ root: HTMLElement; guard: ProjectLeaveGuard }> = [];
|
||||
let unloadHooked = false;
|
||||
|
||||
/** 화면에 붙은 가드만 — 빠진 페이지 것은 버림. */
|
||||
function liveGuards(): ProjectLeaveGuard[] {
|
||||
for (let index = guards.length - 1; index >= 0; index -= 1) {
|
||||
if (!guards[index].root.isConnected) guards.splice(index, 1);
|
||||
}
|
||||
return guards.map((entry) => entry.guard);
|
||||
}
|
||||
|
||||
/** 저장 안 한 고침이 있나 — 화면에 붙은 가드 모두. */
|
||||
export const projectLeaveDirty = (): boolean => liveGuards().some((guard) => guard.dirty());
|
||||
|
||||
/** 떠나도 되나 — 고친 가드를 모아 [취소 · 폐기 · 저장] 한 번 · true = 넘어감. */
|
||||
export async function askProjectLeave(): Promise<boolean> {
|
||||
const live = liveGuards();
|
||||
if (!live.length) return true;
|
||||
const route = routerLink?.currentRoute() ?? "";
|
||||
if (live.some((guard) => guard.samePage?.includes(route))) {
|
||||
// 라우터가 「넘어감」 뒤 물음을 내림 — 같은 페이지 안 이동이면 한 틱 뒤 다시 걺.
|
||||
window.setTimeout(() => liveGuards().length && routerLink?.setLeaveGuard(askProjectLeave), 0);
|
||||
return true;
|
||||
}
|
||||
const dirty = live.filter((guard) => guard.dirty());
|
||||
if (!dirty.length) return true;
|
||||
const [{ leaveOrStay }, { t }] = await Promise.all([
|
||||
import("@ui/ui_template_leave_dialog"),
|
||||
import("@ui/ui_template_locale"),
|
||||
]);
|
||||
const message = (dirty.length === 1 && dirty[0].message) || t("Common_Leave_Unsaved");
|
||||
const got = await leaveOrStay(message, {
|
||||
save: async () => {
|
||||
for (const guard of dirty) {
|
||||
if (guard.dirty() && (await guard.save()) === false) throw new Error("저장 못 함");
|
||||
}
|
||||
},
|
||||
stillDirty: () => dirty.some((guard) => guard.dirty()),
|
||||
discard: async () => {
|
||||
for (const guard of dirty) await guard.discard?.();
|
||||
},
|
||||
});
|
||||
return got !== "stay";
|
||||
}
|
||||
|
||||
/** 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂 · 탭 닫기도 물음. */
|
||||
export function guardProjectLeave(root: HTMLElement, guard: ProjectLeaveGuard): void {
|
||||
liveGuards(); // 빠진 옛 페이지를 붙들지 않음
|
||||
guards.push({ root, guard });
|
||||
if (!unloadHooked) {
|
||||
unloadHooked = true;
|
||||
window.addEventListener("beforeunload", (event) => {
|
||||
if (!projectLeaveDirty()) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
});
|
||||
}
|
||||
routerLink?.setLeaveGuard(askProjectLeave);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
API_BASE_URL,
|
||||
CURRENT_PROJECT_ID_KEY,
|
||||
ROUTES,
|
||||
type RoutePath,
|
||||
} from "@config/config_frontend";
|
||||
import { API_BASE_URL, ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import {
|
||||
hideLoadingOverlay,
|
||||
setLoadingOverlayProgress,
|
||||
@@ -16,7 +11,12 @@ import {
|
||||
fetchSurfaceStatus,
|
||||
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { isProjectPreloaded, setPreloadTarget } from "./b_asset_cache";
|
||||
import { enterProject } from "./b_project_switch";
|
||||
import {
|
||||
currentProjectId,
|
||||
enterProject,
|
||||
forgetProject,
|
||||
setCurrentProject,
|
||||
} from "./b_project_switch";
|
||||
import { navigateTo } from "./router";
|
||||
|
||||
export interface WorkflowState {
|
||||
@@ -77,7 +77,7 @@ export async function fetchWorkflowState(projectId: string): Promise<WorkflowSta
|
||||
/** 기억한 현재 프로젝트가 서버에 없으면(지워짐 · 권한 없음) 기억을 비우고 true.
|
||||
* 그 밖의 실패(서버 오류 · 끊김)는 false — 기억을 지우지 않고 페이지가 하던 대로 둔다. */
|
||||
export async function dropGoneProject(): Promise<boolean> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
if (!projectId) return false;
|
||||
try {
|
||||
await fetchWorkflowState(projectId);
|
||||
@@ -85,7 +85,7 @@ export async function dropGoneProject(): Promise<boolean> {
|
||||
} catch (error) {
|
||||
const status = (error as { status?: number }).status;
|
||||
if (status !== 403 && status !== 404) return false;
|
||||
localStorage.removeItem(CURRENT_PROJECT_ID_KEY);
|
||||
forgetProject(projectId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function goToWorkflowStage(projectId: string, route: RoutePath): Promise<
|
||||
projectId,
|
||||
async (id) => (await fetchWorkflowState(id)).project_name || id,
|
||||
).then(() => {
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId);
|
||||
setCurrentProject(projectId);
|
||||
// 단계가 바뀌어 이동하는 자리다 — 캐시를 버려 다음 화면이 새 상태를 본다.
|
||||
clearWorkflowStateCache();
|
||||
if (!PRELOAD_REQUIRED_ROUTES.includes(route)) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 공용 물음(`ui_template_leave_dialog` · [취소] [폐기] [저장]).
|
||||
*
|
||||
* 고침 = 초안 통 칸 중 고침이 아닌 칸(고름 · 관 id 장부 · 종류 캐시 · 고른 구조물 넘김 · 소단 사본 = 구조물 파생) ·
|
||||
* 다른 페이지 칸(b08 · b09) · 저장 뒤에도 남기는 표준 횡단면(std-cross)을 뺀 것.
|
||||
* 다른 페이지 칸(b08 · b09 · D · E) · 저장 뒤에도 남기는 표준 횡단면(std-cross)을 뺀 것.
|
||||
* 횡단 초안은 [저장] 뒤에도 세션에 남음 — 그래서 「있음」이 아니라 **마지막 깨끗한 때와 다름**으로 봄:
|
||||
* [저장] · [확정]이 끝나면 `markDesignClean` 이 그때 지문을 적음 · [초기화]는 초안을 다 지우고 새로 읽음.
|
||||
* B05 ↔ B06 는 한 페이지 — 그 사이 이동은 묻지 않음.
|
||||
@@ -12,9 +12,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { listenWhileAlive } from "@ui/ui_template_window_watch";
|
||||
import { guardProjectLeave } from "../b_project_switch";
|
||||
import {
|
||||
clearState,
|
||||
namesInBucket,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
stateKey,
|
||||
type StateName,
|
||||
} from "../b_page_state";
|
||||
import { currentRoute, setLeaveGuard } from "../router";
|
||||
|
||||
/** 초안 통이지만 고침으로 안 보는 칸 — 묻지 않고 [폐기]에도 안 지움. */
|
||||
const NOT_EDITS = new Set<StateName>([
|
||||
@@ -34,7 +32,7 @@ const NOT_EDITS = new Set<StateName>([
|
||||
"std-cross",
|
||||
"berm", // 소단 사본 — 열 때 시설 줄이 구조물에서 씀(272 · 고침은 `structures` 칸)
|
||||
]);
|
||||
const OTHER_PAGE = /^b0[89]-/;
|
||||
const OTHER_PAGE = /^(b0[89]|d0\d|e0\d)-/;
|
||||
|
||||
export function editDraftNames(): StateName[] {
|
||||
return namesInBucket("draft").filter((name) => !NOT_EDITS.has(name) && !OTHER_PAGE.test(name));
|
||||
@@ -93,10 +91,8 @@ export function discardDesignEdits(projectId: string): void {
|
||||
editDraftNames().forEach((name) => clearState(name, projectId));
|
||||
}
|
||||
|
||||
const SAME_PAGE = new Set<string>([ROUTES.B05_PROFILE, ROUTES.B06_SECTION]);
|
||||
|
||||
/**
|
||||
* 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂.
|
||||
* 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂(공용 `guardProjectLeave` 위 얇은 껍질).
|
||||
* `save` = [저장] 한 줄기(`saveSharedDesign(shared, false)`).
|
||||
*/
|
||||
export function guardDesignLeave(
|
||||
@@ -104,26 +100,11 @@ export function guardDesignLeave(
|
||||
root: HTMLElement,
|
||||
save: () => Promise<unknown>,
|
||||
): void {
|
||||
const unload = (event: BeforeUnloadEvent): void => {
|
||||
if (!root.isConnected || !designDirty(projectId)) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
};
|
||||
listenWhileAlive(window, "beforeunload", root, unload); // 다시 세운 옛 페이지를 안 붙듦(298-3)
|
||||
const guard = async (): Promise<boolean> => {
|
||||
if (!root.isConnected) return true;
|
||||
if (SAME_PAGE.has(currentRoute())) {
|
||||
// 라우터가 「넘어감」 뒤 물음을 내림 — 같은 페이지 안 이동이면 한 틱 뒤 다시 걺.
|
||||
window.setTimeout(() => root.isConnected && setLeaveGuard(guard), 0);
|
||||
return true;
|
||||
}
|
||||
if (!designDirty(projectId)) return true;
|
||||
const got = await leaveOrStay(L("B05_Design_Leave"), {
|
||||
save,
|
||||
stillDirty: () => designDirty(projectId),
|
||||
discard: () => discardDesignEdits(projectId),
|
||||
});
|
||||
return got !== "stay";
|
||||
};
|
||||
setLeaveGuard(guard);
|
||||
guardProjectLeave(root, {
|
||||
dirty: () => designDirty(projectId),
|
||||
save,
|
||||
discard: () => discardDesignEdits(projectId),
|
||||
message: L("B05_Design_Leave"),
|
||||
samePage: [ROUTES.B05_PROFILE, ROUTES.B06_SECTION],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
* 숨으면 3D 그리기 루프 · 포인터 듣기를 떼고, 보이면 가게 값으로 다시 그린다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { currentProjectId } from "../b_project_switch";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createWorkflowLayout, type WorkflowLayoutHandle } from "@ui/ui_template_workflow_layout";
|
||||
@@ -124,7 +125,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
const page = document.createElement("div");
|
||||
page.className = "design-page";
|
||||
root.replaceChildren(page);
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
const projectId = currentProjectId();
|
||||
pinDraftProject(projectId); // 연 프로젝트 고정(388-3) — 다른 탭이 바꿔도 이 탭 초안 칸 그대로
|
||||
void openCalcSites(projectId); // 계산 자리 판(504-2) — 열 때 한 번 · 열린 동안 바뀐 판은 다음 열기부터
|
||||
usePipeIds(projectId || null); // 관 세션 id 장부 = 이 프로젝트(323-5 · 카드 · 알약 · 종단 · 3D 같은 id)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
import { onStateRemoved, readState, stateKey, writeState } from "../b_page_state";
|
||||
import type { StateName } from "../b_page_state";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "../../config/config_frontend";
|
||||
import { currentProjectId } from "../b_project_switch";
|
||||
import type { StructureInstance } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { DetailPipeInput } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { PageSettingsBody } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
@@ -91,11 +91,7 @@ export function pinDraftProject(projectId: string | null): void {
|
||||
export function draftProjectId(projectId: string | null = null): string {
|
||||
if (projectId) return projectId;
|
||||
if (openedProjectId) return openedProjectId;
|
||||
try {
|
||||
return window.localStorage.getItem(CURRENT_PROJECT_ID_KEY) || "-";
|
||||
} catch {
|
||||
return "-";
|
||||
}
|
||||
return currentProjectId() || "-";
|
||||
}
|
||||
|
||||
interface Slot<T> {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import {
|
||||
API_BASE_URL,
|
||||
CURRENT_PROJECT_ID_KEY,
|
||||
PLAN_STEP_ROUTES,
|
||||
STAB_STEP_ROUTES,
|
||||
type RoutePath,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
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 {
|
||||
@@ -60,15 +60,18 @@ export function mountGroupStep(
|
||||
parts: {
|
||||
mainContent?: HTMLElement;
|
||||
leftPanel?: HTMLElement;
|
||||
/** 떠나기 전 — 저장 등 · false = 머묾 */
|
||||
/** 옛 떠남(저장 등 · 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)),
|
||||
@@ -114,7 +117,7 @@ export async function createGroupProject(
|
||||
const detail = typeof data.detail === "string" ? data.detail : `HTTP ${response.status}`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, data.project_id);
|
||||
setCurrentProject(data.project_id);
|
||||
showToast(t("DE_Reg_Success"), "success");
|
||||
navigateTo(firstStep);
|
||||
} catch (error) {
|
||||
|
||||
+10
-1
@@ -8,6 +8,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ROUTES, DEFAULT_ROUTE, PROTECTED_ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { linkRouter, reloadIfOtherProject } from "./b_project_switch";
|
||||
|
||||
/**
|
||||
* 여러 주소를 맡는 페이지 — 같은 페이지 안 주소 이동은 다시 만들지 않고 `show` 만 부른다
|
||||
@@ -103,6 +104,8 @@ export const setLeaveGuard = (guard: (() => Promise<boolean>) | null): void => {
|
||||
leaveGuard = guard;
|
||||
};
|
||||
export const takeLeaveGuard = (): (() => Promise<boolean>) | null => leaveGuard;
|
||||
// 공용 떠남 물음(401)이 이 자리를 씀 — 함수 선언은 끌어올려져 아래 currentRoute 도 바로 씀
|
||||
linkRouter({ currentRoute, setLeaveGuard });
|
||||
|
||||
/** 현재 프로젝트(기억한 id)로 여는 화면 — 그 프로젝트가 서버에 없으면 안내를 띄운다. */
|
||||
const PROJECT_ROUTES: readonly RoutePath[] = [
|
||||
@@ -116,6 +119,10 @@ const PROJECT_ROUTES: readonly RoutePath[] = [
|
||||
ROUTES.B11_LOADING,
|
||||
];
|
||||
|
||||
/** 프로젝트 화면(401) — B 단계 · 준비 화면 · 초안 길 `project: true`. */
|
||||
export const isProjectRoute = (route: string): boolean =>
|
||||
PROJECT_ROUTES.includes(route as RoutePath) || Boolean(draftRoute(route)?.project);
|
||||
|
||||
/** 지금 떠 있는 여러 주소 페이지 · 렌더 차례(늦게 끝난 옛 렌더를 버림). */
|
||||
let mounted: MountedPage | null = null;
|
||||
let renderSeq = 0;
|
||||
@@ -222,7 +229,9 @@ export async function renderCurrentRoute(outlet: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
// 기억한 현재 프로젝트가 지워졌으면 페이지 대신 안내 — 모든 B 단계 화면이 여기 한 곳을 지난다.
|
||||
if (PROJECT_ROUTES.includes(route) || draft?.project) {
|
||||
if (isProjectRoute(route)) {
|
||||
// 다른 프로젝트 화면이면 새로 엶 — 앞 프로젝트 모듈 메모리가 안 묻어감(401)
|
||||
if (reloadIfOtherProject()) return;
|
||||
const { dropGoneProject } = await import("./b_workflow_nav");
|
||||
if (await dropGoneProject()) {
|
||||
if (seq !== renderSeq) return;
|
||||
|
||||
Reference in New Issue
Block a user