Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_Save.ts
T
eomsangdonandClaude Opus 5.5 659baa7751 feat(484 B07 설정): 좌측 ⚙ 설정 모달 · [현재 도면 확정] · [수정] 지움 · 저장 후 이동(481-3) · 탭 줄 겹판 자리
- 공용 createPanelCard 곁값 onSettings — 캐럿 왼쪽 ⚙ · 누름이 접힘 안 일으킴(B08 · B09 그대로)
- 평면도 ⚙ = 평면도 관리 모달([평면도 관리] 단추 지움) · 횡단도 ⚙ = 배치 칸 · 표 배율 · 안내선(카드 밑 칸 지움) · 구조물 ⚙ = 「M02 그림 — 축척 없음」 · 종단 1장 · 토적 = 단추 안 ⚙ · 표지 없음
- ⚙ 머리 [원본에서 다시 그리기] — 그 묶음 도면을 연 때만 · 저장본이면 invalidate
- 잠금 없음(요구 281) — 읽기 전용 안 묶음 · 자동 B08 이동 · 「모든 횡단 확정」 잠금 지움
- onStepClick 한 길 = 고친 탭(●) + 지금 탭 저장 → B08 · B09 면 단계 4 완료(새 POST design-drawings-complete) → 이동 · 실패 = 머묾
- 저장 · 원본 묶음 = 새 B07_DesignDetail_UI_Save.ts · 모달 = 새 B07_DesignDetail_UI_Settings.ts(UI_Page 633 → 543줄)
- 탭 줄 = .cad-host 의 --cad-tab-slot-left/right(px · CSS left · right 꼴) 있으면 리본 빈 자리에 겹침 · 없으면 지금 자리
- 시험: 새 test_280_5_b07_settings · 130 · 153-1 · 180-3 · 212-3 · 269 · 311 고쳐 씀

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

148 lines
5.9 KiB
TypeScript

/* =============================================================================
* B07_DesignDetail_UI_Save.ts
* 상세 설계 저장 · 원본 되돌림 묶음(요구 280-5 · 280-6 · 281) — `_UI_Page.ts` 가 700줄에 닿아 뗌.
*
* 저장 = 페이지를 옮길 때만([<] [>] · ☰ · 진행 단계 판 — `onStepClick` 한 길).
* 고친 탭(●) + 지금 탭을 `confirm` 길로 · 안 고친 도면은 보내지 않음(늘 원본에서 새로 그림).
* 잠금 없음 — 저장본도 그대로 고침 · 다시 옮길 때 다시 저장.
* 원본 되돌림 = ⚙ 모달 [원본에서 다시 그리기] — 저장본이면 `invalidate` · 고친 것 버리고 다시 실음.
* ========================================================================== */
import {
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import type { CadSaveResult } from "@ui/cad_host/cad_host";
import type { CadTabs } from "@ui/cad_host/cad_host_tabs";
import {
confirmDesignDrawing,
invalidateDesignDrawing,
type CadDrawing,
type DesignDrawingItem,
type QuantityTable,
} from "./B07_DesignDetail_Api_Fetch";
/** 화면 조립이 쥔 상태를 읽고 쓰는 길 — 이 묶음은 상태를 갖지 않음. */
export interface DrawingStore {
projectId: string;
drawings: DesignDrawingItem[];
tabs: CadTabs<CadDrawing>;
current: () => DesignDrawingItem | undefined;
/** CAD 에 저장 안 한 편집이 있나 · 지움 */
cadDirty: () => boolean;
setCadClean: () => void;
requestCad: () => Promise<CadSaveResult<CadDrawing, QuantityTable>>;
isFrameEditing: () => boolean;
/** 도면 캐시에서 뺌 — 다음 열기에 서버에서 다시 받음 */
forget: (id: string) => void;
/** 그 도면을 서버에서 다시 받아 실음 */
reload: (drawing: DesignDrawingItem) => Promise<void>;
}
/** 좌측 단추 저장본 띠(초록) — 저장본 = 서버 `confirmed`. */
function markButton(id: string, saved: boolean): void {
document
.querySelectorAll<HTMLElement>(`.b07-drawing-button[data-drawing-id="${id}"]`)
.forEach((button) => (button.dataset.confirmed = String(saved)));
}
/**
* 고친 도면을 저장 — 지금 탭(CAD 에서 받음 · 횡단은 수량표 같이) + 다른 고친 탭(받아 둔 편집본).
* 고친 것 없으면 요청 없이 참 · 하나라도 실패하면 알림 뒤 거짓(부르는 쪽은 머묾).
*/
export async function saveEditedDrawings(store: DrawingStore): Promise<boolean> {
if (store.isFrameEditing()) {
showToast("도각 편집을 마친 뒤 옮기세요.", "warning");
return false;
}
const current = store.current();
const active = store.tabs.active();
const editable = (item: DesignDrawingItem | undefined): item is DesignDrawingItem =>
!!item && item.kind !== "blank";
const currentEdited = editable(current) && (store.cadDirty() || !!active?.dirty);
const others = store.tabs
.tabs()
.filter((tab) => tab !== active && tab.dirty && tab.drawing)
.map((tab) => ({ tab, item: store.drawings.find((item) => item.id === tab.id) }))
.filter(({ item }) => editable(item));
if (!currentEdited && others.length === 0) return true;
const save = async (
item: DesignDrawingItem,
drawing: CadDrawing,
table: QuantityTable | null,
) => {
await confirmDesignDrawing(store.projectId, item.id, drawing, table);
item.confirmed = true;
markButton(item.id, true);
store.forget(item.id); // 저장본이 정본 — 다음 열기는 서버에서
};
showLoadingOverlay();
try {
if (currentEdited && current) {
const saved = await store.requestCad();
await save(
current,
saved.drawing,
current.kind === "cross" ? (saved.quantityTable ?? null) : null,
);
store.setCadClean();
if (active) store.tabs.markClean(active);
}
for (const { tab, item } of others) {
// 횡단 수량표는 서버가 그림의 표 글에서 다시 뽑음(받아 둔 편집본엔 수량표 칸이 없음)
await save(item as DesignDrawingItem, tab.drawing as CadDrawing, null);
store.tabs.markClean(tab);
}
return true;
} catch (error) {
showToast(
`${error instanceof Error ? error.message : "도면을 저장하지 못했습니다."} — 이 화면에 머묾`,
"error",
);
return false;
} finally {
hideLoadingOverlay();
}
}
/** 지금 도면에 버릴 것(저장본 · 고친 것)이 있나 — ⚙ [원본에서 다시 그리기] 켬. */
export function hasRedrawable(store: DrawingStore): boolean {
const current = store.current();
if (!current || current.kind === "blank") return false;
return current.confirmed || store.cadDirty() || !!store.tabs.active()?.dirty;
}
/** ⚙ [원본에서 다시 그리기] — 물은 뒤 저장본 풂(`invalidate`) · 고친 것 버림 · 다시 실음. 해냈으면 참. */
export async function redrawFromSource(store: DrawingStore): Promise<boolean> {
const current = store.current();
if (!current || !hasRedrawable(store)) return false;
const ok = await showConfirmDialog(
`「${current.label}」 의 저장본 · 고친 것을 버리고 원본에서 다시 그릴까요?`,
"다시 그리기",
);
if (!ok) return false;
showLoadingOverlay();
try {
if (current.confirmed) {
await invalidateDesignDrawing(store.projectId, current.id);
current.confirmed = false;
markButton(current.id, false);
}
store.forget(current.id);
store.setCadClean();
const active = store.tabs.active();
if (active) store.tabs.markClean(active);
await store.reload(current);
showToast("원본에서 다시 그렸습니다.", "success");
return true;
} catch (error) {
showToast(error instanceof Error ? error.message : "원본에서 다시 그리지 못했습니다.", "error");
return false;
} finally {
hideLoadingOverlay();
}
}