refactor(M02): 웹캐드 iframe 부모 흐름 · 도각 편집을 ui_template/cad_host/ 로 뗌 · B07 이 그것을 씀 (PLAN 10-3)
- cad_host.ts — iframe 띄우기 · ready 대기 · 시간초과 · 도면 싣기 · 저장 요청 · 토스트 중계 · 콜백(편집 · 넘기기 · 내보내기) - cad_host_frame_edit.ts — B07_UI_FrameEdit 옮김 · 도각 읽고 쓰는 길을 api 로 받음 - cad_host.css — B07 CSS 의 CAD 칸 · 도각 편집 띠 규칙 옮김(값 그대로 · 클래스 이름만 cad-host · cad-frame-edit) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PxvYb5ufV1kWdBvZbpDfu6
This commit is contained in:
@@ -38,14 +38,20 @@ import {
|
||||
exportDrawing,
|
||||
fetchDesignDrawing,
|
||||
fetchDesignDrawingList,
|
||||
fetchFrameTemplate,
|
||||
importFrameTemplate,
|
||||
invalidateDesignDrawing,
|
||||
resetFrameTemplate,
|
||||
saveFrameTemplate,
|
||||
type CadDrawing,
|
||||
type DesignDrawingItem,
|
||||
type DesignDrawingResponse,
|
||||
type QuantityTable,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
import { appendStructureEntities } from "./B07_DesignDetail_UI_Cad_Structures";
|
||||
import { createFrameTemplateEditor } from "./B07_DesignDetail_UI_FrameEdit";
|
||||
// CAD iframe 부모 흐름 · 도각 편집은 M02 와 한 벌로 공용에 둔다(PLAN 10-3).
|
||||
import { createCadHost, type CadSaveResult } from "@ui/cad_host/cad_host";
|
||||
import { createFrameTemplateEditor } from "@ui/cad_host/cad_host_frame_edit";
|
||||
|
||||
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
|
||||
interface DesignMeta {
|
||||
@@ -62,33 +68,13 @@ interface DesignMeta {
|
||||
}
|
||||
|
||||
/** CAD 저장 응답 (도면 + 편집된 수량표). */
|
||||
interface SaveResult {
|
||||
drawing: CadDrawing;
|
||||
quantityTable: QuantityTable | null;
|
||||
}
|
||||
|
||||
/** B07 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const B07_CAD_APP_URL = "/b07-cad/index.html";
|
||||
type SaveResult = CadSaveResult<CadDrawing, QuantityTable>;
|
||||
|
||||
/** locale 헬퍼 */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
const CAD_LOAD_MESSAGE = "aislo:b08:load-drawing";
|
||||
const CAD_READY_MESSAGE = "aislo:b08:drawing-ready";
|
||||
const CAD_LOADED_MESSAGE = "aislo:b08:drawing-loaded";
|
||||
const CAD_ERROR_MESSAGE = "aislo:b08:drawing-error";
|
||||
const CAD_CHANGED_MESSAGE = "aislo:b08:drawing-changed";
|
||||
const CAD_SAVE_REQUEST_MESSAGE = "aislo:b08:save-request";
|
||||
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response";
|
||||
const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate";
|
||||
const CAD_EXPORT_MESSAGE = "aislo:b08:export-file";
|
||||
/** CAD 앱 알림 — 프로젝트 공용 토스트로 띄운다(2026-08-30 사용자 지시).
|
||||
* CAD 안 react-toastify는 모양·자리가 달라 한 화면에 두 종류가 섞여 보였다. */
|
||||
const CAD_TOAST_MESSAGE = "aislo:b08:toast";
|
||||
const CAD_TOAST_ACTION_MESSAGE = "aislo:b08:toast-action";
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 화면 조립
|
||||
* -------------------------------------------------------------------------- */
|
||||
@@ -114,71 +100,16 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
: "도면 목록을 불러오지 못했습니다.";
|
||||
}
|
||||
|
||||
const cadHost = document.createElement("div");
|
||||
cadHost.className = "b07-cad-host";
|
||||
const frame = document.createElement("iframe");
|
||||
frame.className = "b07-cad-frame";
|
||||
frame.src = B07_CAD_APP_URL;
|
||||
frame.title = L("B07_Design_Title");
|
||||
const license = document.createElement("a");
|
||||
license.className = "b07-cad-license";
|
||||
license.href = "/b07-cad/THIRD_PARTY_LICENSES.txt";
|
||||
license.target = "_blank";
|
||||
license.rel = "noreferrer";
|
||||
license.textContent = "Drawing engine based on OpenWebCAD · MIT License";
|
||||
cadHost.append(frame, license);
|
||||
|
||||
let cadReady = false;
|
||||
let pendingLoad:
|
||||
| {
|
||||
drawing: CadDrawing;
|
||||
meta: DesignMeta | null;
|
||||
frameEdit: boolean;
|
||||
frameFields: Record<string, string>;
|
||||
}
|
||||
| undefined;
|
||||
// ⚠ CAD 는 iframe 이라 **저쪽이 아무 말도 안 하면 화면이 영원히 「불러오는 중」에 머문다**
|
||||
// (2026-09-09 사용자 보고 — 무한 로딩). 끝을 알리는 것은 `drawing-loaded` ·
|
||||
// `drawing-error` 두 통지뿐이고, 그것이 안 오는 길이 둘 있다.
|
||||
// ① iframe 이 아예 안 뜸 — `dist/` 가 없거나 스크립트가 죽음 ⇒ `ready` 가 안 옴
|
||||
// ② 떴는데 도면을 여는 중에 멈춤 ⇒ `loaded` 도 `error` 도 안 옴
|
||||
// 아래 시계가 그 자리를 끊는다. ⚠ **화면 표시만 끊는다** — 뒤늦게 응답이 오면
|
||||
// 그대로 받아 정상으로 되돌아간다(요청을 취소하지 않는다).
|
||||
const CAD_READY_TIMEOUT_MS = 20000;
|
||||
const CAD_LOAD_TIMEOUT_MS = 15000;
|
||||
let loadWatchdog: number | undefined;
|
||||
|
||||
const failCad = (detail: string): void => {
|
||||
cadHost.dataset.loading = "false";
|
||||
cadHost.dataset.error = detail;
|
||||
showToast(detail, "error");
|
||||
};
|
||||
|
||||
const stopLoadWatchdog = (): void => {
|
||||
if (loadWatchdog === undefined) return;
|
||||
window.clearTimeout(loadWatchdog);
|
||||
loadWatchdog = undefined;
|
||||
};
|
||||
|
||||
const startLoadWatchdog = (): void => {
|
||||
stopLoadWatchdog();
|
||||
// 아직 `ready` 를 못 받았으면 iframe 이 뜨기를 기다리는 중이라 더 길게 준다.
|
||||
const wait = cadReady ? CAD_LOAD_TIMEOUT_MS : CAD_READY_TIMEOUT_MS;
|
||||
loadWatchdog = window.setTimeout(() => {
|
||||
loadWatchdog = undefined;
|
||||
if (cadHost.dataset.loading !== "true") return;
|
||||
failCad(
|
||||
cadReady
|
||||
? "CAD 가 도면을 여는 데 너무 오래 걸립니다. 다시 눌러 보세요."
|
||||
: "CAD 화면이 응답하지 않습니다. 새로고침해도 같으면 CAD 빌드(dist)를 확인하세요.",
|
||||
);
|
||||
}, wait);
|
||||
};
|
||||
|
||||
// iframe 자체가 못 뜨는 경우 — 이때는 `ready` 가 영영 안 오므로 기다릴 것 없이 끊는다.
|
||||
frame.addEventListener("error", () => {
|
||||
stopLoadWatchdog();
|
||||
failCad("CAD 화면을 불러오지 못했습니다. CAD 빌드(dist)를 확인하세요.");
|
||||
const cad = createCadHost<CadDrawing, DesignMeta, QuantityTable>({
|
||||
title: L("B07_Design_Title"),
|
||||
// 편집 통지는 **미저장 표시**만 세운다. 확정을 푸는 것은 [수정] 하나뿐이다
|
||||
// (2026-09-01 사용자 확정) — 예전에는 이 통지가 확정을 풀어, 되돌리기나 색
|
||||
// 고르기 같은 곁가지 동작에도 확정이 조용히 날아갔다.
|
||||
onChanged: (dirty) => {
|
||||
if (!frameEditor.isEditing()) cadDirty = dirty;
|
||||
},
|
||||
onNavigate: (direction) => navigateDrawing(direction),
|
||||
onExport: (fileFormat) => void exportDrawingFile(fileFormat),
|
||||
});
|
||||
|
||||
let currentDrawing: DesignDrawingItem | undefined;
|
||||
@@ -188,7 +119,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
const isCross = (item: DesignDrawingItem): boolean => item.kind === "cross";
|
||||
let allDrawingsConfirmed =
|
||||
drawings.some(isCross) && drawings.filter(isCross).every((item) => item.confirmed);
|
||||
let resolveSave: ((payload: SaveResult) => void) | undefined;
|
||||
let drawingListEl: HTMLElement | undefined;
|
||||
const infoPanelHost = document.createElement("div");
|
||||
infoPanelHost.className = "b07-info-host";
|
||||
@@ -262,23 +192,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
hasNext: index < drawings.length - 1,
|
||||
});
|
||||
|
||||
// frameEdit: 도각 편집으로 싣는 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다
|
||||
// (2026-09-06 사용자 지시로 패널을 캐드 안으로 옮김).
|
||||
const sendLoad = (
|
||||
drawing: CadDrawing,
|
||||
meta: DesignMeta | null,
|
||||
frameEdit = false,
|
||||
frameFields: Record<string, string> = {},
|
||||
) => {
|
||||
pendingLoad = { drawing, meta, frameEdit, frameFields };
|
||||
if (!cadReady) return;
|
||||
frame.contentWindow?.postMessage(
|
||||
{ type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit, frameFields },
|
||||
window.location.origin,
|
||||
);
|
||||
pendingLoad = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 도면 캐시 — 진입 직후 **목록의 모든 도면**을 배경에서 받아 둔다(2026-08-30 사용자 지시).
|
||||
*
|
||||
@@ -349,9 +262,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
highlightActive(drawing.id);
|
||||
const button = findButton(drawing.id);
|
||||
if (button) button.dataset.loading = "true";
|
||||
cadHost.dataset.loading = "true";
|
||||
cadHost.dataset.error = ""; // 앞선 실패 표시를 지운다
|
||||
startLoadWatchdog(); // 저쪽이 말이 없으면 여기서 끊는다
|
||||
cad.beginLoading(); // 저쪽이 말이 없으면 시계가 끊는다
|
||||
try {
|
||||
const response = await requestDrawing(drawing);
|
||||
currentDrawing = drawing;
|
||||
@@ -360,10 +271,9 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
cadDirty = false; // 새 도면을 실었다 — 미저장 편집은 이 도면 것이 아니다
|
||||
applyConfirmButtonState();
|
||||
updateInfoPanel(drawing, response);
|
||||
sendLoad(response.drawing, buildMeta(drawing, response, index));
|
||||
cad.load(response.drawing, buildMeta(drawing, response, index));
|
||||
} catch (error) {
|
||||
stopLoadWatchdog(); // 여기서 이미 끝났다 — 시계를 두면 늦게 또 오류를 띄운다
|
||||
failCad(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||
cad.fail(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||
if (currentDrawing) highlightActive(currentDrawing.id);
|
||||
} finally {
|
||||
loadInFlight = false;
|
||||
@@ -384,16 +294,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
void loadDrawing(drawings[target], target);
|
||||
};
|
||||
|
||||
const requestCadDrawing = (): Promise<SaveResult> =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolveSave = resolve;
|
||||
frame.contentWindow?.postMessage({ type: CAD_SAVE_REQUEST_MESSAGE }, window.location.origin);
|
||||
window.setTimeout(() => {
|
||||
if (!resolveSave) return;
|
||||
resolveSave = undefined;
|
||||
reject(new Error("CAD 저장 응답 시간이 초과되었습니다."));
|
||||
}, 5000);
|
||||
});
|
||||
const requestCadDrawing = (): Promise<SaveResult> => cad.requestSave();
|
||||
|
||||
async function confirmCurrentDrawing(): Promise<void> {
|
||||
if (!projectId || !currentDrawing) return;
|
||||
@@ -501,9 +402,15 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const frameEditor = createFrameTemplateEditor({
|
||||
projectId: projectId as string,
|
||||
sendLoad,
|
||||
const frameEditor = createFrameTemplateEditor<CadDrawing>({
|
||||
api: {
|
||||
fetch: () => fetchFrameTemplate(projectId as string),
|
||||
save: (drawing) => saveFrameTemplate(projectId as string, drawing),
|
||||
importFile: (file) => importFrameTemplate(projectId as string, file),
|
||||
reset: () => resetFrameTemplate(projectId as string),
|
||||
},
|
||||
sendLoad: (drawing, meta, frameEdit, frameFields) =>
|
||||
cad.load(drawing, meta, frameEdit, frameFields),
|
||||
requestCadDrawing: async () => (await requestCadDrawing()).drawing,
|
||||
restoreDrawing: () => {
|
||||
if (currentDrawing) void loadDrawing(currentDrawing, currentIndex);
|
||||
@@ -513,77 +420,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
currentDrawing ? { label: currentDrawing.label, number: String(currentIndex + 1) } : null,
|
||||
});
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
||||
if (event.origin !== window.location.origin || event.source !== frame.contentWindow) return;
|
||||
const message = event.data as {
|
||||
type?: string;
|
||||
detail?: string;
|
||||
drawing?: CadDrawing;
|
||||
quantityTable?: QuantityTable | null;
|
||||
direction?: "prev" | "next";
|
||||
fileFormat?: "dxf" | "dwg";
|
||||
dirty?: boolean;
|
||||
kind?: string;
|
||||
text?: string;
|
||||
actionId?: string;
|
||||
durationMs?: number;
|
||||
};
|
||||
if (message.type === CAD_TOAST_MESSAGE) {
|
||||
const kind = (["info", "success", "warning", "error"] as const).find(
|
||||
(item) => item === message.kind,
|
||||
);
|
||||
// autoClose:false로 온 안내(백업 되살리기)는 오래 띄운다 — 누를 시간을 준다.
|
||||
const duration = message.durationMs === 0 ? 15000 : (message.durationMs ?? 3000);
|
||||
const actionId = message.actionId;
|
||||
showToast(
|
||||
message.text ?? "",
|
||||
kind ?? "info",
|
||||
duration,
|
||||
actionId
|
||||
? () =>
|
||||
frame.contentWindow?.postMessage(
|
||||
{ type: CAD_TOAST_ACTION_MESSAGE, actionId },
|
||||
window.location.origin,
|
||||
)
|
||||
: undefined,
|
||||
);
|
||||
} else if (message.type === CAD_READY_MESSAGE) {
|
||||
cadReady = true;
|
||||
if (pendingLoad) {
|
||||
sendLoad(
|
||||
pendingLoad.drawing,
|
||||
pendingLoad.meta,
|
||||
pendingLoad.frameEdit,
|
||||
pendingLoad.frameFields,
|
||||
);
|
||||
// 기다리던 것이 「iframe 이 뜨기」에서 「도면이 열리기」로 바뀌었다 — 시계를 다시 건다.
|
||||
if (cadHost.dataset.loading === "true") startLoadWatchdog();
|
||||
}
|
||||
} else if (message.type === CAD_LOADED_MESSAGE) {
|
||||
stopLoadWatchdog();
|
||||
cadHost.dataset.loading = "false";
|
||||
} else if (message.type === CAD_ERROR_MESSAGE) {
|
||||
stopLoadWatchdog();
|
||||
failCad(message.detail ?? "CAD 도면을 표시하지 못했습니다.");
|
||||
} else if (message.type === CAD_CHANGED_MESSAGE) {
|
||||
// 편집 통지는 **미저장 표시**만 세운다. 확정을 푸는 것은 [수정] 하나뿐이다
|
||||
// (2026-09-01 사용자 확정) — 예전에는 이 통지가 확정을 풀어, 되돌리기나 색
|
||||
// 고르기 같은 곁가지 동작에도 확정이 조용히 날아갔다.
|
||||
if (!frameEditor.isEditing()) cadDirty = message.dirty !== false;
|
||||
} else if (message.type === CAD_NAVIGATE_MESSAGE && message.direction) {
|
||||
navigateDrawing(message.direction);
|
||||
} else if (message.type === CAD_EXPORT_MESSAGE && message.fileFormat) {
|
||||
void exportDrawingFile(message.fileFormat);
|
||||
} else if (message.type === CAD_SAVE_RESPONSE_MESSAGE && message.drawing && resolveSave) {
|
||||
const resolve = resolveSave;
|
||||
resolveSave = undefined;
|
||||
resolve({
|
||||
drawing: message.drawing,
|
||||
quantityTable: message.quantityTable ?? null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const drawingPanel = buildDrawingSidePanel(drawings, selectDrawing, drawingError, devBypass);
|
||||
drawingListEl = drawingPanel;
|
||||
const confirmActions = document.createElement("div");
|
||||
@@ -604,7 +440,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
steps: workflowSteps(),
|
||||
activeStep: 4,
|
||||
leftPanel: drawingPanel,
|
||||
mainContent: cadHost,
|
||||
mainContent: cad.element,
|
||||
stages: workflowState?.stages,
|
||||
currentStage: workflowState?.current_stage,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
@@ -624,7 +460,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
root.replaceChildren(layout.root);
|
||||
|
||||
// 페이지 진입 시 첫 도면(종단도)을 자동 선택 — 빈 CAD 화면 방지.
|
||||
// CAD가 아직 준비 전이면 sendLoad가 pendingLoad로 대기했다가 ready 시 전송한다.
|
||||
// CAD가 아직 준비 전이면 cad.load 가 붙들고 있다가 ready 때 보낸다.
|
||||
if (drawings.length > 0) {
|
||||
void loadDrawing(drawings[0], 0).then(() => {
|
||||
// 첫 장을 띄운 뒤에 나머지를 받는다 — 진입 속도를 뺏지 않는다.
|
||||
|
||||
@@ -157,76 +157,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* CAD 뷰어 호스트 (상세 페이지 영역) */
|
||||
.b07-cad-host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
/* 도면을 받는 동안 띄우는 표시. 이게 없으면 유역도처럼 2~3초 걸리는 도면에서
|
||||
화면이 멎은 것처럼 보이고, 실패해도 이전 도면이 남아 사용자가 모른다
|
||||
(2026-09-01 실측 — 코드는 data-loading을 붙이는데 받는 규칙이 없었다). */
|
||||
.b07-cad-host[data-loading="true"]::after {
|
||||
content: "도면을 불러오는 중…";
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 2px 10px rgb(0 0 0 / 18%);
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.b07-cad-host[data-error]:not([data-error=""])::before {
|
||||
content: attr(data-error);
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 70%;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-danger, #d33);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-danger, #d33);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* B07 독립형 CAD 앱 임베드 */
|
||||
.b07-cad-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.b07-cad-license {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
bottom: 7px;
|
||||
color: color-mix(in srgb, var(--color-text-muted) 55%, transparent);
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.b07-cad-license:hover {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 선택 횡단도의 지반정보/계획정보 (잠정치) */
|
||||
.b07-info-host:empty {
|
||||
display: none;
|
||||
@@ -294,40 +224,6 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 도각 편집 모드 띠 — 도면 목록 하단 액션 칸의 1행. CAD 위에 떠 있던 배치는 리본과
|
||||
겹쳐 문구가 접히고 버튼이 찌그러졌다(2026-09-02 사용자 지시로 사이드바로 옮김). */
|
||||
.b07-frame-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.b07-frame-edit[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-frame-edit__label {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b07-frame-edit__buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-frame-edit__buttons > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* 확정을 건너뛴 개발 상태 알림 — 눈에 띄되 도면 목록을 밀어내지 않게 한 줄만. */
|
||||
.b07-drawing-list__bypass {
|
||||
margin: var(--spacing-4) 0 0;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* ui_template/cad_host — 웹캐드 iframe 칸 · 도각 편집 띠 (B07 · M02 공용). */
|
||||
|
||||
/* CAD 뷰어 호스트 (상세 페이지 영역) */
|
||||
.cad-host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
/* 도면을 받는 동안 띄우는 표시. 이게 없으면 유역도처럼 2~3초 걸리는 도면에서
|
||||
화면이 멎은 것처럼 보이고, 실패해도 이전 도면이 남아 사용자가 모른다
|
||||
(2026-09-01 실측 — 코드는 data-loading을 붙이는데 받는 규칙이 없었다). */
|
||||
.cad-host[data-loading="true"]::after {
|
||||
content: "도면을 불러오는 중…";
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 2px 10px rgb(0 0 0 / 18%);
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cad-host[data-error]:not([data-error=""])::before {
|
||||
content: attr(data-error);
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 70%;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-danger, #d33);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-danger, #d33);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* B07 독립형 CAD 앱 임베드 */
|
||||
.cad-host__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.cad-host__license {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
bottom: 7px;
|
||||
color: color-mix(in srgb, var(--color-text-muted) 55%, transparent);
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cad-host__license:hover {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 도각 편집 모드 띠 — 도면 목록 하단 액션 칸의 1행. CAD 위에 떠 있던 배치는 리본과
|
||||
겹쳐 문구가 접히고 버튼이 찌그러졌다(2026-09-02 사용자 지시로 사이드바로 옮김). */
|
||||
.cad-frame-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.cad-frame-edit[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cad-frame-edit__label {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cad-frame-edit__buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.cad-frame-edit__buttons > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/* =============================================================================
|
||||
* ui_template/cad_host/cad_host.ts
|
||||
* 웹캐드(openwebcad) iframe 부모 쪽 한 벌 — B07 상세 설계 · M02 도면 양식이 함께 쓴다.
|
||||
*
|
||||
* iframe 띄우기 · ready 대기 · 시간초과 · 도면 싣기 · 저장 요청 · 토스트 중계를 맡는다.
|
||||
* 페이지마다 다른 일(도면 넘기기 · 내보내기 · 미저장 표시)은 콜백으로 받는다.
|
||||
* CAD 앱 쪽 짝은 `B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts`.
|
||||
* ========================================================================== */
|
||||
|
||||
import "./cad_host.css";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
|
||||
/** CAD 앱 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const CAD_APP_URL = "/b07-cad/index.html";
|
||||
|
||||
const CAD_LOAD_MESSAGE = "aislo:b08:load-drawing";
|
||||
const CAD_READY_MESSAGE = "aislo:b08:drawing-ready";
|
||||
const CAD_LOADED_MESSAGE = "aislo:b08:drawing-loaded";
|
||||
const CAD_ERROR_MESSAGE = "aislo:b08:drawing-error";
|
||||
const CAD_CHANGED_MESSAGE = "aislo:b08:drawing-changed";
|
||||
const CAD_SAVE_REQUEST_MESSAGE = "aislo:b08:save-request";
|
||||
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response";
|
||||
const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate";
|
||||
const CAD_EXPORT_MESSAGE = "aislo:b08:export-file";
|
||||
/** CAD 앱 알림 — 프로젝트 공용 토스트로 띄운다(2026-08-30 사용자 지시).
|
||||
* CAD 안 react-toastify는 모양·자리가 달라 한 화면에 두 종류가 섞여 보였다. */
|
||||
const CAD_TOAST_MESSAGE = "aislo:b08:toast";
|
||||
const CAD_TOAST_ACTION_MESSAGE = "aislo:b08:toast-action";
|
||||
|
||||
/** CAD 가 주고받는 도면 JSON — 도형·층 밖의 칸은 페이지마다 다르다. */
|
||||
export interface CadHostDrawing {
|
||||
entities: Record<string, unknown>[];
|
||||
layers?: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
/** CAD 저장 응답 (도면 + 수량표 — 수량표는 B07 횡단도만). */
|
||||
export interface CadSaveResult<D, Q> {
|
||||
drawing: D;
|
||||
quantityTable: Q | null;
|
||||
}
|
||||
|
||||
export interface CadHostOptions {
|
||||
/** iframe 제목(접근성). */
|
||||
title: string;
|
||||
/** CAD 편집 통지 — dirty 는 미저장 편집이 있는가. */
|
||||
onChanged?: (dirty: boolean) => void;
|
||||
/** CAD 리본의 이전·다음 도면 단추. */
|
||||
onNavigate?: (direction: "prev" | "next") => void;
|
||||
/** CAD 리본의 DXF·DWG 내보내기 단추. */
|
||||
onExport?: (fileFormat: "dxf" | "dwg") => void;
|
||||
}
|
||||
|
||||
export interface CadHost<D extends CadHostDrawing, M, Q> {
|
||||
/** iframe 과 라이선스 글을 담은 칸 — 페이지 메인 칸에 붙인다. */
|
||||
element: HTMLElement;
|
||||
/** 「불러오는 중」을 켜고 시계를 건다. 끝은 CAD 의 loaded·error 통지가 알린다. */
|
||||
beginLoading: () => void;
|
||||
/** 불러오기 실패 — 시계를 끊고 실패 표시 · 토스트. */
|
||||
fail: (detail: string) => void;
|
||||
/** 도면을 싣는다 — CAD 가 아직 안 떴으면 ready 때 보낸다.
|
||||
* frameEdit: 도각 편집으로 싣는 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다
|
||||
* (2026-09-06 사용자 지시로 패널을 캐드 안으로 옮김). */
|
||||
load: (
|
||||
drawing: D,
|
||||
meta: M | null,
|
||||
frameEdit?: boolean,
|
||||
frameFields?: Record<string, string>,
|
||||
) => void;
|
||||
/** CAD 의 지금 편집본을 받는다. */
|
||||
requestSave: () => Promise<CadSaveResult<D, Q>>;
|
||||
/** 메시지 듣기를 멈춘다 — 페이지를 떠날 때. */
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createCadHost<D extends CadHostDrawing, M = unknown, Q = unknown>(
|
||||
options: CadHostOptions,
|
||||
): CadHost<D, M, Q> {
|
||||
const element = document.createElement("div");
|
||||
element.className = "cad-host";
|
||||
const frame = document.createElement("iframe");
|
||||
frame.className = "cad-host__frame";
|
||||
frame.src = CAD_APP_URL;
|
||||
frame.title = options.title;
|
||||
const license = document.createElement("a");
|
||||
license.className = "cad-host__license";
|
||||
license.href = "/b07-cad/THIRD_PARTY_LICENSES.txt";
|
||||
license.target = "_blank";
|
||||
license.rel = "noreferrer";
|
||||
license.textContent = "Drawing engine based on OpenWebCAD · MIT License";
|
||||
element.append(frame, license);
|
||||
|
||||
let cadReady = false;
|
||||
let pendingLoad:
|
||||
| { drawing: D; meta: M | null; frameEdit: boolean; frameFields: Record<string, string> }
|
||||
| undefined;
|
||||
let resolveSave: ((payload: CadSaveResult<D, Q>) => void) | undefined;
|
||||
// ⚠ CAD 는 iframe 이라 **저쪽이 아무 말도 안 하면 화면이 영원히 「불러오는 중」에 머문다**
|
||||
// (2026-09-09 사용자 보고 — 무한 로딩). 끝을 알리는 것은 `drawing-loaded` ·
|
||||
// `drawing-error` 두 통지뿐이고, 그것이 안 오는 길이 둘 있다.
|
||||
// ① iframe 이 아예 안 뜸 — `dist/` 가 없거나 스크립트가 죽음 ⇒ `ready` 가 안 옴
|
||||
// ② 떴는데 도면을 여는 중에 멈춤 ⇒ `loaded` 도 `error` 도 안 옴
|
||||
// 아래 시계가 그 자리를 끊는다. ⚠ **화면 표시만 끊는다** — 뒤늦게 응답이 오면
|
||||
// 그대로 받아 정상으로 되돌아간다(요청을 취소하지 않는다).
|
||||
const CAD_READY_TIMEOUT_MS = 20000;
|
||||
const CAD_LOAD_TIMEOUT_MS = 15000;
|
||||
let loadWatchdog: number | undefined;
|
||||
|
||||
const showFailure = (detail: string): void => {
|
||||
element.dataset.loading = "false";
|
||||
element.dataset.error = detail;
|
||||
showToast(detail, "error");
|
||||
};
|
||||
|
||||
const stopLoadWatchdog = (): void => {
|
||||
if (loadWatchdog === undefined) return;
|
||||
window.clearTimeout(loadWatchdog);
|
||||
loadWatchdog = undefined;
|
||||
};
|
||||
|
||||
const startLoadWatchdog = (): void => {
|
||||
stopLoadWatchdog();
|
||||
// 아직 `ready` 를 못 받았으면 iframe 이 뜨기를 기다리는 중이라 더 길게 준다.
|
||||
const wait = cadReady ? CAD_LOAD_TIMEOUT_MS : CAD_READY_TIMEOUT_MS;
|
||||
loadWatchdog = window.setTimeout(() => {
|
||||
loadWatchdog = undefined;
|
||||
if (element.dataset.loading !== "true") return;
|
||||
showFailure(
|
||||
cadReady
|
||||
? "CAD 가 도면을 여는 데 너무 오래 걸립니다. 다시 눌러 보세요."
|
||||
: "CAD 화면이 응답하지 않습니다. 새로고침해도 같으면 CAD 빌드(dist)를 확인하세요.",
|
||||
);
|
||||
}, wait);
|
||||
};
|
||||
|
||||
// iframe 자체가 못 뜨는 경우 — 이때는 `ready` 가 영영 안 오므로 기다릴 것 없이 끊는다.
|
||||
frame.addEventListener("error", () => {
|
||||
stopLoadWatchdog();
|
||||
showFailure("CAD 화면을 불러오지 못했습니다. CAD 빌드(dist)를 확인하세요.");
|
||||
});
|
||||
|
||||
const post = (message: Record<string, unknown>): void => {
|
||||
frame.contentWindow?.postMessage(message, window.location.origin);
|
||||
};
|
||||
|
||||
const load: CadHost<D, M, Q>["load"] = (drawing, meta, frameEdit = false, frameFields = {}) => {
|
||||
pendingLoad = { drawing, meta, frameEdit, frameFields };
|
||||
if (!cadReady) return;
|
||||
post({ type: CAD_LOAD_MESSAGE, drawing, meta, frameEdit, frameFields });
|
||||
pendingLoad = undefined;
|
||||
};
|
||||
|
||||
const requestSave = (): Promise<CadSaveResult<D, Q>> =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolveSave = resolve;
|
||||
post({ type: CAD_SAVE_REQUEST_MESSAGE });
|
||||
window.setTimeout(() => {
|
||||
if (!resolveSave) return;
|
||||
resolveSave = undefined;
|
||||
reject(new Error("CAD 저장 응답 시간이 초과되었습니다."));
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
const onMessage = (event: MessageEvent<unknown>): void => {
|
||||
if (event.origin !== window.location.origin || event.source !== frame.contentWindow) return;
|
||||
const message = event.data as {
|
||||
type?: string;
|
||||
detail?: string;
|
||||
drawing?: D;
|
||||
quantityTable?: Q | null;
|
||||
direction?: "prev" | "next";
|
||||
fileFormat?: "dxf" | "dwg";
|
||||
dirty?: boolean;
|
||||
kind?: string;
|
||||
text?: string;
|
||||
actionId?: string;
|
||||
durationMs?: number;
|
||||
};
|
||||
if (message.type === CAD_TOAST_MESSAGE) {
|
||||
const kind = (["info", "success", "warning", "error"] as const).find(
|
||||
(item) => item === message.kind,
|
||||
);
|
||||
// autoClose:false로 온 안내(백업 되살리기)는 오래 띄운다 — 누를 시간을 준다.
|
||||
const duration = message.durationMs === 0 ? 15000 : (message.durationMs ?? 3000);
|
||||
const actionId = message.actionId;
|
||||
showToast(
|
||||
message.text ?? "",
|
||||
kind ?? "info",
|
||||
duration,
|
||||
actionId ? () => post({ type: CAD_TOAST_ACTION_MESSAGE, actionId }) : undefined,
|
||||
);
|
||||
} else if (message.type === CAD_READY_MESSAGE) {
|
||||
cadReady = true;
|
||||
if (pendingLoad) {
|
||||
load(pendingLoad.drawing, pendingLoad.meta, pendingLoad.frameEdit, pendingLoad.frameFields);
|
||||
// 기다리던 것이 「iframe 이 뜨기」에서 「도면이 열리기」로 바뀌었다 — 시계를 다시 건다.
|
||||
if (element.dataset.loading === "true") startLoadWatchdog();
|
||||
}
|
||||
} else if (message.type === CAD_LOADED_MESSAGE) {
|
||||
stopLoadWatchdog();
|
||||
element.dataset.loading = "false";
|
||||
} else if (message.type === CAD_ERROR_MESSAGE) {
|
||||
stopLoadWatchdog();
|
||||
showFailure(message.detail ?? "CAD 도면을 표시하지 못했습니다.");
|
||||
} else if (message.type === CAD_CHANGED_MESSAGE) {
|
||||
options.onChanged?.(message.dirty !== false);
|
||||
} else if (message.type === CAD_NAVIGATE_MESSAGE && message.direction) {
|
||||
options.onNavigate?.(message.direction);
|
||||
} else if (message.type === CAD_EXPORT_MESSAGE && message.fileFormat) {
|
||||
options.onExport?.(message.fileFormat);
|
||||
} else if (message.type === CAD_SAVE_RESPONSE_MESSAGE && message.drawing && resolveSave) {
|
||||
const resolve = resolveSave;
|
||||
resolveSave = undefined;
|
||||
resolve({ drawing: message.drawing, quantityTable: message.quantityTable ?? null });
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", onMessage);
|
||||
|
||||
return {
|
||||
element,
|
||||
beginLoading: () => {
|
||||
element.dataset.loading = "true";
|
||||
element.dataset.error = ""; // 앞선 실패 표시를 지운다
|
||||
startLoadWatchdog(); // 저쪽이 말이 없으면 여기서 끊는다
|
||||
},
|
||||
fail: (detail) => {
|
||||
stopLoadWatchdog(); // 여기서 이미 끝났다 — 시계를 두면 늦게 또 오류를 띄운다
|
||||
showFailure(detail);
|
||||
},
|
||||
load,
|
||||
requestSave,
|
||||
destroy: () => {
|
||||
stopLoadWatchdog();
|
||||
window.removeEventListener("message", onMessage);
|
||||
},
|
||||
};
|
||||
}
|
||||
+28
-20
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* B07 도각 편집 모드 — 캐드 화면에서 도각(양식)만 따로 열어 고치고 [완료]로 한 번에 반영한다.
|
||||
* 도각 편집 모드 — 캐드 화면에서 도각(양식)만 따로 열어 고치고 [완료]로 한 번에 반영한다.
|
||||
* B07 에서 떼어 공용으로 둔다 — 도각을 읽고 쓰는 길(`api`)만 페이지가 넘긴다.
|
||||
*
|
||||
* 정본(`resources/master_template/drawing/00_template_A1.json`)은 프로그램 기본 도각이라
|
||||
* 건드리지 않는다. 고친 도각은 **회사 도각**(`storage/{회사}/templates/`)으로 저장되고,
|
||||
@@ -9,13 +10,18 @@
|
||||
*/
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
type CadDrawing,
|
||||
fetchFrameTemplate,
|
||||
importFrameTemplate,
|
||||
resetFrameTemplate,
|
||||
saveFrameTemplate,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
import type { CadHostDrawing } from "./cad_host";
|
||||
|
||||
/** 도각을 읽고 쓰는 길 — 페이지가 자기 서버 길로 채운다. */
|
||||
export interface FrameTemplateApi<D extends CadHostDrawing> {
|
||||
/** 편집할 도각 한 장 · 고친 도각인가 · 자리표에 보여 줄 실제 값. */
|
||||
fetch: () => Promise<{ drawing: D; customized: boolean; fields?: Record<string, string> }>;
|
||||
save: (drawing: D) => Promise<void>;
|
||||
/** 외부 도각 파일(DXF·DWG)을 읽어 편집 화면에 실을 도면으로 받는다 — 아직 저장하지 않는다. */
|
||||
importFile: (file: File) => Promise<{ drawing: D; entity_count: number }>;
|
||||
/** 고친 도각을 지우고 기본 도각으로 되돌린다. */
|
||||
reset: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface FrameTemplateEditor {
|
||||
/** 도면 목록 아래에 놓는 「도각 편집」 버튼. */
|
||||
@@ -26,18 +32,18 @@ export interface FrameTemplateEditor {
|
||||
isEditing: () => boolean;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
projectId: string;
|
||||
interface Options<D extends CadHostDrawing> {
|
||||
api: FrameTemplateApi<D>;
|
||||
/** CAD에 도면을 싣는다 (meta null이면 수량 패널을 숨긴다).
|
||||
* frameEdit 을 켜면 캐드 안 자리표 패널이 함께 뜬다. */
|
||||
sendLoad: (
|
||||
drawing: CadDrawing,
|
||||
drawing: D,
|
||||
meta: null,
|
||||
frameEdit?: boolean,
|
||||
frameFields?: Record<string, string>,
|
||||
) => void;
|
||||
/** CAD에서 현재 편집본을 받아온다. */
|
||||
requestCadDrawing: () => Promise<CadDrawing>;
|
||||
requestCadDrawing: () => Promise<D>;
|
||||
/** 편집을 마친 뒤 보던 도면으로 돌아간다. */
|
||||
restoreDrawing: () => void;
|
||||
/** 도각이 바뀌었으니 받아 둔 도면 캐시를 버린다 — 안 버리면 옛 도각이 그대로 보인다. */
|
||||
@@ -46,16 +52,18 @@ interface Options {
|
||||
currentDrawingInfo: () => { label: string; number: string } | null;
|
||||
}
|
||||
|
||||
export function createFrameTemplateEditor(options: Options): FrameTemplateEditor {
|
||||
export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
options: Options<D>,
|
||||
): FrameTemplateEditor {
|
||||
let editing = false;
|
||||
// 자리표에 보여 줄 실제 값 — 도각을 열 때 서버에서 받아 캐드에 함께 넘긴다.
|
||||
let frameFields: Record<string, string> = {};
|
||||
|
||||
const banner = document.createElement("div");
|
||||
banner.className = "b07-frame-edit";
|
||||
banner.className = "cad-frame-edit";
|
||||
banner.hidden = true;
|
||||
const label = document.createElement("span");
|
||||
label.className = "b07-frame-edit__label";
|
||||
label.className = "cad-frame-edit__label";
|
||||
banner.append(label);
|
||||
|
||||
const finishButton = createButton({
|
||||
@@ -95,7 +103,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
onClick: () => leave(),
|
||||
});
|
||||
const bannerButtons = document.createElement("div");
|
||||
bannerButtons.className = "b07-frame-edit__buttons";
|
||||
bannerButtons.className = "cad-frame-edit__buttons";
|
||||
bannerButtons.append(finishButton, importButton, resetButton, cancelButton);
|
||||
banner.append(bannerButtons, fileInput);
|
||||
|
||||
@@ -118,7 +126,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
if (!file) return;
|
||||
importButton.disabled = true;
|
||||
try {
|
||||
const response = await importFrameTemplate(options.projectId, file);
|
||||
const response = await options.api.importFile(file);
|
||||
options.sendLoad(response.drawing, null, true, frameFields);
|
||||
label.textContent = `${file.name} 을(를) 불러왔습니다 — 자리표를 놓고 [완료]를 누르십시오.`;
|
||||
showToast(`도형 ${response.entity_count}개를 불러왔습니다.`, "success");
|
||||
@@ -134,7 +142,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
|
||||
async function enter(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchFrameTemplate(options.projectId);
|
||||
const response = await options.api.fetch();
|
||||
// 도면명·도면번호는 도면마다 달라 서버가 담지 않는다 — 보던 도면 값을 견본으로 얹는다.
|
||||
const info = options.currentDrawingInfo();
|
||||
frameFields = {
|
||||
@@ -163,7 +171,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
return;
|
||||
resetButton.disabled = true;
|
||||
try {
|
||||
await resetFrameTemplate(options.projectId);
|
||||
await options.api.reset();
|
||||
options.onSaved();
|
||||
showToast("기본 도각으로 되돌렸습니다.", "success");
|
||||
leave();
|
||||
@@ -182,7 +190,7 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
finishButton.disabled = true;
|
||||
try {
|
||||
const drawing = await options.requestCadDrawing();
|
||||
await saveFrameTemplate(options.projectId, drawing);
|
||||
await options.api.save(drawing);
|
||||
options.onSaved();
|
||||
showToast("도각을 저장했습니다. 확정하지 않은 도면부터 새 도각으로 나옵니다.", "success");
|
||||
leave();
|
||||
Reference in New Issue
Block a user