- 1-A 도면 GET · 저장 PUT 한도 60 s · 저장 중 미리받기 쉼 · 끊긴 저장 = 목록 confirmed 로 판정
- 2 CAD 자동백업 열쇠 = b07:{프로젝트}:{도면} · 도각 편집 = b07-frame:{프로젝트} · [초기화] 같은 열쇠
- 3-1 [초기화] 물음 글 = 프로젝트 도각 그대로 · [도각 편집] → [기본 도각 복원] 안내
- 3-2 풀린 장 저장본 json · 수량표 찌꺼기 걷음(_write_manifest 한 곳)
- 5-1 확정 풀기 · 배치 · 축척 = mark_stage(save)
- 7-3 start_stage 가 DEV_UNLOCK 표식 남김
- 6-a navigateDrawing · onNavigate · hasPrev · hasNext 걷음 · 6-b cad-host-save 토글 걷음 · 4-2 B08 안 쓰는 글 11 걷음
- 6-f 특성 판 색 끌기 = 되돌리기 한 칸(paintNowCommitLater 한 벌) · 6-g LAYMRG 사본 길
- 4-1 B09 [인쇄 미리보기] = 저장 뒤 이동 · 단계 그대로 · 6-e M02 카드 도움말 DWG 한 줄
- 시험 test_506_3_* 둘 · cad/undo-506-3 · test_298_2_b07_buttons 열쇠 맞춤
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
225 lines
9.1 KiB
TypeScript
225 lines
9.1 KiB
TypeScript
/* =============================================================================
|
|
* B07_DesignDetail_UI_Save.ts
|
|
* 상세 설계 저장 · 원본 되돌림 묶음(요구 280-5 · 280-6 · 281) — `_UI_Page.ts` 가 700줄에 닿아 뗌.
|
|
*
|
|
* 저장 = 페이지를 옮길 때만([<] [>] · ☰ · 진행 단계 판 — `onStepClick` 한 길).
|
|
* 고친 탭(●) + 지금 탭을 `confirm` 길로 · 안 고친 도면은 보내지 않음(늘 원본에서 새로 그림).
|
|
* 잠금 없음 — 저장본도 그대로 고침 · 다시 옮길 때 다시 저장.
|
|
* 원본 되돌림 = ⚙ 모달 [원본에서 다시 그리기] — 저장본이면 `invalidate` · 고친 것 버리고 다시 실음.
|
|
* 좌측 [저장](298-2) = 이동 없이 같은 저장 · [초기화] = 물음 → `reset`(저장본 풂 · 설정 지움) →
|
|
* CAD 자동백업 지움 → 페이지 다시 읽음(기준 = 지금 앞 단계로 그린 원본).
|
|
* ========================================================================== */
|
|
|
|
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,
|
|
fetchDesignDrawingList,
|
|
invalidateDesignDrawing,
|
|
resetDesignDrawings,
|
|
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>;
|
|
}
|
|
|
|
/** 저장 PUT 이 도는 중인가 — 미리받기(`_UI_Page`)가 이 동안 쉼. */
|
|
let saving = false;
|
|
export const isSaving = (): boolean => saving;
|
|
|
|
/** CAD 자동백업 칸 이름 — 프로젝트 · 도면마다(같은 도면 id 의 남의 프로젝트 백업과 안 겹침). */
|
|
export const recoveryScopeOf = (projectId: string, drawingId: string): string =>
|
|
`b07:${projectId}:${drawingId}`;
|
|
|
|
/** 끊긴(abort) 저장 — 서버는 끝까지 저장하므로 목록의 `confirmed` 로 판정 · 못 읽으면 거짓. */
|
|
async function savedOnServer(projectId: string, id: string): Promise<boolean> {
|
|
try {
|
|
const list = await fetchDesignDrawingList(projectId);
|
|
return list.drawings.some((item) => item.id === id && item.confirmed);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** 좌측 단추 저장본 띠(초록) — 저장본 = 서버 `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,
|
|
) => {
|
|
try {
|
|
await confirmDesignDrawing(store.projectId, item.id, drawing, table);
|
|
} catch (error) {
|
|
const aborted = error instanceof DOMException && error.name === "AbortError";
|
|
if (!aborted || !(await savedOnServer(store.projectId, item.id))) throw error;
|
|
}
|
|
item.confirmed = true;
|
|
markButton(item.id, true);
|
|
store.forget(item.id); // 저장본이 정본 — 다음 열기는 서버에서
|
|
};
|
|
showLoadingOverlay();
|
|
saving = true;
|
|
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 {
|
|
saving = false;
|
|
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();
|
|
}
|
|
}
|
|
|
|
/** 좌측 [저장](298-2) — 이동 없이 고친 탭 저장 · 저장했으면 `onSaved`(단계 색 다시 읽음). */
|
|
export async function saveDrawingsNow(store: DrawingStore, onSaved: () => void): Promise<void> {
|
|
if (!store.cadDirty() && !store.tabs.anyDirty()) {
|
|
showToast("고친 도면이 없습니다.", "info");
|
|
return;
|
|
}
|
|
if (!(await saveEditedDrawings(store))) return;
|
|
showToast("저장했습니다.", "success");
|
|
onSaved();
|
|
}
|
|
|
|
/** CAD 자동백업 칸(`openwebcad/src/helpers/autosave.ts`) — 칸 이름 = `recoveryScopeOf`. */
|
|
const CAD_RECOVERY_KEY = "OPEN_WEB_CAD__RECOVERY";
|
|
|
|
/**
|
|
* 좌측 [초기화](298-2 · 300 C1 · C3) — 물은 뒤 서버가 저장본을 풀고 설정을 지움 → 고친 것 · CAD 자동백업 버림 →
|
|
* 페이지 다시 읽음(다음 GET 이 지금 앞 단계 저장본으로 원본을 그림). [취소] = 그대로.
|
|
*/
|
|
export async function resetDrawings(store: DrawingStore): Promise<void> {
|
|
if (store.isFrameEditing()) {
|
|
showToast("도각 편집을 마친 뒤 초기화하세요.", "warning");
|
|
return;
|
|
}
|
|
const ok = await showConfirmDialog(
|
|
"이 페이지에서 저장한 도면 · 고친 것 · 설정(축척 · 횡단 배치 · 평면도 종류 · 표식)을 모두 지우고 " +
|
|
"지금 앞 단계 값으로 다시 그릴까요? " +
|
|
"(프로젝트 도각은 그대로 — 되돌리려면 [도각 편집] → [기본 도각 복원])",
|
|
"초기화",
|
|
);
|
|
if (!ok) return;
|
|
showLoadingOverlay();
|
|
try {
|
|
await resetDesignDrawings(store.projectId);
|
|
store.drawings.forEach((item) =>
|
|
localStorage.removeItem(`${CAD_RECOVERY_KEY}__${recoveryScopeOf(store.projectId, item.id)}`),
|
|
);
|
|
showToast("초기화했습니다.", "success");
|
|
location.reload();
|
|
} catch (error) {
|
|
showToast(
|
|
error instanceof Error ? error.message : "상세 설계를 초기화하지 못했습니다.",
|
|
"error",
|
|
);
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|