/* ============================================================================= * M02_MasterTemplete_Drawing.ts * M02 도면 양식 편집 부품 — 웹캐드를 도각 편집 모드로 띄워 양식을 만들고 고친다 (PLAN 10-3). * * 계약(`tmp/M02_분석/6_계약.md` 화면 부품) — `mountDrawingTemplate(칸, 문서, {onSave, readOnly})` * → `{getDoc, destroy}`. 페이지(sub1)가 메인 칸에 붙인다. * `getDoc()` 은 CAD iframe 에서 편집본을 받아 오므로 **Promise** 다 — `await` 로 받는다. * * 양식 문서 = openwebcad 도면 JSON(`entities` · `layers`) + 양식 칸(`format` · `source` · * `drawing_area`). CAD 는 양식 칸을 모르므로 돌려줄 때 원래 칸 위에 편집본을 얹는다. * ========================================================================== */ import "./M02_MasterTemplete_Drawing.css"; import { API_BASE_URL } from "@config/config_frontend"; import { createCadHost, type CadHostDrawing } from "@ui/cad_host/cad_host"; import { createButton, createInputField, showToast } from "@ui/ui_template_elements"; /** 도면 양식 문서 — 작도 영역은 [x0, y0, x1, y1] (양식 좌표 mm). */ export interface DrawingTemplateDoc extends CadHostDrawing { format?: number; source?: string; drawing_area?: [number, number, number, number]; [key: string]: unknown; } export interface DrawingTemplateOptions { /** [저장] 을 누르면 편집본을 넘긴다. 없으면 [저장] 단추를 두지 않는다. */ onSave?: (doc: DrawingTemplateDoc) => void | Promise; /** 보기 전용 — CAD 그리기·수정 · 작도 영역 · 불러오기가 막힌다. */ readOnly?: boolean; /** 양식 이름 — CAD 자동백업 칸을 양식마다 나눈다(B07 도면 백업과도 안 겹침). */ name?: string; } export interface DrawingTemplateHandle { getDoc: () => Promise; destroy: () => void; } /** 작도 영역 칸이 없는 양식이 쓰는 값 — 서버 `Engine_Template._A1_INNER` 와 같다. */ const DEFAULT_AREA: [number, number, number, number] = [42, 47, 812, 567]; const AREA_LABELS = ["왼쪽 x", "아래 y", "오른쪽 x", "위 y"]; interface DrawingField { key: string; source: string; label: string; } async function fetchDrawingFields(): Promise { const response = await fetch(`${API_BASE_URL}/m02/drawing-fields`, { credentials: "include" }); const payload = (await response.json()) as { fields?: DrawingField[]; message?: string }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload.fields ?? []; } async function importDrawingFile( file: File, ): Promise<{ drawing: DrawingTemplateDoc; entity_count: number }> { const form = new FormData(); form.append("file", file); const response = await fetch(`${API_BASE_URL}/m02/drawing-import`, { method: "POST", credentials: "include", body: form, }); const payload = (await response.json()) as { drawing: DrawingTemplateDoc; entity_count: number; message?: string; }; if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`); return payload; } export function mountDrawingTemplate( container: HTMLElement, doc: DrawingTemplateDoc, options: DrawingTemplateOptions = {}, ): DrawingTemplateHandle { const readOnly = options.readOnly === true; const recoveryScope = `m02:${options.name ?? "drawing"}`; let base: DrawingTemplateDoc = doc; const root = document.createElement("div"); root.className = "m02-drawing"; const toolbar = document.createElement("div"); toolbar.className = "m02-drawing__toolbar"; // 작도 영역 — 도면 내용이 이 칸 한가운데에 놓인다. const area = document.createElement("div"); area.className = "m02-drawing__area"; const areaTitle = document.createElement("span"); areaTitle.className = "m02-drawing__title"; areaTitle.textContent = "작도 영역"; const start = doc.drawing_area ?? DEFAULT_AREA; const areaInputs = AREA_LABELS.map((label, index) => { const field = createInputField({ label, type: "number", value: String(start[index]) }); field.input.disabled = readOnly; return field; }); area.append(areaTitle, ...areaInputs.map((field) => field.root)); const readArea = (): [number, number, number, number] | null => { const values = areaInputs.map((field) => Number(field.input.value)); const valid = values.every(Number.isFinite) && values[0] < values[2] && values[1] < values[3]; areaInputs.forEach((field) => field.setError(valid ? undefined : "왼쪽<오른쪽 · 아래<위")); return valid ? (values as [number, number, number, number]) : null; }; // 자리표 키 — 누르면 `{{키}}` 를 복사한다. 도각 글자에 붙여 넣으면 그릴 때 값이 채워진다. const fields = document.createElement("div"); fields.className = "m02-drawing__fields"; const fieldsTitle = document.createElement("span"); fieldsTitle.className = "m02-drawing__title"; fieldsTitle.textContent = "자리표"; fields.append(fieldsTitle); void fetchDrawingFields() .then((list) => { for (const field of list) { const chip = document.createElement("button"); chip.type = "button"; chip.className = "m02-drawing__field"; chip.dataset.source = field.source; chip.textContent = `{{${field.key}}}`; chip.title = `${field.label} (${field.source}) — 눌러 복사`; chip.addEventListener("click", () => { void navigator.clipboard ?.writeText(`{{${field.key}}}`) .then(() => showToast(`{{${field.key}}} 를 복사했습니다.`, "success")); }); fields.append(chip); } }) .catch((error) => showToast(error instanceof Error ? error.message : "자리표 목록을 받지 못했습니다.", "error"), ); const cad = createCadHost({ title: "도면 양식" }); const load = (drawing: DrawingTemplateDoc): void => { cad.beginLoading(); cad.load(drawing, null, !readOnly, {}, { recoveryScope, readOnly }); }; const actions = document.createElement("div"); actions.className = "m02-drawing__actions"; if (!readOnly) { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".dxf,.dwg"; fileInput.hidden = true; fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; fileInput.value = ""; if (!file) return; importButton.disabled = true; importDrawingFile(file) .then((response) => { // 불러온 도각은 아직 저장하지 않는다 — 자리표를 놓고 [저장]을 눌러야 양식이 된다. load(response.drawing); showToast(`도형 ${response.entity_count}개를 불러왔습니다.`, "success"); }) .catch((error) => showToast( error instanceof Error ? error.message : "도각 파일을 불러오지 못했습니다.", "error", ), ) .finally(() => (importButton.disabled = false)); }); const importButton = createButton({ label: "파일 불러오기", variant: "ghost", onClick: () => fileInput.click(), }); actions.append(importButton, fileInput); } const getDoc = async (): Promise => { const { drawing } = await cad.requestSave(); const drawingArea = readArea(); if (!drawingArea) throw new Error("작도 영역 값이 올바르지 않습니다."); base = { ...base, ...drawing, drawing_area: drawingArea }; return base; }; if (options.onSave && !readOnly) { const onSave = options.onSave; const saveButton = createButton({ label: "저장", variant: "filled", onClick: () => { saveButton.disabled = true; getDoc() .then((next) => onSave(next)) .catch((error) => showToast( error instanceof Error ? error.message : "양식을 저장하지 못했습니다.", "error", ), ) .finally(() => (saveButton.disabled = false)); }, }); actions.append(saveButton); } toolbar.append(area, fields, actions); root.append(toolbar, cad.element); container.append(root); load(doc); return { getDoc, destroy: () => { cad.destroy(); root.remove(); }, }; }