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:
2026-10-28 18:07:58 +09:00
co-authored by Claude Opus 5.5
parent 92d952d8ac
commit 676b46eb9e
21 changed files with 154 additions and 47 deletions
@@ -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",
+10 -2
View File
@@ -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",
+3 -2
View File
@@ -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";