Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
545 lines
22 KiB
TypeScript
545 lines
22 KiB
TypeScript
/* =============================================================================
|
|
* 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 { createCadTabs, type CadTab, type CadTabs } from "@ui/cad_host/cad_host_tabs";
|
|
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
|
|
/** 도면 양식 문서 — 작도 영역은 [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 {
|
|
/** CAD 안 💾 · Ctrl+S 가 부른다 — 페이지 머리 [저장] 과 같은 일(판 · 409 흐름)을 하게 페이지가 넘긴다. */
|
|
onSave?: (doc: DrawingTemplateDoc) => void | Promise<void>;
|
|
/** 보기 전용 — CAD 그리기·수정 · 작도 영역 · 불러오기가 막힌다. */
|
|
readOnly?: boolean;
|
|
/** 양식 이름 — CAD 자동백업 칸을 양식마다 나눈다(B07 도면 백업과도 안 겹침). */
|
|
name?: string;
|
|
/** 양식 층 — 같은 이름도 층마다 백업 칸이 갈린다(`m02:층:이름`). */
|
|
layer?: string;
|
|
/** 프로젝트 층이면 프로젝트 id — 칸이 `m02:project:프로젝트id:이름` 이 된다. */
|
|
projectId?: string | null;
|
|
/** 저장 안 한 고침이 생기면 true — CAD 편집 · 작도 영역 값 바꿈. */
|
|
onChanged?: (dirty: boolean) => void;
|
|
/** 페이지 제목 줄 안의 빈 칸 — 있으면 작도 영역 칸 · 파일 불러오기를 거기 한 줄로 놓는다. */
|
|
headerSlot?: HTMLElement;
|
|
/** CAD 좌측 패널(특성 · 도면층)을 접힌 채로 연다 — 누르면 펼침(구조물 도면 미리보기). */
|
|
panelsCollapsed?: boolean;
|
|
/** 미리보기(집계표 카드) — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
|
|
preview?: boolean;
|
|
/** 좌표 0,0 기준점 십자를 그린다 — 구조물 도면(편집 · 미리보기)만. */
|
|
originMarker?: boolean;
|
|
/** 구조물 도면 — 작도 영역 단추 · 자리표(도각 자리) 표시를 없애고, 문서의 작도 영역 값은 무시. */
|
|
noArea?: boolean;
|
|
/** B07 도각을 외곽 좌하단 (0,0) 에 얹음 — 잠금 층 · 보여주기만(돌려줄 때 뺌 · 저장 안 됨). */
|
|
frame?: boolean;
|
|
/** 파일 불러오기(📂) 단추를 안 둠 — 집계표 카드 미리보기(불러오기는 도면 상세에서만). */
|
|
noImport?: boolean;
|
|
/** 장id(82-1) — 자동백업 칸을 장마다 나눔(1장 = 빈 값 · 옛 칸 그대로) */
|
|
sheet?: string;
|
|
/** 장 도번(82-2) — 얹은 도각 `{{도면번호}}` 자리 */
|
|
frameNumber?: string;
|
|
/** 탭 줄(82-1) — 그림 칸 바로 위에 놓음 */
|
|
tabs?: HTMLElement;
|
|
}
|
|
|
|
const RECOVERY_PREFIX = "OPEN_WEB_CAD__RECOVERY__";
|
|
|
|
/** 자동백업 칸 이름 — 층(과 프로젝트)을 붙여 같은 이름의 양식끼리 안 겹치게 한다. */
|
|
function recoveryScopeOf(options: DrawingTemplateOptions, sheet = ""): string {
|
|
const name = `${options.name ?? "drawing"}${sheet ? `#${sheet}` : ""}`;
|
|
if (!options.layer) return `m02:${name}`;
|
|
const project = options.layer === "project" ? `:${options.projectId ?? ""}` : "";
|
|
return `m02:${options.layer}${project}:${name}`;
|
|
}
|
|
|
|
/** 층 없는 옛 칸(`m02:이름`)을 새 칸으로 한 번 옮기고 지운다 — 새 칸에 이미 있으면 그대로 두고 지움. */
|
|
function migrateOldRecovery(options: DrawingTemplateOptions, scope: string): void {
|
|
const old = `${RECOVERY_PREFIX}m02:${options.name ?? "drawing"}`;
|
|
if (!options.layer || old === `${RECOVERY_PREFIX}${scope}`) return;
|
|
try {
|
|
const value = localStorage.getItem(old);
|
|
if (value === null) return;
|
|
if (localStorage.getItem(`${RECOVERY_PREFIX}${scope}`) === null) {
|
|
localStorage.setItem(`${RECOVERY_PREFIX}${scope}`, value);
|
|
}
|
|
localStorage.removeItem(old);
|
|
} catch {
|
|
// 저장소를 못 써도 편집은 됨
|
|
}
|
|
}
|
|
|
|
export interface DrawingTemplateHandle {
|
|
getDoc: () => Promise<DrawingTemplateDoc>;
|
|
/** 표 놓기 · 칸 갈기(41장) — `cad_host` 그대로 */
|
|
placeTable: (entity: Record<string, unknown>, link: string) => void;
|
|
updateTable: (link: string, cells: unknown[][]) => Promise<number>;
|
|
/** 다른 장을 같은 CAD 에 실음(82-1) — 백업 칸 · 도각 도번도 그 장 것으로 */
|
|
loadSheet: (
|
|
drawing: DrawingTemplateDoc,
|
|
sheet: string,
|
|
frameNumber?: string,
|
|
after?: () => void,
|
|
) => Promise<void>;
|
|
destroy: () => void;
|
|
}
|
|
|
|
/** 작도 영역 칸이 없는 양식이 쓰는 값 — 서버 `Engine_Template._A1_INNER` 와 같다. */
|
|
const DEFAULT_AREA: [number, number, number, number] = [42, 47, 812, 567];
|
|
const AREA_LABELS = ["왼쪽 x", "아래 y", "오른쪽 x", "위 y"];
|
|
|
|
/** 얹은 도각 층 — 서버 `STRUCTURE_FRAME_LAYER` 와 같음 · 돌려줄 때 이 층을 통째로 뺌 */
|
|
const FRAME_LAYER_ID = "m02-structure-frame";
|
|
interface StructureFrame {
|
|
entities: Record<string, unknown>[];
|
|
layer: Record<string, unknown> | null;
|
|
}
|
|
const BLANK_LAYER = { id: "기본", name: "기본", isVisible: true, isLocked: false };
|
|
const framePromises = new Map<string, Promise<StructureFrame | null>>();
|
|
|
|
/** 구조물 도각 — 장 도번마다 한 번만 받아 돌려씀(`{{도면번호}}` · 82-2) · 못 받으면 도각 없이 */
|
|
function fetchFrame(number = ""): Promise<StructureFrame | null> {
|
|
const query = number ? `?number=${encodeURIComponent(number)}` : "";
|
|
const got =
|
|
framePromises.get(number) ??
|
|
fetch(`${API_BASE_URL}/m02/structure-frame${query}`, { credentials: "include" })
|
|
.then((response) => (response.ok ? (response.json() as Promise<StructureFrame>) : null))
|
|
.catch(() => null)
|
|
.then((frame) => {
|
|
if (!frame?.layer) framePromises.delete(number); // 다음에 다시 받음
|
|
return frame?.layer ? frame : null;
|
|
});
|
|
framePromises.set(number, got);
|
|
return got;
|
|
}
|
|
|
|
/** 도각 층을 뺀 도면 — 구조물 엔티티 · 층만 남음 */
|
|
function withoutFrame(drawing: DrawingTemplateDoc): DrawingTemplateDoc {
|
|
return {
|
|
...drawing,
|
|
entities: drawing.entities.filter((e) => e.layerId !== FRAME_LAYER_ID),
|
|
layers: drawing.layers?.filter((layer) => layer.id !== FRAME_LAYER_ID),
|
|
};
|
|
}
|
|
|
|
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 noArea = options.noArea === true;
|
|
let recoveryScope = recoveryScopeOf(options, options.sheet);
|
|
let frameNumber = options.frameNumber ?? "";
|
|
migrateOldRecovery(options, recoveryScopeOf(options));
|
|
let base: DrawingTemplateDoc = doc;
|
|
|
|
const root = document.createElement("div");
|
|
root.className = "m02-drawing";
|
|
const toolbar = document.createElement("div");
|
|
toolbar.className = "m02-drawing__toolbar";
|
|
|
|
// 작도 영역 — 도면 내용이 이 칸 한가운데에 놓인다. 제목 줄에는 값이 다 보이는 단추 하나,
|
|
// 누르면 칸 넷이 작은 창으로 펼쳐진다(칸은 창을 닫아도 그대로 살아 있음).
|
|
const areaForm = document.createElement("div");
|
|
areaForm.className = "m02-drawing__areaform";
|
|
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;
|
|
field.input.addEventListener("input", () => {
|
|
options.onChanged?.(true);
|
|
readArea();
|
|
});
|
|
return field;
|
|
});
|
|
areaForm.append(...areaInputs.map((field) => field.root));
|
|
|
|
const areaButton = createButton({ label: "", variant: "ghost" });
|
|
areaButton.classList.add("m02-drawing__areabtn");
|
|
areaButton.title = "작도 영역 — 도면 내용이 놓이는 칸(양식 좌표 mm) · 눌러서 고침";
|
|
let areaModal: { close: () => void } | null = null;
|
|
|
|
const readArea = (): [number, number, number, number] | null => {
|
|
const values = areaInputs.map((field) => Number(field.input.value));
|
|
const valid =
|
|
areaInputs.every((field) => field.input.value.trim() !== "") &&
|
|
values.every(Number.isFinite) &&
|
|
values[0] < values[2] &&
|
|
values[1] < values[3];
|
|
// 빈 칸 · 숫자 아님은 그 칸만 짚음 — 모두 숫자인데 어긋나면 크기 순서 안내
|
|
const own = areaInputs.map((field) => {
|
|
const raw = field.input.value.trim();
|
|
return raw === "" ? "값을 넣을 것" : Number.isFinite(Number(raw)) ? undefined : "숫자만";
|
|
});
|
|
const order = own.some(Boolean) ? undefined : "왼쪽<오른쪽 · 아래<위";
|
|
areaInputs.forEach((field, index) => field.setError(valid ? undefined : (own[index] ?? order)));
|
|
areaButton.textContent = `작도 영역 ${areaInputs.map((f) => f.input.value.trim() || "?").join(" · ")}`;
|
|
areaButton.classList.toggle("is-invalid", !valid);
|
|
return valid ? (values as [number, number, number, number]) : null;
|
|
};
|
|
readArea();
|
|
areaButton.addEventListener("click", () => {
|
|
areaModal?.close();
|
|
areaModal = openModal({
|
|
title: "작도 영역",
|
|
closeLabel: "닫기",
|
|
dialogClass: "m02-modal",
|
|
backdropClass: "m02-pop",
|
|
mount: (body) => body.append(areaForm),
|
|
});
|
|
// 단추 바로 아래 · 오른쪽 맞춤 · 화면 밖으로 안 나가게
|
|
const dialog = document.querySelector<HTMLElement>(".m02-pop > .ui-modal");
|
|
if (!dialog) return;
|
|
const at = areaButton.getBoundingClientRect();
|
|
const width = dialog.offsetWidth;
|
|
dialog.style.top = `${at.bottom + 4}px`;
|
|
dialog.style.left = `${Math.max(8, Math.min(at.right - width, innerWidth - width - 8))}px`;
|
|
dialog.style.maxHeight = `${innerHeight - at.bottom - 12}px`;
|
|
});
|
|
|
|
// CAD 안 💾 · Ctrl+S — 양식 파일은 페이지 [저장] 만 쓴다. 저장 중 또 눌러도 한 번만 부른다.
|
|
let saving = false;
|
|
const hostSave = (): void => {
|
|
if (readOnly) return void showToast("이 양식은 볼 수만 있습니다.", "info");
|
|
if (saving || !options.onSave) return;
|
|
saving = true;
|
|
getDoc()
|
|
.then((next) => options.onSave?.(next))
|
|
.catch((error) =>
|
|
showToast(error instanceof Error ? error.message : "양식을 저장하지 못했습니다.", "error"),
|
|
)
|
|
.finally(() => (saving = false));
|
|
};
|
|
|
|
const cad = createCadHost<DrawingTemplateDoc>({
|
|
title: "도면 양식",
|
|
onChanged: (dirty) => options.onChanged?.(dirty),
|
|
onHostSave: hostSave,
|
|
});
|
|
let loadSeq = 0;
|
|
/** `after` = 싣기를 보낸 바로 뒤(이 싣기가 마지막일 때만) — 표 놓기를 「열린 뒤」로 미룰 때 */
|
|
const load = async (drawing: DrawingTemplateDoc, after?: () => void): Promise<void> => {
|
|
const mine = ++loadSeq;
|
|
cad.beginLoading();
|
|
const frame = options.frame ? await fetchFrame(frameNumber) : null;
|
|
if (mine !== loadSeq) return; // 그새 다른 도면을 실음
|
|
const own = withoutFrame(drawing);
|
|
const shown = frame?.layer
|
|
? {
|
|
...own,
|
|
entities: [...own.entities, ...frame.entities],
|
|
// 층 없는 빈 문서 — 잠금 도각 층만 남으면 새 그림이 도각 층에 떨어짐 · 기본 층(서버 `_BLANK_LAYER`)을 채움
|
|
layers: [...(own.layers?.length ? own.layers : [BLANK_LAYER]), frame.layer],
|
|
}
|
|
: drawing;
|
|
cad.load(
|
|
shown,
|
|
null,
|
|
!readOnly && !noArea,
|
|
{},
|
|
{
|
|
recoveryScope,
|
|
readOnly,
|
|
hostTitle: "도면 양식 편집",
|
|
hostSave: true,
|
|
ribbonCollapsed: true,
|
|
panelsCollapsed: options.panelsCollapsed,
|
|
viewOnly: options.preview === true,
|
|
originMarker: options.originMarker === true,
|
|
},
|
|
);
|
|
after?.();
|
|
};
|
|
|
|
const actions = document.createElement("div");
|
|
actions.className = "m02-drawing__actions";
|
|
const canImport = !readOnly && options.noImport !== true;
|
|
if (canImport) {
|
|
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) => {
|
|
// 불러온 도각은 아직 저장하지 않는다 — 자리표를 놓고 [저장]을 눌러야 양식이 된다.
|
|
void load(response.drawing);
|
|
options.onChanged?.(true); // 불러온 것은 아직 저장 전 — 떠날 때 묻게 함
|
|
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(),
|
|
});
|
|
importButton.title = importButton.ariaLabel = "파일 불러오기 (DXF · DWG)";
|
|
actions.append(importButton, fileInput);
|
|
}
|
|
|
|
const getDoc = async (): Promise<DrawingTemplateDoc> => {
|
|
const { drawing: saved } = await cad.requestSave();
|
|
const drawing = options.frame ? withoutFrame(saved) : saved;
|
|
if (noArea) {
|
|
base = { ...base, ...drawing };
|
|
return base;
|
|
}
|
|
const drawingArea = readArea();
|
|
if (!drawingArea) throw new Error("작도 영역 값이 올바르지 않습니다.");
|
|
base = { ...base, ...drawing, drawing_area: drawingArea };
|
|
return base;
|
|
};
|
|
|
|
// 한 줄 — 페이지가 머리 칸(headerSlot)을 주면 거기, 아니면 그림 칸 위에 작게.
|
|
toolbar.append(...(noArea ? [] : [areaButton]), actions);
|
|
const tabs = options.tabs ? [options.tabs] : [];
|
|
if (options.headerSlot) {
|
|
toolbar.classList.add("m02-drawing__toolbar--slot");
|
|
options.headerSlot.append(toolbar);
|
|
root.append(...tabs, cad.element);
|
|
} else root.append(...(noArea && !canImport ? [] : [toolbar]), ...tabs, cad.element);
|
|
container.append(root);
|
|
void load(doc);
|
|
|
|
return {
|
|
getDoc,
|
|
placeTable: cad.placeTable,
|
|
updateTable: cad.updateTable,
|
|
loadSheet: (drawing, sheet, number, after) => {
|
|
recoveryScope = recoveryScopeOf(options, sheet);
|
|
frameNumber = number ?? "";
|
|
base = drawing;
|
|
return load(drawing, after);
|
|
},
|
|
destroy: () => {
|
|
areaModal?.close();
|
|
cad.destroy();
|
|
toolbar.remove();
|
|
root.remove();
|
|
},
|
|
};
|
|
}
|
|
|
|
/* --- 구조물 도면 여러 장(82-1 · 82-2) — 한 파일 `도면`(1장) + `더한장[]` · 탭 줄 = 공용 `cad_host_tabs` --- */
|
|
|
|
/** 구조물 도면 더한 장 — `M02_MasterTemplete_Api_Fetch` `StructureSheet` 와 같은 꼴 */
|
|
interface SheetDoc {
|
|
장id: string;
|
|
이름: string;
|
|
도면: DrawingTemplateDoc;
|
|
}
|
|
/** 구조물 도면 문서에서 쓰는 칸 */
|
|
interface SheetsDoc {
|
|
도번?: string;
|
|
도면: DrawingTemplateDoc;
|
|
더한장?: SheetDoc[];
|
|
}
|
|
|
|
const FIRST_TAB = "#1"; // 1장 탭 id — 장id 는 「#」 를 안 씀(저장은 빈 값)
|
|
const FIRST_NAME = "1장";
|
|
|
|
/** 장 도번 — 1장 = 도번 · n 째 장 = 도번-n(SD-000010-2) */
|
|
export const sheetNumber = (number: string | undefined, index: number): string =>
|
|
!number ? "" : index === 0 ? number : `${number}-${index + 1}`;
|
|
|
|
/** 표 칸 갈기 — 안 실린 장(받아 둔 편집본)에서 · 크기는 칸 수가 바뀔 때만(서버 `refresh_drawing` 과 같음) */
|
|
function updateStashedTables(
|
|
drawing: DrawingTemplateDoc,
|
|
link: string,
|
|
cells: unknown[][],
|
|
): number {
|
|
let count = 0;
|
|
for (const entity of drawing.entities) {
|
|
const shape = (entity as { type?: string; shapeData?: Record<string, unknown> }).shapeData;
|
|
const style = shape?.style as Record<string, unknown> | undefined;
|
|
if (entity.type !== "Table" || !shape || style?.연결 !== link) continue;
|
|
shape.cells = cells;
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
export interface StructureSheetsHandle extends Omit<DrawingTemplateHandle, "getDoc" | "loadSheet"> {
|
|
/** `{도면, 더한장?}` — 부르는 쪽이 읽은 문서에 얹음 */
|
|
getDoc: () => Promise<{ 도면: DrawingTemplateDoc; 더한장?: SheetDoc[] }>;
|
|
/** 지금 탭 장id(1장 = 빈 값) */
|
|
activeSheet: () => string;
|
|
}
|
|
|
|
/** 구조물 도면 상세 — 탭 줄 + 한 CAD · 탭 바꾸면 지금 장을 받아 두고 다음 장을 실음 */
|
|
export function mountStructureSheets(
|
|
container: HTMLElement,
|
|
doc: SheetsDoc,
|
|
options: DrawingTemplateOptions = {},
|
|
): StructureSheetsHandle {
|
|
const readOnly = options.readOnly === true;
|
|
const more = doc.더한장 ?? [];
|
|
let cadDirty = false;
|
|
let listDirty = false;
|
|
/** 아직 안 놓은 보낸 표(82-3) — 놓기 전에 탭을 바꾸면 그 장에 다시 걺(놓을 장 = 지금 탭) */
|
|
let placing: { entity: Record<string, unknown>; link: string } | null = null;
|
|
const placed = (got: DrawingTemplateDoc, link: string): boolean =>
|
|
got.entities.some((e) => {
|
|
const style = (e as { shapeData?: { style?: Record<string, unknown> } }).shapeData?.style;
|
|
return e.type === "Table" && style?.연결 === link;
|
|
});
|
|
const report = (): void => options.onChanged?.(cadDirty || listDirty || tabs.anyDirty());
|
|
const numberOf = (id: string): string =>
|
|
sheetNumber(
|
|
doc.도번,
|
|
tabs.tabs().findIndex((tab) => tab.id === id),
|
|
);
|
|
|
|
const tabs: CadTabs<DrawingTemplateDoc> = createCadTabs<DrawingTemplateDoc>({
|
|
features: readOnly ? {} : { add: true, rename: true, remove: true, reorder: true },
|
|
capture: async () => {
|
|
const got = await drawing.getDoc();
|
|
if (placing && placed(got, placing.link)) placing = null;
|
|
return { drawing: got, dirty: cadDirty };
|
|
},
|
|
show: async (tab) => {
|
|
cadDirty = false;
|
|
// 놓기는 싣기를 보낸 바로 뒤 — cad_host 가 「열린 뒤」로 미룸(먼저 보내면 싣기가 도구를 끔)
|
|
await drawing.loadSheet(
|
|
tab.drawing ?? { entities: [], layers: [BLANK_LAYER] },
|
|
tab.id === FIRST_TAB ? "" : tab.id,
|
|
numberOf(tab.id),
|
|
() => placing && drawing.placeTable(placing.entity, placing.link),
|
|
);
|
|
report();
|
|
},
|
|
create: (): CadTab<DrawingTemplateDoc> => {
|
|
const names = new Set(tabs.tabs().map((tab) => tab.name));
|
|
let n = tabs.tabs().length + 1;
|
|
while (names.has(`${n}장`)) n += 1;
|
|
const id = `s${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
return {
|
|
id,
|
|
name: `${n}장`,
|
|
drawing: { format: 6, entities: [], layers: [{ ...BLANK_LAYER }] },
|
|
};
|
|
},
|
|
onChange: () => {
|
|
listDirty = true;
|
|
retitle();
|
|
report();
|
|
},
|
|
});
|
|
const retitle = (): void => {
|
|
for (const tab of tabs.tabs()) tab.title = numberOf(tab.id);
|
|
};
|
|
tabs.set(
|
|
[
|
|
{ id: FIRST_TAB, name: FIRST_NAME, drawing: doc.도면, fixed: true },
|
|
...more.map((sheet) => ({ id: sheet.장id, name: sheet.이름, drawing: sheet.도면 })),
|
|
],
|
|
FIRST_TAB,
|
|
);
|
|
retitle();
|
|
tabs.set(tabs.tabs(), FIRST_TAB); // 풍선 글을 그림
|
|
if (readOnly && !more.length) tabs.element.hidden = true; // 볼 장이 하나 — 줄 없음
|
|
|
|
const drawing = mountDrawingTemplate(container, doc.도면, {
|
|
...options,
|
|
sheet: "",
|
|
frameNumber: doc.도번,
|
|
tabs: tabs.element,
|
|
onChanged: (dirty) => {
|
|
cadDirty = dirty;
|
|
if (dirty && !readOnly) tabs.markDirty();
|
|
report();
|
|
},
|
|
});
|
|
|
|
return {
|
|
getDoc: async () => {
|
|
const all = await tabs.collect();
|
|
const first = all.find((tab) => tab.id === FIRST_TAB);
|
|
const rest = all
|
|
.filter((tab) => tab.id !== FIRST_TAB)
|
|
.map((tab) => ({
|
|
장id: tab.id,
|
|
이름: tab.name,
|
|
도면: tab.drawing ?? { format: 6, entities: [], layers: [{ ...BLANK_LAYER }] },
|
|
}));
|
|
listDirty = false;
|
|
cadDirty = false; // 받기(requestSave)가 CAD 미저장도 내림 — 다음 고침이 다시 알림
|
|
tabs.markClean(undefined, true);
|
|
return {
|
|
도면: first?.drawing ?? doc.도면,
|
|
...(rest.length || doc.더한장 ? { 더한장: rest } : {}),
|
|
};
|
|
},
|
|
activeSheet: () => {
|
|
const id = tabs.active()?.id ?? FIRST_TAB;
|
|
return id === FIRST_TAB ? "" : id;
|
|
},
|
|
placeTable: (entity, link) => {
|
|
placing = { entity, link };
|
|
drawing.placeTable(entity, link);
|
|
},
|
|
// 실린 장은 CAD · 안 실린 장은 받아 둔 편집본에서(82-3)
|
|
updateTable: async (link, cells) => {
|
|
const now = tabs.active();
|
|
let count = 0;
|
|
for (const tab of tabs.tabs()) {
|
|
if (tab === now || !tab.drawing) continue;
|
|
const changed = updateStashedTables(tab.drawing, link, cells);
|
|
if (changed) tab.dirty = true;
|
|
count += changed;
|
|
}
|
|
if (count) report();
|
|
return count + (await drawing.updateTable(link, cells).catch(() => 0));
|
|
},
|
|
destroy: drawing.destroy,
|
|
};
|
|
}
|