feat(B 페이지): 떠남 물음 가드 · 지금 프로젝트 = 탭마다 (PLAN 572-2 · 요구 400~403)
- B04 · B07 · B08 · B09 가 공용 guardProjectLeave 에 {dirty, save, discard} 등록 — 고침 있을 때만 [취소 · 폐기 · 저장]
- B04 = 관 지점 고침(drainageDirty) · 저장 = 관 지점 · 세부유역 영구저장
- B07 = CAD · 탭 고침 · 저장 = saveEditedDrawings · 폐기 = 고친 도면 CAD 자동백업도 버림(discardEditedRecovery)
- B08 = 세션 b08-* 칸 넷 · 저장 = saveShown · B09 = b09-estimation 칸 · 저장 = saveChanged
- 지금 프로젝트 직접 읽기(localStorage) → currentProjectId() · B02 새 프로젝트 = setCurrentProject — B02 · B03 · B04 · B07 · B08 · B09 · B11
- 다른 프로젝트 보관함 지우기 셋(B04 · B05 종단 · B11) 뺌 — enterProject 한 곳 · 다른 탭 보관분 안 지움
- B02 · B03 은 가드 없음 — 저장 안 한 프로젝트 값 없음(B03 올리기 끊김은 이어 올리기 배너가 받음)
- 시험 test_560_4 넓힘(B 가드 · 탭 프로젝트 · 보관함) · helper_298_2 · helper_481_2 · test_204_2 흉내를 currentProjectId 로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { purgeOtherProjects } from "../b_asset_cache";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { fetchConfirmedSurface } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
@@ -96,8 +95,6 @@ export async function mountProfileView(
|
||||
): Promise<void> {
|
||||
const activeProjectId = shared.projectId;
|
||||
if (!activeProjectId) return;
|
||||
// 새로고침으로 바로 들어온 경우에도 다른 프로젝트 자료는 보관함에서 지운다.
|
||||
void purgeOtherProjects(activeProjectId);
|
||||
|
||||
const viewer = createRouteViewer();
|
||||
// 구 우클릭 메뉴의 자유 텍스트 구조물 → 레지스트리 타입(2026-08-17 이관 매핑과 동일).
|
||||
|
||||
@@ -25,8 +25,9 @@ import { isBlank } from "@util/common_util_validate";
|
||||
import { fetchCompanyMembers, fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { createNameComposer, type NameParts } from "./B02_ProjRegister_UI_Name";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { API_BASE_URL, ROUTES } from "@config/config_frontend";
|
||||
import "./B02_ProjRegister_UI_Style.css";
|
||||
import { setCurrentProject } from "../A00_Common/b_project_switch";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
@@ -267,7 +268,7 @@ export function renderB02ProjRegister(root: HTMLElement): void {
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { project_id: string };
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, data.project_id);
|
||||
setCurrentProject(data.project_id);
|
||||
showToast(L("B02_Proj_Success"), "success");
|
||||
navigateTo(ROUTES.B03_FILE_INPUT);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
CURRENT_PROJECT_ID_KEY,
|
||||
ROUTES,
|
||||
UPLOAD_ALLOWED_EXT,
|
||||
UPLOAD_MAX_FILES,
|
||||
} from "@config/config_frontend";
|
||||
import { ROUTES, UPLOAD_ALLOWED_EXT, UPLOAD_MAX_FILES } from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createDrop } from "@ui/ui_template_drop";
|
||||
@@ -56,6 +51,7 @@ import {
|
||||
} from "./B03_FileInput_UI_Support";
|
||||
import "./B03_FileInput_UI_Style.css";
|
||||
import "./B03_FileInput_UI_Style_Card.css";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -72,7 +68,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const progressMap = new Map<FileSlot, ProgressBarHandle>();
|
||||
let uploadButton: HTMLButtonElement;
|
||||
let resumeBanner: HTMLDivElement;
|
||||
let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
let activeProjectId = currentProjectId();
|
||||
// 업로드가 도는 동안은 화면 전체를 잠근다 — 같은 파일을 두 번 올리거나 도중에 다른
|
||||
// 단계로 넘어가는 것을 막는다(2026-08-08 사용자 지시).
|
||||
let pageRoot: HTMLElement | null = null;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 화면이 들고 있는 상태는 `ctx` 로 받아 쓰기만 한다 — 동작·순서는 종전과 같다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { CURRENT_PROJECT_ID_KEY, type RoutePath } from "@config/config_frontend";
|
||||
import { type RoutePath } from "@config/config_frontend";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type FileSlotState,
|
||||
type StoredUploadSession,
|
||||
} from "./B03_FileInput_UI_Support";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -70,7 +71,7 @@ function clearDerivedCaches(projectId: string): void {
|
||||
|
||||
export function createUploadFlow(ctx: UploadFlowContext): UploadFlowHandle {
|
||||
async function detectPausedUploads(): Promise<void> {
|
||||
ctx.setProjectId(localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "");
|
||||
ctx.setProjectId(currentProjectId());
|
||||
ctx.resumeBanner.replaceChildren();
|
||||
ctx.resumeBanner.classList.remove("is-visible");
|
||||
if (!ctx.projectId()) return;
|
||||
@@ -128,7 +129,7 @@ export function createUploadFlow(ctx: UploadFlowContext): UploadFlowHandle {
|
||||
async function attachSelectedTempBatch(): Promise<void> {
|
||||
const batch = ctx.tempPicker.selected();
|
||||
if (!batch) return;
|
||||
ctx.setProjectId(localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "");
|
||||
ctx.setProjectId(currentProjectId());
|
||||
if (!ctx.projectId()) {
|
||||
ctx.pageError.textContent = L("B03_File_Error_Project");
|
||||
return;
|
||||
@@ -193,7 +194,7 @@ export function createUploadFlow(ctx: UploadFlowContext): UploadFlowHandle {
|
||||
}
|
||||
return;
|
||||
}
|
||||
ctx.setProjectId(localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "");
|
||||
ctx.setProjectId(currentProjectId());
|
||||
const validation = validateSlots(
|
||||
ctx.slots,
|
||||
ctx.selectedStates(),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 옛 결과가 지워지면 서버가 409 confirm 으로 묻고 → 확인 창 → confirm_replace 로 다시.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import {
|
||||
createButton,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { createListRow } from "@ui/ui_template_list_row";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
interface AssignedRoute {
|
||||
assignee_id: number;
|
||||
@@ -100,7 +101,7 @@ export function mountPlanImport(
|
||||
bannerHost: HTMLElement,
|
||||
onImported: () => void,
|
||||
): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
const projectId = currentProjectId();
|
||||
if (!projectId) return;
|
||||
const lasFree = (): boolean => localStorage.getItem(`b03_las_free_${projectId}`) === "1";
|
||||
const banner = el("div", { className: "b03-file__plan-update" });
|
||||
|
||||
@@ -150,6 +150,7 @@ export interface DetailBasinOverlay {
|
||||
handleClick: (view: ViewState, x: number, y: number) => boolean;
|
||||
/** 모델 확정 시점에 관 지점과 세부유역을 영구저장한다. */
|
||||
commit: () => Promise<number>;
|
||||
isDirty: () => boolean;
|
||||
draw: (context: CanvasRenderingContext2D, normalizer: Normalizer | null, view: ViewState) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
@@ -474,6 +475,7 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
|
||||
]);
|
||||
return true;
|
||||
},
|
||||
isDirty: () => dirty,
|
||||
async commit() {
|
||||
if (!projectId) return 0;
|
||||
const response = await saveDetailPipePoints(
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface SurfaceMapViewer {
|
||||
render: (projectId: string, routeBounds?: PlanBounds | null) => void;
|
||||
/** 관 매설 지점·세부유역을 영구저장한다. 저장한 관 개수를 돌려준다(모델 확정과 함께 호출). */
|
||||
commitDrainage: () => Promise<number>;
|
||||
/** 관 지점을 고치고 아직 저장 안 했나(떠날 때 물음 · 572-2). */
|
||||
drainageDirty: () => boolean;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
@@ -675,6 +677,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
},
|
||||
/** 모델 확정과 함께 관 매설 지점·세부유역을 영구저장한다. */
|
||||
commitDrainage: () => detailBasins.commit(),
|
||||
drainageDirty: () => detailBasins.isDirty(),
|
||||
dispose() {
|
||||
loadSequence += 1;
|
||||
if (frameHandle) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
@@ -15,7 +15,7 @@ import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { clearPreloadMark, purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||
import { clearPreloadMark } from "../A00_Common/b_asset_cache";
|
||||
import { clearRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ import { createSurfacePointCloudViewer } from "./B04_PreProcess_UI_Viewer";
|
||||
import "./B04_PreProcess_UI_Style.css";
|
||||
// 700줄 제한으로 잘라낸 조각 — 본체 **다음에** 불러야 캐스케이드 순서가 같다(2026-09-04).
|
||||
import "./B04_PreProcess_UI_Style_Map.css";
|
||||
import { currentProjectId, guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
|
||||
// 고를 수 있는 필터. 자동 전처리는 이 중 기본 하나만 만들고, 나머지는 관리자가
|
||||
// 드롭다운에서 고를 때 그 조합만 계산한다 (2026-09-01 사용자 확정).
|
||||
@@ -93,9 +94,7 @@ function getModelFilter(model: SurfaceModelSummary): string {
|
||||
}
|
||||
|
||||
export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
const guardedProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
// 새로고침으로 바로 들어온 경우에도 다른 프로젝트 자료는 보관함에서 지운다.
|
||||
if (guardedProjectId) void purgeOtherProjects(guardedProjectId);
|
||||
const guardedProjectId = currentProjectId();
|
||||
if (guardedProjectId) {
|
||||
const user = await fetchDashboardMe();
|
||||
if (user.role !== "SYSTEM_ADMIN") {
|
||||
@@ -293,7 +292,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
workspace.append(statusBox, viewers, sheetSection, mapViewer.root);
|
||||
|
||||
let workflowState: WorkflowState | undefined;
|
||||
const layoutProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const layoutProjectId = currentProjectId();
|
||||
if (layoutProjectId) {
|
||||
try {
|
||||
workflowState = await fetchWorkflowState(layoutProjectId);
|
||||
@@ -317,7 +316,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
|
||||
function getProjectId(): string | null {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
if (!projectId) showToast(L("B04_Surface_Error_Project"), "error");
|
||||
return projectId;
|
||||
}
|
||||
@@ -670,6 +669,11 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
|
||||
root.replaceChildren(layout.root);
|
||||
// 572-2 떠남 물음 — 고침 = 관 지점 · 저장 = 관 지점 · 세부유역 영구저장(모델 [확정]과 같은 길)
|
||||
guardProjectLeave(layout.root, {
|
||||
dirty: mapViewer.drainageDirty,
|
||||
save: mapViewer.commitDrainage,
|
||||
});
|
||||
const projectId = getProjectId();
|
||||
if (projectId) void onB04_Surface_Reset_Click();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
// 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||
import {
|
||||
buildDesignInfoPanel,
|
||||
@@ -70,12 +69,14 @@ import {
|
||||
resetDrawings,
|
||||
saveDrawingsNow,
|
||||
saveEditedDrawings,
|
||||
discardEditedRecovery,
|
||||
} from "./B07_DesignDetail_UI_Save";
|
||||
import { openDrawingSettings } from "./B07_DesignDetail_UI_Settings";
|
||||
// CAD iframe 부모 흐름 · 도각 편집은 M02 와 한 벌로 공용에 둔다(PLAN 10-3).
|
||||
import { createCadHost, type CadSaveResult } from "@ui/cad_host/cad_host";
|
||||
import { createFrameTemplateEditor } from "@ui/cad_host/cad_host_frame_edit";
|
||||
import { createCadTabs, type CadTab } from "@ui/cad_host/cad_host_tabs";
|
||||
import { currentProjectId, guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
|
||||
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
|
||||
interface DesignMeta {
|
||||
@@ -106,7 +107,7 @@ let reopenDrawingId: string | undefined;
|
||||
let reopenCrossSheet: number | undefined;
|
||||
|
||||
export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
// 311 다시 세우면 옛 목록 접힘 그대로 — 기다리기 전에 읽음(처음 들어오면 B07 목록이 없어 기본값)
|
||||
const folded = foldedGroups(root);
|
||||
let workflowState: WorkflowState | undefined;
|
||||
@@ -516,6 +517,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
layout.root.classList.add("b07-design-layout");
|
||||
root.replaceChildren(layout.root);
|
||||
// 572-2 떠남 물음 — 고침 = CAD · 탭 · 저장 = 단계 이동과 같은 길 · 폐기 = 고친 도면 CAD 자동백업도
|
||||
if (projectId)
|
||||
guardProjectLeave(layout.root, {
|
||||
dirty: () => cadDirty || tabs.anyDirty(),
|
||||
save: () => saveEditedDrawings(store),
|
||||
discard: () => discardEditedRecovery(store),
|
||||
});
|
||||
|
||||
// 페이지 진입 시 첫 도면(표지 · 311)을 자동 선택 — 빈 CAD 화면 방지.
|
||||
// CAD가 아직 준비 전이면 cad.load 가 붙들고 있다가 ready 때 보낸다.
|
||||
|
||||
@@ -222,3 +222,16 @@ export async function resetDrawings(store: DrawingStore): Promise<void> {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/** 떠남 [폐기](572-2) — 고친 도면(지금 CAD · 고친 탭)의 CAD 자동백업만 버림 · 다음에 들어와도 안 살아남. */
|
||||
export function discardEditedRecovery(store: DrawingStore): void {
|
||||
const ids = store.tabs
|
||||
.tabs()
|
||||
.filter((tab) => tab.dirty)
|
||||
.map((tab) => tab.id);
|
||||
const current = store.current();
|
||||
if (current && store.cadDirty()) ids.push(current.id);
|
||||
ids.forEach((id) =>
|
||||
localStorage.removeItem(`${CAD_RECOVERY_KEY}__${recoveryScopeOf(store.projectId, id)}`),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import "./B08_Quantity_UI_Page.css";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { moveStage, WORKFLOW_STEP_ROUTES, type StageStep } from "../A00_Common/b_workflow_nav";
|
||||
@@ -26,6 +25,8 @@ import { renderHydro, request } from "./B08_Quantity_UI_Hydro";
|
||||
import { renderEarthwork } from "./B08_Quantity_UI_Earthwork";
|
||||
import { createOptionsPanel } from "./B08_Quantity_UI_Options";
|
||||
import { renderMilestone } from "./B08_Quantity_UI_Milestone";
|
||||
import { currentProjectId, guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
import { clearState, readState } from "../A00_Common/b_page_state";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -112,7 +113,7 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null, onSa
|
||||
}
|
||||
|
||||
export function renderB08Quantity(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "b08-quantity__tabs";
|
||||
@@ -170,4 +171,12 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
},
|
||||
});
|
||||
root.append(layout.root);
|
||||
// 572-2 떠남 물음 — 고침 = 세션 b08-* 칸 · 저장 = 단계 이동과 같은 길
|
||||
const drafts = ["b08-options", "b08-materials", "b08-hydro", "b08-milestone"] as const;
|
||||
if (projectId)
|
||||
guardProjectLeave(layout.root, {
|
||||
dirty: () => drafts.some((name) => readState(name, projectId) !== null),
|
||||
save: saveShown,
|
||||
discard: () => drafts.forEach((name) => clearState(name, projectId)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* 연결 없음 = 단가연결 목록 → 옵션 값 창 옛 길).
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import {
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
noteBox,
|
||||
type StepBlock,
|
||||
} from "./B09_Estimation_UI_Haul_Steps";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
export interface HaulModalOpts extends PriceLinkOpts {
|
||||
/** 지금 연결(없으면 null) */
|
||||
@@ -70,7 +71,7 @@ interface Preview {
|
||||
}
|
||||
|
||||
async function preview(code: string, given: Row, rate: unknown, picks: unknown): Promise<Preview> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
if (!projectId) throw new Error("프로젝트 없음");
|
||||
const res = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/logic-preview`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./B09_Estimation_UI_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
renderGroupPanel,
|
||||
unpickOnRelease,
|
||||
} from "./B09_Estimation_UI_Tabs_Groups";
|
||||
import { currentProjectId, guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
|
||||
// 252 초안 탭(공기 등) — draft 폴더가 있으면 등록 · 지우면 빈 목록(오류 없음)
|
||||
import.meta.glob("./draft/*_UI.ts", { eager: true });
|
||||
@@ -121,7 +122,7 @@ export function computeSheets(payload: Row, user: Row): Row {
|
||||
}
|
||||
|
||||
export function renderB09Estimation(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
let payload: Row | null = null;
|
||||
let user: Row = {};
|
||||
let sheets: Row = {};
|
||||
@@ -378,6 +379,13 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
});
|
||||
root.append(layout.root);
|
||||
show(current);
|
||||
// 572-2 떠남 물음 — 고침 = 세션 칸(불러온 뒤) · 저장 = 단계 이동과 같은 길
|
||||
if (projectId)
|
||||
guardProjectLeave(layout.root, {
|
||||
dirty: () => !!payload && readState("b09-estimation", projectId) !== null,
|
||||
save: saveChanged,
|
||||
discard: () => clearState("b09-estimation", projectId),
|
||||
});
|
||||
|
||||
if (projectId)
|
||||
request<Row>(`/projects/${projectId}/estimation`).then(
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* 길 = `B09_Estimation_Router_Quotes.py` · 바뀐 목록은 onChange(이름들)로 표 칸 「n 파일」에 반영
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
interface QuoteFile {
|
||||
name: string;
|
||||
@@ -17,7 +18,7 @@ interface QuoteFile {
|
||||
}
|
||||
|
||||
const url = (key: string, name?: string): string => {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
const projectId = currentProjectId();
|
||||
const q = new URLSearchParams({ key, ...(name ? { name } : {}) });
|
||||
return `${API_BASE_URL}/projects/${projectId}/estimation/quotes${name ? "/file" : ""}?${q}`;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 층 읽기 권한 = 서버(`M02_Layer_Access`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { loadRows } from "@ui/ui_template_item_picker";
|
||||
import { itemName, type PickerLayer } from "@ui/ui_template_item_picker_data";
|
||||
@@ -24,6 +24,7 @@ import type { Dist } from "../B08_Quantity/B08_Quantity_Haul_Cycle";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { openHaulModal } from "./B09_Estimation_UI_Haul_Modal";
|
||||
import type { TabContext } from "./B09_Estimation_UI_Tabs";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
/** 연결 적용 — ref = null 이면 풀기 · from = 고른 호표(명칭 · 규격 · 단위 채움) */
|
||||
export type ApplyRef = (ref: Row | null, from?: Row) => void;
|
||||
@@ -89,7 +90,7 @@ export function parseLib(lib: string): LibPick | null {
|
||||
}
|
||||
|
||||
async function postLib(path: string, body: Row): Promise<Row> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
if (!projectId) throw new Error("프로젝트 없음");
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/${path}`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "../B09_Estimation_UI_Page.css"; // 좌측 하단 단추 줄(b09-dock) 규칙 한 벌
|
||||
import "./B09_Draft_Print_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { API_BASE_URL, ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
FetchDrawingsPdf,
|
||||
PrintSheet,
|
||||
} from "./B09_Draft_Print_Types";
|
||||
import { currentProjectId } from "../../A00_Common/b_project_switch";
|
||||
|
||||
type AreaId = "drawings" | "quantity" | "cost";
|
||||
|
||||
@@ -116,7 +117,7 @@ function save(blob: Blob, name: string): void {
|
||||
}
|
||||
|
||||
export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
|
||||
const status = createText("", "muted");
|
||||
status.className += " b09-print__status";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
markProjectPreloaded,
|
||||
preloadSurfaceAssets,
|
||||
PRELOAD_NO_SURFACE,
|
||||
purgeOtherProjects,
|
||||
readPreloadTarget,
|
||||
} from "../A00_Common/b_asset_cache";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { fetchSurfaceStatus } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import "./B11_Status_UI_Style.css";
|
||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||
|
||||
/* =============================================================================
|
||||
* 자료 준비 화면 (B11)
|
||||
@@ -51,7 +51,7 @@ async function waitWhileDesigning(
|
||||
}
|
||||
|
||||
export async function renderB11Loading(root: HTMLElement): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const projectId = currentProjectId();
|
||||
const target = (readPreloadTarget() ?? ROUTES.B03_FILE_INPUT) as RoutePath;
|
||||
|
||||
const progress = createProgressCircle({ label: t("B11_Loading_Start"), size: 96 });
|
||||
@@ -83,8 +83,6 @@ export async function renderB11Loading(root: HTMLElement): Promise<void> {
|
||||
// 초기 설계 체인이 도는 동안은 여기서 기다린다(2026-08-29 사용자 확정) — 자료를 담아도
|
||||
// 계산이 안 끝난 화면으로 보내면 반쯤 된 값을 만지게 된다.
|
||||
await waitWhileDesigning(projectId, (label) => progress.set(0, label));
|
||||
// 다른 프로젝트 자료가 남아 있으면 지운다 — 프로젝트끼리 섞이면 안 된다.
|
||||
await purgeOtherProjects(projectId);
|
||||
const signature = await preloadSurfaceAssets(projectId, (label, ratio) => {
|
||||
progress.set(ratio, label);
|
||||
});
|
||||
|
||||
@@ -140,6 +140,8 @@ const common = {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4
|
||||
"../A00_Common/b_project_switch": { currentProjectId: () => "P1", guardProjectLeave() {} },
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name) => store.get(name) ?? null,
|
||||
writeState: (name, v) => store.set(name, v),
|
||||
|
||||
@@ -128,6 +128,8 @@ const common = {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4
|
||||
"../A00_Common/b_project_switch": { currentProjectId: () => "P1", guardProjectLeave() {} },
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name) => store.get(name) ?? null,
|
||||
writeState: (name, v) => store.set(name, v),
|
||||
|
||||
@@ -186,8 +186,7 @@ const toasts = [];
|
||||
const sent = [];
|
||||
globalThis.showToast = (m, k) => toasts.push([k, m]);
|
||||
globalThis.API_BASE_URL = "/api";
|
||||
globalThis.CURRENT_PROJECT_ID_KEY = "pid";
|
||||
globalThis.localStorage = { getItem: () => "P1" };
|
||||
globalThis.currentProjectId = () => "P1";
|
||||
let picker = null; // 251-6 단가연결 = 한 창(openLogicPicker · 사전조합 + 덧붙인 줄 구조물 조합)
|
||||
globalThis.openLogicPicker = (done, lib) => { picker = lib;
|
||||
sent.push(["pre", lib.layer, lib.kindFilter.map((k) => k.value), lib.title]); };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""560-4 D 단계 이동 = 저장 뒤 이동 지킴 시험 — 서버 · 화면 없이 소스만 본다.
|
||||
"""560-4 D 단계 이동 = 저장 뒤 이동 · 572-2 B 떠남 물음 지킴 시험 — 서버 · 화면 없이 소스만 본다.
|
||||
단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로가 한 함수(beforeLeave)를 거치고 · D02 첫 불러오기 실패에도 지도를 세움."""
|
||||
|
||||
import re
|
||||
@@ -25,3 +25,62 @@ def test_d02_leave_saves_and_builds_map_on_fail():
|
||||
# 불러오기 catch 뒤에 paint() — 실패해도 지도 · 표를 세움
|
||||
assert re.search(r"\} catch \(error\) \{[^}]*\}\s*active = firstEmpty\(\);\s*paint\(\);", tail)
|
||||
assert "sameBase" in ts # 옛 캐시는 그때 서버 값이 지금과 같을 때만
|
||||
|
||||
|
||||
# 572-2 B 페이지 떠남 물음 — 공용 가드에 {dirty, save} · 지금 프로젝트 = 탭마다(currentProjectId)
|
||||
B_GUARDS = {
|
||||
"B04_PreProcess/B04_PreProcess_UI_Page.ts": (
|
||||
"mapViewer.drainageDirty",
|
||||
"mapViewer.commitDrainage",
|
||||
),
|
||||
"B07_DesignDetail/B07_DesignDetail_UI_Page.ts": (
|
||||
"cadDirty || tabs.anyDirty()",
|
||||
"saveEditedDrawings(store)",
|
||||
),
|
||||
"B08_Quantity/B08_Quantity_UI_Page.ts": ("readState(name, projectId)", "save: saveShown"),
|
||||
"B09_Estimation/B09_Estimation_UI_Shell.ts": (
|
||||
'readState("b09-estimation", projectId)',
|
||||
"save: saveChanged",
|
||||
),
|
||||
}
|
||||
|
||||
B_PROJECT_READS = [
|
||||
"B02_ProjRegister/B02_ProjRegister_UI_Page.ts",
|
||||
"B03_FileInput/B03_FileInput_UI_Page.ts",
|
||||
"B03_FileInput/B03_FileInput_UI_Page_Flow.ts",
|
||||
"B03_FileInput/B03_FileInput_UI_PlanImport.ts",
|
||||
*B_GUARDS,
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts",
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Base_Quote.ts",
|
||||
"B09_Estimation/B09_Estimation_UI_Haul_Modal.ts",
|
||||
"B09_Estimation/draft/B09_Draft_Print_Page.ts",
|
||||
"B11_Status/B11_Status_UI_Loading.ts",
|
||||
]
|
||||
|
||||
|
||||
def test_b_pages_guard_leave():
|
||||
for path, (dirty, save) in B_GUARDS.items():
|
||||
ts = _src(path)
|
||||
at = ts.index("guardProjectLeave(layout.root, {")
|
||||
block = ts[at : ts.index("});", at)]
|
||||
assert dirty in block and save in block, path
|
||||
b07 = _src("B07_DesignDetail/B07_DesignDetail_UI_Page.ts")
|
||||
assert "discard: () => discardEditedRecovery(store)" in b07 # 폐기 = CAD 자동백업도
|
||||
|
||||
|
||||
def test_b_pages_read_tab_project():
|
||||
for path in B_PROJECT_READS:
|
||||
ts = _src(path)
|
||||
# 탭 공용 localStorage 직접 읽기 · 쓰기 없음
|
||||
assert "CURRENT_PROJECT_ID_KEY" not in ts, path
|
||||
assert "currentProjectId()" in ts or "setCurrentProject(" in ts, path
|
||||
|
||||
|
||||
def test_b_pages_leave_other_project_cache_to_enter():
|
||||
# 다른 프로젝트 보관함 지우기 = enterProject 한 곳(다른 탭 보관분을 지우지 않게)
|
||||
for path in (
|
||||
"B04_PreProcess/B04_PreProcess_UI_Page.ts",
|
||||
"A00_Common/design/design_view_profile.ts",
|
||||
"B11_Status/B11_Status_UI_Loading.ts",
|
||||
):
|
||||
assert "purgeOtherProjects" not in _src(path), path
|
||||
|
||||
Reference in New Issue
Block a user