Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_Page.ts
T
eomsangdonandClaude Opus 5.5 659baa7751 feat(484 B07 설정): 좌측 ⚙ 설정 모달 · [현재 도면 확정] · [수정] 지움 · 저장 후 이동(481-3) · 탭 줄 겹판 자리
- 공용 createPanelCard 곁값 onSettings — 캐럿 왼쪽 ⚙ · 누름이 접힘 안 일으킴(B08 · B09 그대로)
- 평면도 ⚙ = 평면도 관리 모달([평면도 관리] 단추 지움) · 횡단도 ⚙ = 배치 칸 · 표 배율 · 안내선(카드 밑 칸 지움) · 구조물 ⚙ = 「M02 그림 — 축척 없음」 · 종단 1장 · 토적 = 단추 안 ⚙ · 표지 없음
- ⚙ 머리 [원본에서 다시 그리기] — 그 묶음 도면을 연 때만 · 저장본이면 invalidate
- 잠금 없음(요구 281) — 읽기 전용 안 묶음 · 자동 B08 이동 · 「모든 횡단 확정」 잠금 지움
- onStepClick 한 길 = 고친 탭(●) + 지금 탭 저장 → B08 · B09 면 단계 4 완료(새 POST design-drawings-complete) → 이동 · 실패 = 머묾
- 저장 · 원본 묶음 = 새 B07_DesignDetail_UI_Save.ts · 모달 = 새 B07_DesignDetail_UI_Settings.ts(UI_Page 633 → 543줄)
- 탭 줄 = .cad-host 의 --cad-tab-slot-left/right(px · CSS left · right 꼴) 있으면 리본 빈 자리에 겹침 · 없으면 지금 자리
- 시험: 새 test_280_5_b07_settings · 130 · 153-1 · 180-3 · 212-3 · 269 · 311 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:13:04 +09:00

540 lines
24 KiB
TypeScript

/* =============================================================================
* B07_DesignDetail_UI_Page.ts
* 로그인 후 07: 4차 워크플로우 (상세 설계) — 독립형 2D CAD 임베드
*
* B07_DesignDetail/openwebcad를 프로젝트 소유 B07 CAD 앱으로 빌드하여
* /b07-cad 경로로 서빙한다. 업무 도면은 추후 same-origin postMessage로
* JSON만 전달하며 DXF/DWG 파싱은 이 브라우저 앱에서 수행하지 않는다.
*
* 레이아웃 (사용자 지시): 사이드 패널 빈 상태 유지 + 상세 영역 CAD 화면.
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowLayout 재사용.
* ========================================================================== */
import "./B07_DesignDetail_UI_Style.css";
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
// 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04).
import {
buildDesignInfoPanel,
buildDrawingSidePanel,
foldedGroups,
isCrossSheet,
} from "./B07_DesignDetail_UI_Panels";
import { workflowSteps } from "../A00_Common/b_page_scaffold";
import {
fetchWorkflowState,
goToWorkflowStage,
WORKFLOW_STEP_ROUTES,
type WorkflowState,
} from "../A00_Common/b_workflow_nav";
import {
completeDesignStage,
exportDrawing,
fetchCrossLayout,
fetchDesignDrawing,
fetchDesignDrawingList,
fetchFrameTemplate,
importFrameTemplate,
resetFrameTemplate,
saveCrossLayout,
saveFrameTemplate,
type CadDrawing,
type CrossLayout,
type DesignDrawingItem,
type DesignDrawingResponse,
type QuantityTable,
} from "./B07_DesignDetail_Api_Fetch";
import { appendStructureEntities } from "./B07_DesignDetail_UI_Cad_Structures";
import { buildGuideToggle, guideShown } from "./B07_DesignDetail_UI_GuideToggle";
import { hasRedrawable, redrawFromSource, saveEditedDrawings } from "./B07_DesignDetail_UI_Save";
import { openDrawingSettings } from "./B07_DesignDetail_UI_Settings";
// 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";
import { createCadTabs, type CadTab } from "@ui/cad_host/cad_host_tabs";
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
interface DesignMeta {
kind: DesignDrawingItem["kind"];
/** 도면 식별자 — CAD가 자동백업 칸을 도면별로 나누는 데 쓴다. */
drawingId: string;
title: string;
info: string;
/** 참이면 CAD 읽기 전용 — 잠금 없음(요구 281)이라 늘 거짓 · 저장본도 그대로 고침. */
confirmed: boolean;
quantityTable: QuantityTable | null;
hasPrev: boolean;
hasNext: boolean;
}
/** CAD 저장 응답 (도면 + 편집된 수량표). */
type SaveResult = CadSaveResult<CadDrawing, QuantityTable>;
/** locale 헬퍼 */
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
/* -----------------------------------------------------------------------------
* 화면 조립
* -------------------------------------------------------------------------- */
/** [평면도 관리]에서 고쳐 화면을 다시 세울 때 — 보던 장 id. 다시 세운 뒤 그 장을 연다(없으면 첫 장). */
let reopenDrawingId: string | undefined;
/** 횡단 장 배치를 바꿔 다시 세울 때 — 보던 장 번호(배치가 바뀌면 장 id 가 바뀜 · 140-3). */
let reopenCrossSheet: number | undefined;
export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
// 311 다시 세우면 옛 목록 접힘 그대로 — 기다리기 전에 읽음(처음 들어오면 B07 목록이 없어 기본값)
const folded = foldedGroups(root);
let workflowState: WorkflowState | undefined;
let drawings: DesignDrawingItem[] = [];
let drawingError: string | undefined;
let devBypass = false;
if (projectId) {
const [workflowResult, drawingResult] = await Promise.allSettled([
fetchWorkflowState(projectId),
fetchDesignDrawingList(projectId),
]);
if (workflowResult.status === "fulfilled") workflowState = workflowResult.value;
if (drawingResult.status === "fulfilled") {
drawings = drawingResult.value.drawings;
devBypass = drawingResult.value.dev_bypass === true;
} else
drawingError =
drawingResult.reason instanceof Error
? drawingResult.reason.message
: "도면 목록을 불러오지 못했습니다.";
}
const cad = createCadHost<CadDrawing, DesignMeta, QuantityTable>({
title: L("B07_Design_Title"),
// 편집 통지는 **미저장 표시**(탭 ●)만 세운다 — 저장은 페이지를 옮길 때(요구 281).
onChanged: (dirty) => {
if (frameEditor.isEditing()) return;
cadDirty = dirty;
if (dirty) tabs.markDirty();
},
onNavigate: (direction) => navigateDrawing(direction),
onExport: (fileFormat) => void exportDrawingFile(fileFormat),
onGuideChange: (area) => frameEditor.guideChanged(area),
});
// 180-3 안내선 보이기 / 숨기기 — 숨김은 화면을 다시 세워도 이어 줌(`…_UI_GuideToggle` 모듈 값)
if (!guideShown()) cad.setGuideVisible(false);
const guideToggle = buildGuideToggle((visible) => cad.setGuideVisible(visible));
let currentDrawing: DesignDrawingItem | undefined;
let currentIndex = -1;
let drawingListEl: HTMLElement | undefined;
const infoPanelHost = document.createElement("div");
infoPanelHost.className = "b07-info-host";
// 표준도 제원 칸·[여기서 고치기]는 2026-09-13 B08 「구조물도」 탭으로 이관(PLAN 3장 ④).
const updateInfoPanel = (drawing: DesignDrawingItem, response: DesignDrawingResponse): void => {
guideToggle.disabled = !isCrossSheet(drawing); // 장의 안내선만
if (drawing.kind !== "cross") {
infoPanelHost.replaceChildren();
return;
}
// 장은 id 가 `cross_s…`(여러 측점 묶음), 측점 도면은 `cross_…m` 이다. `chainage_m` 은
// 장에도 첫 측점 값이 실려 오므로 그것으로는 갈리지 않는다.
const scope = isCrossSheet(drawing) ? "sheet" : "station";
infoPanelHost.replaceChildren(
buildDesignInfoPanel(drawing.label, response.design ?? null, scope),
);
};
let crossLayoutRequest: Promise<CrossLayout> | undefined;
const crossLayout = (): Promise<CrossLayout> =>
(crossLayoutRequest ??= fetchCrossLayout(projectId as string)).catch((error) => {
crossLayoutRequest = undefined; // 실패는 남기지 않음 — 다음 ⚙ 에 다시 받음
throw error;
});
/** 장 배치 저장(140-3 · ⚙ 횡단 모달) — 서버 쓰기 → 화면 다시 세움 · 같은 장 번호를 다시 엶. */
const saveCrossLayoutAndRebuild = async (next: CrossLayout, sheet: number) => {
if (!projectId || !mayDiscardEdits()) return;
showLoadingOverlay();
try {
await saveCrossLayout(projectId, next);
reopenCrossSheet = sheet || undefined;
reopenDrawingId = sheet ? undefined : currentDrawing?.id;
cad.destroy();
void renderB07DesignDetail(root);
} catch (error) {
showToast(error instanceof Error ? error.message : "배치를 저장하지 못했습니다.", "error");
} finally {
hideLoadingOverlay();
}
};
/**
* CAD에 저장하지 않은 편집이 있는가. CAD가 편집마다 알려 주고, 도면을 싣거나
* 저장하면 내려간다. 이 값이 참인 채로 도면을 바꾸면 그은 선이 소리 없이 사라진다
* (2026-09-01 실측) — 그래서 바꾸기 전에 묻는다.
*/
let cadDirty = false;
const findButton = (drawingId: string) =>
drawingListEl?.querySelector<HTMLButtonElement>(
`.b07-drawing-button[data-drawing-id="${drawingId}"]`,
) ?? undefined;
const highlightActive = (drawingId: string) => {
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b07-drawing-button").forEach((item) => {
item.dataset.active = String(item.dataset.drawingId === drawingId);
});
};
const buildMeta = (
drawing: DesignDrawingItem,
response: DesignDrawingResponse,
index: number,
): DesignMeta => ({
kind: drawing.kind,
drawingId: drawing.id,
title: drawing.label,
info: drawing.kind === "cross" ? drawing.label : "",
confirmed: false, // 잠금 없음(요구 281)
quantityTable: response.quantity_table ?? null,
hasPrev: index > 0,
hasNext: index < drawings.length - 1,
});
/**
* 도면 캐시 — 진입 직후 **목록의 모든 도면**을 배경에서 받아 둔다(2026-08-30 사용자 지시).
*
* 서버가 도면을 요청마다 새로 조립해(측점 설계선·장 배치·표) 클릭 한 번에 0.3~0.6초,
* 구조물 얹기까지 더하면 0.8~1.0초가 걸렸다. 미리 받아 두면 클릭은 캐시에서 즉시 꺼낸다.
* sessionStorage가 아니라 **모듈 메모리**에 둔다 — 유역도 한 장이 엔티티 1,581개라
* 세션 저장 한도(5MB)를 넘길 위험이 있고, 새로고침이면 정본에서 다시 받는 편이 맞다.
*/
const drawingCache = new Map<string, Promise<DesignDrawingResponse>>();
/** 도면 하나를 받아 구조물까지 얹은 응답. 같은 id로 겹쳐 부르면 같은 Promise를 쓴다. */
const requestDrawing = (drawing: DesignDrawingItem): Promise<DesignDrawingResponse> => {
const cached = drawingCache.get(drawing.id);
if (cached) return cached;
const request = (async () => {
const response = await fetchDesignDrawing(projectId as string, drawing.id);
// 구조물(배수관·기슭막이·세월교·BOX·물넘이포장)은 B06 산식이 프론트에 있어
// 여기서 얹는다. 확정본은 이미 구조물이 담겨 저장돼 있으므로 건드리지 않는다.
if (drawing.kind === "cross" && !response.confirmed) {
await appendStructureEntities(projectId as string, response.route_id, response.drawing);
}
return response;
})().catch((error) => {
drawingCache.delete(drawing.id); // 실패는 캐시에 남기지 않는다 — 다음 클릭에 다시 받는다.
throw error;
});
drawingCache.set(drawing.id, request);
return request;
};
/** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다.
*
* ⚠ **사용자가 누른 도면에 자리를 내준다** (2026-09-09 — 무한 로딩으로 보이던 자리).
* 도면이 31 장이고 한 장에 0.3~1.0초라, 미리 받기가 도는 동안 클릭하면 그 요청이
* **앞의 여러 장 뒤에 줄을 서서** 30초 넘게 안 열렸다. 화면은 「불러오는 중」 그대로였다.
* 한 장 받을 때마다 **클릭이 끼어들 틈**을 주고, 클릭이 도는 동안은 쉬어 준다.
*/
const prefetchAllDrawings = async (): Promise<void> => {
if (!projectId) return;
for (const item of drawings) {
// 클릭이 도는 중이면 끝날 때까지 기다린다 — 미리 받기는 급하지 않다.
while (loadInFlight) await new Promise((done) => window.setTimeout(done, 120));
if (drawingCache.has(item.id)) continue; // 클릭이 이미 받아 둔 장은 건너뛴다
try {
await requestDrawing(item);
} catch {
/* 한 장을 못 받아도 나머지는 계속 받는다 — 클릭 때 다시 시도한다. */
}
}
};
/** 저장하지 않은 편집이 있으면 묻는다. 버리기로 해야 이동한다. */
const mayDiscardEdits = (): boolean => {
if (!cadDirty && !tabs.anyDirty()) return true;
return window.confirm(
"저장하지 않은 편집이 있습니다.\n" +
"지금 도면을 바꾸면 편집이 사라집니다. 버리고 이동할까요?\n\n" +
"남기려면 [취소] — 고친 도면은 페이지를 옮길 때([<] [>]) 저장됩니다.",
);
};
/** 도면 전환 중인가 — 겹쳐 누르면 늦게 온 응답이 화면을 덮는다. */
let loadInFlight = false;
/** `stash` = 탭에 받아 둔 저장 안 한 편집본(82-4) — 있으면 그것을 싣고 미저장 그대로 */
const loadDrawing = async (drawing: DesignDrawingItem, index: number, stash?: CadDrawing) => {
if (!projectId || loadInFlight) return;
loadInFlight = true;
highlightActive(drawing.id);
const button = findButton(drawing.id);
if (button) button.dataset.loading = "true";
cad.beginLoading(); // 저쪽이 말이 없으면 시계가 끊는다
try {
const response = await requestDrawing(drawing);
currentDrawing = drawing;
currentIndex = index;
cadDirty = !!stash; // 새 도면을 실었다 — 미저장은 받아 둔 편집본일 때만
updateInfoPanel(drawing, response);
// 가이드 = 작도 영역 점선 · 넘친 칸 붉은 틀(169-1 · 169-2 · 보기만)
const guide = response.guide ?? null;
const meta = buildMeta(drawing, response, index);
cad.load(stash ?? response.drawing, meta, false, {}, { guide });
if (guideShown() && guide?.marks?.length) {
showToast(`${guide.marks.length} 칸 넘침 — 열 · 행을 줄일 것`, "warning");
}
if (response.drawing.stale_layout) {
showToast(
"옛 배치로 저장된 장입니다. ⚙ [원본에서 다시 그리기]로 표를 칸 아래 가운데에 맞춘 새 배치로 다시 만듭니다.",
"info",
);
} else if (response.drawing.stale_scale) {
showToast(
"옛 표 배율로 저장된 장입니다. ⚙ [원본에서 다시 그리기]로 지금 배율로 다시 만듭니다.",
"info",
);
} else if (response.drawing.stale_layers) {
showToast(
"측점 도면층 앞에 저장된 장입니다. ⚙ [원본에서 다시 그리기]로 측점마다 층(횡단 No.)으로 다시 만듭니다.",
"info",
);
}
} catch (error) {
cad.fail(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
if (currentDrawing) highlightActive(currentDrawing.id);
} finally {
loadInFlight = false;
if (button) button.dataset.loading = "false";
}
};
/**
* 목록에서 연 도면 = 탭(82-4) — 탭을 바꿀 때 지금 도면을 받아 두고(저장 안 한 편집 그대로) 다음 장을 싣는다.
* 새 장 · 지우기는 없음(서버 길 없음) · 닫기는 미저장 탭만 물음 · 마지막 한 탭은 못 닫음.
*/
const tabs = createCadTabs<CadDrawing>({
features: { close: true },
capture: async () => ({ drawing: (await requestCadDrawing()).drawing, dirty: cadDirty }),
canLeave: () => {
if (!frameEditor.isEditing()) return !loadInFlight;
showToast("도각 편집을 마친 뒤 다른 도면으로 옮기세요.", "warning");
return false;
},
show: async (tab: CadTab<CadDrawing>) => {
const index = drawings.findIndex((item) => item.id === tab.id);
if (index >= 0)
await loadDrawing(drawings[index], index, tab.dirty ? tab.drawing : undefined);
},
});
const openTab = (drawing: DesignDrawingItem): Promise<boolean> =>
tabs.open({ id: drawing.id, name: drawing.label, title: drawing.label });
const selectDrawing = (drawing: DesignDrawingItem) => {
if (drawing === currentDrawing) return;
void openTab(drawing);
};
const navigateDrawing = (direction: "prev" | "next") => {
if (currentIndex < 0) return;
const target = direction === "prev" ? currentIndex - 1 : currentIndex + 1;
if (target < 0 || target >= drawings.length) return;
void openTab(drawings[target]);
};
const requestCadDrawing = (): Promise<SaveResult> => cad.requestSave();
/** 저장 · 원본 되돌림 묶음(`_UI_Save`)이 쓰는 상태 길. */
const store = {
projectId: projectId as string,
drawings,
tabs,
current: () => currentDrawing,
cadDirty: () => cadDirty,
setCadClean: () => void (cadDirty = false),
requestCad: requestCadDrawing,
isFrameEditing: () => frameEditor.isEditing(),
forget: (id: string) => void drawingCache.delete(id),
reload: (drawing: DesignDrawingItem) => loadDrawing(drawing, drawings.indexOf(drawing)),
};
/**
* 지금 보고 있는 도면을 DXF·DWG 파일로 내려받는다 (2026-09-06 사용자 지시).
* 파일 만들기는 서버가 한다 — 캐드는 도면만 넘긴다.
*/
async function exportDrawingFile(fileFormat: "dxf" | "dwg"): Promise<void> {
showLoadingOverlay();
try {
const { drawing } = await requestCadDrawing();
const name = currentDrawing?.label ?? "도면";
const result = await exportDrawing(projectId as string, drawing, fileFormat, name);
const link = document.createElement("a");
link.href = URL.createObjectURL(result.blob);
link.download = `${name}.${fileFormat}`;
link.click();
URL.revokeObjectURL(link.href);
showToast(
result.skipped > 0
? `${fileFormat.toUpperCase()} 로 내보냈습니다. 그림 ${result.skipped}개는 담기지 않았습니다.`
: `${fileFormat.toUpperCase()} 로 내보냈습니다.`,
"success",
);
} catch (error) {
showToast(error instanceof Error ? error.message : "도면을 내보내지 못했습니다.", "error");
} finally {
hideLoadingOverlay();
}
}
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, extra) =>
cad.load(drawing, meta, frameEdit, frameFields, extra),
setGuideArea: (area) => cad.setGuideArea(area),
requestCadDrawing: async () => (await requestCadDrawing()).drawing,
restoreDrawing: () => {
if (currentDrawing) void loadDrawing(currentDrawing, currentIndex);
},
onSaved: () => drawingCache.clear(),
currentDrawingInfo: () =>
currentDrawing ? { label: currentDrawing.label, number: String(currentIndex + 1) } : null,
});
/** [평면도 관리] 모달을 닫음 — 고쳤으면 목록 · 도면을 서버에서 다시 받아 화면을 다시 세운다. */
const onPlanClosed = (changed: boolean, openId?: string): void => {
if (!changed) {
const target = drawings.find((item) => item.id === openId);
if (target) selectDrawing(target);
return;
}
if (!mayDiscardEdits()) return;
reopenDrawingId = openId ?? currentDrawing?.id;
cad.destroy();
void renderB07DesignDetail(root);
};
// 묶음 ⚙(280-5) — 평면도 = 평면도 관리 · 횡단도 = 장 배치 · 안내선 · 머리 [원본에서 다시 그리기]
const openSettings = (kind: DesignDrawingItem["kind"]): void =>
openDrawingSettings(kind, {
drawings,
current: () => currentDrawing,
canRedraw: () => hasRedrawable(store),
redraw: () => redrawFromSource(store),
plan: projectId ? { projectId, onClosed: onPlanClosed } : undefined,
cross: {
layout: crossLayout,
save: (next, sheet) => void saveCrossLayoutAndRebuild(next, sheet),
guideToggle,
},
});
const drawingPanel = buildDrawingSidePanel(
drawings,
selectDrawing,
drawingError,
devBypass,
projectId ? openSettings : undefined,
folded,
);
drawingListEl = drawingPanel;
const actions = document.createElement("div");
// 하단 고정은 공용 ui-sidebar-actions로 통일 — 사이드 본문이 [스크롤 영역][액션 줄]로
// 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B07 공통).
actions.className = "b07-drawing-actions ui-sidebar-actions";
// 액션 칸은 두 줄이다 — 1행 도각 편집 띠(평소 숨김), 2행 [도각 편집]
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김 · 확정 단추는 281 에 지움).
const actionRow = document.createElement("div");
actionRow.className = "b07-drawing-actions__row";
actionRow.dataset.helpRow = "";
actionRow.append(frameEditor.button);
actions.append(frameEditor.banner, actionRow);
drawingPanel.append(infoPanelHost, actions);
const cadStack = document.createElement("div");
cadStack.className = "b07-cad-stack";
cadStack.append(tabs.element, cad.element);
// 탭 줄 겹판(280-6) — CAD 가 리본 [출력] 오른쪽 빈 자리를 `.cad-host` 에 `--cad-tab-slot-left/right`
// (px · CSS left · right 꼴)로 적으면 그 자리에 겹침 · 값이 없으면 CAD 위 지금 자리.
const placeTabs = (): void => {
const slotted = cad.element.style.getPropertyValue("--cad-tab-slot-left") !== "";
if (slotted === (tabs.element.parentElement === cad.element)) return;
if (slotted) cad.element.append(tabs.element);
else cadStack.prepend(tabs.element);
};
new MutationObserver(placeTabs).observe(cad.element, {
attributes: true,
attributeFilter: ["style"],
});
placeTabs();
/** 페이지 이동(281) — 고친 도면 저장 → B08 · B09 면 단계 4 완료 → 이동 · 실패면 머묾. */
let moving = false;
const moveToStage = async (stepIndex: number): Promise<void> => {
if (!projectId || moving) return;
moving = true;
try {
if (!(await saveEditedDrawings(store))) return;
if (stepIndex > 4 && drawings.length > 0) {
try {
await completeDesignStage(projectId);
} catch (error) {
showToast(
error instanceof Error ? error.message : "상세 설계 단계를 완료하지 못했습니다.",
"error",
);
return;
}
}
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
} finally {
moving = false;
}
};
const layout = createWorkflowLayout({
title: L("B07_Design_Title"),
steps: workflowSteps(),
activeStep: 4,
leftPanel: drawingPanel,
mainContent: cadStack,
stages: workflowState?.stages,
// B08 = 옮길 때 단계 4 를 완료하므로 운영에서 막힌 상태여도 켬(281) — 공용 판은 이 단계를 켬
currentStage: 5,
routes: WORKFLOW_STEP_ROUTES,
// ☰ · 진행 단계 판 · [<] [>] 한 길 — 저장 후 이동(옛 「모든 횡단 확정」 잠금 · 자동 B08 이동 없음)
onStepClick: (stepIndex) => void moveToStage(stepIndex),
});
layout.root.classList.add("b07-design-layout");
root.replaceChildren(layout.root);
// 페이지 진입 시 첫 도면(표지 · 311)을 자동 선택 — 빈 CAD 화면 방지.
// CAD가 아직 준비 전이면 cad.load 가 붙들고 있다가 ready 때 보낸다.
if (drawings.length > 0) {
const crossSheets = drawings.filter(isCrossSheet);
const reopenId = reopenCrossSheet
? crossSheets[Math.min(reopenCrossSheet, crossSheets.length) - 1]?.id
: reopenDrawingId;
const reopen = drawings.findIndex((item) => item.id === reopenId);
reopenDrawingId = undefined;
reopenCrossSheet = undefined;
// 311 기억한 장이 없으면 표지
const cover = drawings.findIndex((item) => item.kind === "cover");
const first = reopen >= 0 ? reopen : Math.max(cover, 0);
void openTab(drawings[first]).then(() => {
// 첫 장을 띄운 뒤에 나머지를 받는다 — 진입 속도를 뺏지 않는다.
void prefetchAllDrawings();
});
}
}