/* ============================================================================= * M02_MasterTemplete_DrawingCard.ts * 항목 카드 「도면 정보」 미리보기 — 고칠 수 있으면 메뉴(제목줄 · 리본 · 패널 · 상태줄) 없이 * **단축키로 그리기 · 고치기**(전체 편집기 [도면 수정]과 같은 웹캐드 · 같은 단축키) · 카드 안 [저장] * (전체 편집기와 같은 저장 길 = 그 항목 구조물 도면 파일 `도면` 칸만 갈아 끼움) · ? = 단축키 도움말 * (PLAN 31-4). 고칠 수 없으면(읽기 층 · 저장 길 없는 자료 자리) 예전처럼 보기만. * CAD 앱은 안 고침 — 메뉴는 같은 출처 iframe 에 스타일을 얹어 숨김. * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { StaleError, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch"; import { mountDrawingTemplate, type DrawingTemplateDoc } from "./M02_MasterTemplete_Drawing"; import type { ItemSource } from "./M02_MasterTemplete_ItemSource"; const TEXT = { save: "저장", help: "?", helpTitle: "단축키 — 글자를 치고 Enter", saved: "도면을 저장함", stale: "그 사이 도면이 바뀜 — 항목을 다시 골라 새로 읽음", failed: "도면을 저장 못 함 — {value}", }; /** 단축키 도움말 — 전체 편집기 명령 별칭(openwebcad `commands.*.ts`)과 같은 것만 */ const KEYS: [string, string][] = [ ["L · PL", "선 · 폴리선"], ["C · A · REC", "원 · 호 · 사각형"], ["T · DT", "글(여러 줄 · 한 줄)"], ["DLI", "치수"], ["M · CO · RO", "옮기기 · 복사 · 회전"], ["SC · MI · O", "축척 · 대칭 · 간격 띄우기"], ["TR · EX · F", "자르기 · 연장 · 모깎기"], ["E · Delete", "지우기"], ["누름 · 끌기 · Ctrl+A", "고르기 · 전부"], ["Esc", "고름 풀기 · 명령 멈춤"], ["Ctrl+Z · Ctrl+Y", "되돌리기 · 다시 하기"], ["Ctrl+S", "저장(카드 [저장]과 같음)"], ["휠 · 가운데 끌기", "확대·축소 · 화면 옮기기"], ]; /** 보기만 = 제목줄 · 리본만 숨김(예전 그대로) · 고치기 = 패널 · 상태줄까지(명령 안내 줄은 남김) */ const HIDE_VIEW = ".cad-titlebar, .cad-ribbon { display: none !important; }"; const HIDE_EDIT = ".cad-titlebar, .cad-ribbon, .cad-inspector, .cad-statusbar, .cad-quick-access" + " { display: none !important; }"; /** 같은 출처 iframe 안쪽에 스타일 한 벌 — 다시 뜨면(load) 또 얹음 */ function hideChrome(frame: HTMLIFrameElement, css: string): 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 = css; inner.head.append(style); }; frame.addEventListener("load", apply); apply(); } export interface DrawingCardOptions { /** 구조물 도면 문서 통째(`도번` · `도면` …) — 저장은 `도면` 칸만 갈아 끼움 */ doc: StructureDoc; /** 읽은 판 — 저장 때 보냄(다르면 409) */ version: string | null; source: ItemSource; column: string; /** 고칠 권한(층) — 자료 자리에 저장 길(`write`)도 있어야 고치기 */ editable: boolean; /** 저장 안 한 고침이 생김 · 없어짐 */ onDirty?: (dirty: boolean) => void; } export interface DrawingCardHandle { /** 카드 머리에 붙일 [저장] · ? (보기만이면 빈 칸) */ tools: HTMLElement; dirty: () => boolean; /** 고친 것이 있으면 카드 [저장]과 같이 저장 — 못 하면 던짐(위 [저장]이 먼저 부름) */ save: () => Promise; destroy: () => void; } export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): DrawingCardHandle { const write = opt.source.write; const edit = opt.editable && !!write; let doc = opt.doc; let version = opt.version ?? ""; let dirty = false; let saving = false; const save = createButton({ label: TEXT.save, variant: "filled", size: "sm" }); const help = createButton({ label: TEXT.help, variant: "ghost", size: "sm" }); help.title = help.ariaLabel = TEXT.helpTitle; const helpBox = el("div", { className: "m02-struct__keys", attrs: { hidden: "" }, children: [ el("strong", { text: TEXT.helpTitle }), el("dl", { children: KEYS.flatMap(([key, what]) => [ el("dt", { text: key }), el("dd", { text: what }), ]), }), ], }); help.addEventListener("click", () => (helpBox.hidden = !helpBox.hidden)); const setDirty = (next: boolean): void => { dirty = next; save.disabled = !next || saving; opt.onDirty?.(next); }; /** 전체 편집기 [저장]과 같은 길 — 읽은 문서에 `도면` 만 갈아 끼워 그 항목 파일에 */ async function doSave(drawing?: DrawingTemplateDoc): Promise { if (!write || saving) return; saving = true; save.disabled = true; try { const next = { ...doc, 도면: (drawing ?? (await handle.getDoc())) as StructureDoc["도면"] }; version = (await write("structure", opt.column, version, next)).판; doc = next; setDirty(false); showToast(TEXT.saved, "success"); } catch (error) { const why = error instanceof Error ? error.message : ""; showToast( error instanceof StaleError ? TEXT.stale : TEXT.failed.replace("{value}", why), "error", ); } finally { saving = false; save.disabled = !dirty; } } save.addEventListener("click", () => void doSave()); save.disabled = true; const handle = mountDrawingTemplate(host, doc.도면 as unknown as DrawingTemplateDoc, { readOnly: !edit, name: `preview:${opt.column}`, // 자동백업 칸은 전체 편집 화면과 갈라 둠 layer: opt.source.layer, projectId: opt.source.projectId, panelsCollapsed: true, preview: !edit, // 보기만 = 왼쪽 끌기 화면 옮기기 · 고치기 = 누름 고르기 · 단축키 originMarker: true, // 좌표 0,0 기준점 십자 noArea: true, // 구조물도는 도각이 없음 ...(edit ? { onChanged: setDirty, onSave: (drawing) => doSave(drawing) } : {}), }); const frame = host.querySelector("iframe"); if (frame) hideChrome(frame, edit ? HIDE_EDIT : HIDE_VIEW); if (edit) host.append(helpBox); 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(); }, }; }