diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index cb51508ff..0d3bcc8d2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -66,6 +66,7 @@ const TEXT = { saveLabel: "저장하고 보내기", failed: "표를 못 만듦 — {value}", noRange: "보낼 범위를 시트에서 먼저 고름", + empty: "보낼 칸에 내용이 없음", place: "도면에서 표를 놓을 자리를 누름(Esc = 그만)", noPlace: "이 도면 화면은 표 놓기를 아직 못 함", stale: "면적산출 값이 바뀐 표 {n}개", @@ -129,6 +130,10 @@ export interface AreaToDrawing { onArea: (at: Selection, readOnly: boolean, slot: HTMLElement) => void; /** 도면 상세가 섰음 — 넘겨받은 표 놓기 · 연결표 다름 알림 */ onDrawing: (handle: unknown, at: Selection, doc: StructureDoc) => void; + /** 보낸 표 놓기 기다리는 중 — 편집 칸 「저장 안 한 고침」 에 듦 */ + waiting: () => boolean; + /** 떠나기로 함(물음 [확인]) — 기다리던 표 버림 */ + drop: () => void; /** 도면 저장 문서 — 맨 위 `연결표` 를 도면의 표에 맞춤 */ withLinks: (doc: StructureDoc) => StructureDoc; } @@ -141,6 +146,8 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { let fresh = new Map(); let latest = new Map>(); let shown: string | null = null; + /** 보낸 표가 도면에 놓이길 기다림 — 떠날 때 「저장 안 한 고침」 물음(`waiting`) */ + let waiting = false; /** 글자 크기 작은 창 — 보내기면 크기 · 닫으면 null */ const askSize = (spec: { 범위: string }): Promise => @@ -183,15 +190,20 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { if (opt.isDirty()) return; // 저장 실패 — 알림은 저장 길이 띄움 } const picked = range; + const get = (size: number): Promise => + fetchTable(at.layer, at.name, { 시트: picked.시트, 범위: picked.범위, 글자크기: size }).catch( + (error: unknown) => (showToast(fill(TEXT.failed, "value", why(error)), "error"), null), + ); + // 보이는 글이 하나도 없는 범위(빈 칸만)는 크기를 묻기 전에 막음 + let got = await get(SIZE); + if (!got) return; + if (!Object.values(got.값 ?? {}).some((v) => String(v ?? "").trim())) { + return showToast(TEXT.empty, "error"); + } const size = await askSize(picked); if (size === null) return; - const spec: LinkSpec = { 시트: picked.시트, 범위: picked.범위, 글자크기: size }; - let got: AreaTable; - try { - got = await fetchTable(at.layer, at.name, spec); - } catch (error) { - return showToast(fill(TEXT.failed, "value", why(error)), "error"); - } + if (size !== SIZE) got = await get(size); + if (!got) return; pending = { layer: at.layer, column: at.name, got, 글자크기: size }; await opt.open({ layer: at.layer, @@ -247,7 +259,10 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { return { select: (s) => (range = { 시트: s.시트, 범위: s.범위 }), + waiting: () => waiting, + drop: () => (waiting = false), onArea: (at, readOnly, slot) => { + waiting = false; // 범위는 그대로 — 시트가 처음 고름(A1)을 이 갈고리보다 먼저 알림 if (readOnly || at.layer === "project") return; const button = createButton({ label: TEXT.send, variant: "ghost", size: "sm" }); @@ -257,6 +272,7 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { slot.append(button); }, onDrawing: (raw, at, doc) => { + waiting = false; const handle = raw as TableHandle; shown = `${at.layer}:${at.name}`; range = null; // 면적 상세를 떠남 @@ -273,12 +289,14 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { const style = { ...(표.shapeData.style ?? {}), [LINK]: id }; fresh.set(id, { 연결: id, 시트, 범위, 글자크기: give.글자크기, 값 }); handle.placeTable({ ...표, shapeData: { ...표.shapeData, style } }, id); + waiting = true; // 놓기 전 떠나도 물음(놓으면 도면 고침 · [저장] 때 풂) showToast(TEXT.place, "info"); } } void checkStale(handle, at, doc); }, withLinks: (doc) => { + waiting = false; const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row])); const rows: TableLink[] = []; for (const e of doc.도면?.entities ?? []) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 7e6d10079..13630fac5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -185,7 +185,7 @@ export function createMain( let seq = 0; let dirty = false; /** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 31-3) */ - const busy = (): boolean => dirty || !!panel?.dirty(); + const busy = (): boolean => dirty || !!panel?.dirty() || toDrawing.waiting(); // + 놓기 기다리는 표(41) const guard = (event: BeforeUnloadEvent): void => { if (!root.isConnected) return window.removeEventListener("beforeunload", guard); @@ -204,6 +204,7 @@ export function createMain( return false; } dirty = false; + toDrawing.drop(); return true; }; setLeaveGuard(confirmLeave); // 머리 메뉴 · 뒤로 가기로 떠날 때도 물음 diff --git a/resources/tester/test_m02_area_to_drawing.py b/resources/tester/test_m02_area_to_drawing.py index b0cf649be..b0e1a1923 100644 --- a/resources/tester/test_m02_area_to_drawing.py +++ b/resources/tester/test_m02_area_to_drawing.py @@ -94,6 +94,9 @@ globalThis.fetch = async (url, init) => { const old = { 연결: "T1", 시트: "s1", 범위: "A1:A1", 글자크기: 3, 값: { "s1!A1": "옛" } }; const drawing = (ents, links) => ({ 양식: "구조물도면", 종류: "structure", 판: 1, 열: "item1", 도번: "SD-1", 도면: { entities: ents }, ...(links ? { 연결표: links } : {}) }); ad.onDrawing(handle, { ...area, kind: "structure" }, drawing([table([["옛"]], { 연결: "T1" })], [old])); + const waitPlaced = ad.waiting(); // 놓기 기다림 = 저장 안 한 고침 + ad.drop(); + const waitDropped = ad.waiting(); await new Promise((r) => setTimeout(r, 20)); const notice = notices.at(-1) ?? []; const upd = notice.find((x) => x && x.on); @@ -109,7 +112,14 @@ globalThis.fetch = async (url, init) => { await slot.kids[0].on.click(); await new Promise((r) => setTimeout(r, 20)); ad.onDrawing(handle, { ...area, kind: "structure", name: "item2" }, drawing([])); - console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before })); + // 빈 칸만 고른 범위 — 크기를 묻기 전에 막고 알림 · 도면 안 엶 + now = [["", " "]]; + const openedBefore = opened.length; + ad.select({ 시트: "산출", 칸: "E1", 범위: "E1:F1" }); + await slot.kids[0].on.click(); + await new Promise((r) => setTimeout(r, 20)); + const empty = { opened: opened.length - openedBefore, toast: toasts.at(-1) }; + console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before, waitPlaced, waitDropped, waitSaved: ad.waiting(), empty })); })(); """ @@ -163,4 +173,14 @@ def test_다른_열_도면에는_안_놓음(got): def test_편집_칸은_갈고리만(): main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8") - assert main.count("toDrawing.") == 4 # select · onArea · onDrawing · withLinks + # select · onArea · onDrawing · withLinks · waiting(저장 안 한 고침) · drop(떠나기 확인) + assert main.count("toDrawing.") == 6 + assert "|| toDrawing.waiting()" in main.split("const busy = ")[1].splitlines()[0] + + +def test_놓기_기다림은_저장_안_한_고침(got): + assert got["waitPlaced"] is True and got["waitDropped"] is False and got["waitSaved"] is False + + +def test_빈_범위는_막고_알림(got): + assert got["empty"] == {"opened": 0, "toast": ["error", "보낼 칸에 내용이 없음"]}