diff --git a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts index d12cc9b4..46d1f7dc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts @@ -75,6 +75,8 @@ export interface DrawingCardHandle { /** 카드 머리에 붙일 [저장] · ? (보기만이면 빈 칸) */ tools: HTMLElement; dirty: () => boolean; + /** 고친 것이 있으면 카드 [저장]과 같이 저장 — 못 하면 던짐(위 [저장]이 먼저 부름) */ + save: () => Promise; destroy: () => void; } @@ -153,6 +155,11 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr return { tools: el("span", { className: "m02-struct__tools", children: edit ? [help, save] : [] }), dirty: () => dirty, + save: async () => { + if (!dirty) return; + await doSave(); + if (dirty) throw new Error(TEXT.failed.replace("{value}", "")); + }, destroy: () => { handle.destroy(); helpBox.remove(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 4d5b1a6a..4c034a8d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -47,13 +47,10 @@ const TEXT = { noFieldNumber: "{value} 없음", noSheet: "스프레드시트를 못 띄움 — {value}", readFailed: "구조물 도면을 못 읽음 — {value}", -<<<<<<< HEAD discard: "도면 카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?", -======= save: "저장", saved: "저장했음", saveFailed: "저장 못 함 — {value}", ->>>>>>> origin/sub_laptop_8 }; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); @@ -84,10 +81,8 @@ export interface ItemCardsOptions { warnOnFail?: (column: string) => boolean; /** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */ extraCards?: HTMLElement[]; -<<<<<<< HEAD /** 도면 카드에서 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4) */ onDirty?: (dirty: boolean) => void; -======= /** 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */ sheetEdit?: SheetEditOptions; } @@ -114,7 +109,6 @@ export interface SheetEditOptions { ) => Promise<{ links?: LinkOptions; below?: HTMLElement; refresh?: () => void }>; /** 카드 [저장] — 전체 편집기와 같은 저장 길 · 새 판을 돌려줌 */ save: (kind: SheetKind, column: string, doc: unknown, version: string) => Promise; ->>>>>>> origin/sub_laptop_8 } export interface ItemCardsHandle { @@ -127,15 +121,12 @@ export interface ItemCardsHandle { relabel: (labels: Partial>) => void; /** 지금 보이는 열 id */ column: () => string | null; -<<<<<<< HEAD - /** 도면 카드에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */ + /** 카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */ dirty: () => boolean; -======= - /** 카드에 저장 안 한 고침이 있나(31-3) */ - dirty: () => boolean; - /** 고친 카드를 모두 저장 — 실패하면 던짐 */ + /** 고친 카드를 모두 저장(도면 · 엑셀) — 실패하면 던짐 */ saveAll: () => Promise; ->>>>>>> origin/sub_laptop_8 + /** 저장 안 한 카드 고침을 버림(물음에서 「확인」 뒤) */ + discard: () => void; destroy: () => void; } @@ -240,8 +231,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { } }; -<<<<<<< HEAD -======= /** 카드 [저장] — 읽은 문서(번호 등 계약 밖 칸)에 시트를 덮어 저장(전체 편집기 `doSave` 와 같은 모양) */ async function saveCard(c: SheetCard): Promise { const at = column; @@ -311,21 +300,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { c.save.disabled = true; }; - /** 미리보기 CAD 는 iframe — 안쪽 도구 모음(제목줄 · 리본)을 스타일로 숨김(같은 출처라 접근 가능) */ - const hideToolbar = (frame: HTMLIFrameElement): void => { - const apply = (): void => { - const inner = frame.contentDocument; - if (!inner?.head || inner.getElementById("m02-struct-hide")) return; - const style = inner.createElement("style"); - style.id = "m02-struct-hide"; - style.textContent = ".cad-titlebar, .cad-ribbon { display: none !important; }"; - inner.head.append(style); - }; - frame.addEventListener("load", apply); - apply(); - }; - ->>>>>>> origin/sub_laptop_8 /** 스프레드시트 미리보기 — 보기만(고침은 [수정] 이 여는 전체 화면) · 부품을 늦게 받음 · * 그 사이 열이 바뀌면 버림 */ const paintSheet = async (c: SheetCard, mine: number): Promise => { @@ -438,14 +412,15 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { } }, column: () => column, -<<<<<<< HEAD - dirty: () => !!preview?.dirty(), -======= - dirty: () => sheets.some((c) => c.dirty), + dirty: () => !!preview?.dirty() || sheets.some((c) => c.dirty), saveAll: async () => { + await preview?.save(); // 도면 카드(31-4) for (const c of sheets) await saveCard(c); }, ->>>>>>> origin/sub_laptop_8 + discard: () => { + drop(); + opts.onDirty?.(false); + }, destroy: () => { seq += 1; drop(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index c1475dc1..30a1b1be 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -70,7 +70,7 @@ const TEXT = { noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름", noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음", inputsFailed: "일위대가 입력값을 못 받음 — {value}", - discardCards: "면적산출 · 재료산출 카드에 저장 안 한 고침이 있음 — 버리고 넘어갈까?", + discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?", }; /** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 · 복사 */ @@ -381,14 +381,12 @@ export interface StructurePanelHandle { reload: () => Promise; /** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */ saveInputs: () => Promise; - /** 면적산출 · 재료산출 카드에 저장 안 한 고침(31-3) */ + /** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 · 재료산출 31-3) — 페이지가 떠날 때 함께 물음 */ dirty: () => boolean; - /** 고친 카드 저장 — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */ + /** 고친 카드 저장(도면 · 엑셀) — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */ saveCards: () => Promise; /** 고른 열이 바뀜 — 같은 열이면 이름만 새로(CAD 를 다시 안 띄움) */ show: (colId: string | null, columns: StructureColumn[]) => void; - /** 도면 카드에 저장 안 한 고침(31-4 · 카드 [저장] 몫) — 페이지가 떠날 때 함께 물음 */ - dirty: () => boolean; /** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */ refresh: (columns: StructureColumn[]) => void; /** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) · @@ -443,8 +441,9 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane // 카드에 저장 안 한 고침 — 버릴지 물음 · 머물면 표의 고른 열을 되돌림 const from = column; void showConfirmDialog(TEXT.discardCards).then((ok) => { - if (ok) void load(col); - else if (from) opts.keepColumn?.(from); + if (!ok) return void (from && opts.keepColumn?.(from)); + cards.discard(); // 한 번 물었음 — 카드가 다시 묻지 않게 + void load(col); }); return; } @@ -522,10 +521,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane }, saveInputs: () => inputs.save(), dirty: () => cards.dirty(), -<<<<<<< HEAD -======= saveCards: () => cards.saveAll(), ->>>>>>> origin/sub_laptop_8 show, refresh: (columns) => { const at = column; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index bb934ef8..480f213f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -194,26 +194,18 @@ export function createMain( let cpanel: ComboPanel | null = null; // 면적산출 · 재료산출 오른쪽 조합 패널(프로젝트 층 아님) let seq = 0; let dirty = false; - /** 저장 안 한 고침 — 이 화면 + 면적산출 · 재료산출 카드(31-3) */ + /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 · 재료산출 31-3) */ const busy = (): boolean => dirty || !!panel?.dirty(); const guard = (event: BeforeUnloadEvent): void => { if (!root.isConnected) return window.removeEventListener("beforeunload", guard); -<<<<<<< HEAD - if (!dirty && !panel?.dirty()) return; -======= if (!busy()) return; ->>>>>>> origin/sub_laptop_8 event.preventDefault(); event.returnValue = ""; }; window.addEventListener("beforeunload", guard); const confirmLeave = async (): Promise => { -<<<<<<< HEAD - if (!dirty && !panel?.dirty()) return true; // 도면 카드 고침(31-4)도 같은 물음 -======= - if (!busy()) return true; ->>>>>>> origin/sub_laptop_8 + if (!busy()) return true; // 카드 고침(도면 · 엑셀)도 같은 물음 if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) { // 취소 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로 host diff --git a/resources/tester/test_m02_drawing_card.py b/resources/tester/test_m02_drawing_card.py index 38f4ef4a..03521d06 100644 --- a/resources/tester/test_m02_drawing_card.py +++ b/resources/tester/test_m02_drawing_card.py @@ -45,7 +45,9 @@ def test_저장_안_한_카드_고침은_물음(): "if (preview?.dirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard)))" in cards ) - assert "dirty: () => !!preview?.dirty()," in cards + # 도면 카드 + 엑셀 카드(31-3) 둘 다 봄 + assert "dirty: () => !!preview?.dirty() || sheets.some((c) => c.dirty)," in cards assert "dirty: () => cards.dirty()," in _text("M02_MasterTemplete_Structure.ts") main = _text("M02_MasterTemplete_UI_Main.ts") - assert main.count("!dirty && !panel?.dirty()") == 2 # 떠날 때 물음 · 창 닫기 막음 + assert "const busy = (): boolean => dirty || !!panel?.dirty();" in main + assert main.count("!busy()") >= 2 # 떠날 때 물음 · 창 닫기 막음