Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
211 lines
7.7 KiB
TypeScript
211 lines
7.7 KiB
TypeScript
/* =============================================================================
|
||
* B07_DesignDetail_UI_PlanManage.ts
|
||
* [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음.
|
||
*
|
||
* 왼쪽 = 종류 목록(× 지우기) · 종류 추가 · 업데이트 폼 · 대피소 · 사토장 자리 · 평면도 장 목록
|
||
* 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 장 바꾸기) · [확정 풀기] · [CAD 에서 열기]
|
||
* 고치기는 지금 서버 길 그대로(종류 파일 · 확정 풀기) — 고칠 때마다 모달 안을 다시 받고,
|
||
* 닫을 때 `onClosed(changed)` 로 좌측 목록 · 도면을 다시 읽음.
|
||
* ========================================================================== */
|
||
|
||
import { showToast } from "@ui/ui_template_elements";
|
||
import { openModal } from "@ui/ui_template_modal";
|
||
import { createCadHost } from "@ui/cad_host/cad_host";
|
||
import {
|
||
deletePlanKind,
|
||
fetchDesignDrawing,
|
||
fetchDesignDrawingList,
|
||
fetchPlanKinds,
|
||
invalidateDesignDrawing,
|
||
type CadDrawing,
|
||
type DesignDrawingItem,
|
||
type PlanKindsState,
|
||
} from "./B07_DesignDetail_Api_Fetch";
|
||
import {
|
||
groupPlanSheets,
|
||
kindForm,
|
||
marksForm,
|
||
run,
|
||
type PlanKindsControls,
|
||
} from "./B07_DesignDetail_UI_PlanKinds";
|
||
|
||
export interface PlanManageControls {
|
||
projectId: string;
|
||
/** 모달을 닫음 — changed = 종류 · 자리 · 확정을 고쳤나 · openId = [CAD 에서 열기]로 고른 장. */
|
||
onClosed: (changed: boolean, openId?: string) => void;
|
||
}
|
||
|
||
function el<K extends keyof HTMLElementTagNameMap>(
|
||
tag: K,
|
||
className: string,
|
||
text = "",
|
||
): HTMLElementTagNameMap[K] {
|
||
const node = document.createElement(tag);
|
||
node.className = className;
|
||
if (text) node.textContent = text;
|
||
return node;
|
||
}
|
||
|
||
function button(className: string, text: string, onClick: () => void): HTMLButtonElement {
|
||
const node = el("button", className, text);
|
||
node.type = "button";
|
||
node.addEventListener("click", onClick);
|
||
return node;
|
||
}
|
||
|
||
export function openPlanManager(controls: PlanManageControls): void {
|
||
const { projectId } = controls;
|
||
let changed = false;
|
||
let closed = false;
|
||
/** 미리보기 중인 장 — 늦게 온 응답이 다른 장을 덮지 않게 이것과 맞춰 봄. */
|
||
let picked: string | undefined;
|
||
const preview = createCadHost<CadDrawing>({ title: "평면도 미리보기" });
|
||
const side = el("div", "b07-plan-manage__side");
|
||
const head = el("div", "b07-plan-manage__head");
|
||
const view = el("div", "b07-plan-manage__view");
|
||
view.append(head, preview.element);
|
||
|
||
// 종류 · 자리 폼은 좌측에 있던 것 그대로 — 고친 뒤 모달 안만 다시 세움(화면은 닫을 때).
|
||
const edits: PlanKindsControls = {
|
||
projectId,
|
||
onChanged: () => {
|
||
changed = true;
|
||
void refresh();
|
||
},
|
||
};
|
||
|
||
const kindList = (state: PlanKindsState, sheets: DesignDrawingItem[]): HTMLElement => {
|
||
const list = el("div", "b07-plan-tools__block b07-plan-manage__kinds");
|
||
list.append(el("strong", "", "종류 목록"));
|
||
for (const kind of state.kinds) {
|
||
const row = el("div", "b07-plan-kind__head");
|
||
row.dataset.planKind = kind.id;
|
||
const count = sheets.filter((item) => item.plan_kind === kind.id).length;
|
||
row.append(el("span", "b07-plan-kind__name", `${kind.name} · ${count}장`));
|
||
const remove = button("b07-plan-tools__remove", "×", () => {
|
||
if (
|
||
!window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`)
|
||
)
|
||
return;
|
||
void run(() => deletePlanKind(projectId, kind.id), edits);
|
||
});
|
||
remove.title = `「${kind.name}」 종류 지우기`;
|
||
row.append(remove);
|
||
list.append(row);
|
||
}
|
||
return list;
|
||
};
|
||
|
||
const sheetList = (sheets: DesignDrawingItem[]): HTMLElement => {
|
||
const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets");
|
||
list.append(el("strong", "", "평면도 장"));
|
||
for (const group of groupPlanSheets(sheets)) {
|
||
for (const { drawing, label } of group.sheets) {
|
||
const item = button("b07-drawing-button", label, () => void show(drawing, label));
|
||
item.dataset.drawingId = drawing.id;
|
||
item.dataset.confirmed = String(drawing.confirmed);
|
||
list.append(item);
|
||
}
|
||
}
|
||
return list;
|
||
};
|
||
|
||
const markActive = (): void =>
|
||
side
|
||
.querySelectorAll<HTMLElement>(".b07-plan-manage__sheets .b07-drawing-button")
|
||
.forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked)));
|
||
|
||
/** 장 하나를 미리보기에 — 보기 전용 · 확정이면 [확정 풀기](지금 [수정]과 같은 서버 길). */
|
||
const show = async (drawing: DesignDrawingItem, label: string): Promise<void> => {
|
||
picked = drawing.id;
|
||
markActive();
|
||
head.replaceChildren(el("strong", "", label));
|
||
preview.beginLoading();
|
||
try {
|
||
const response = await fetchDesignDrawing(projectId, drawing.id);
|
||
if (closed || picked !== drawing.id) return;
|
||
const badge = el("span", "b07-plan-manage__badge", response.confirmed ? "확정" : "확정 전");
|
||
badge.dataset.confirmed = String(response.confirmed);
|
||
head.append(badge);
|
||
if (response.confirmed)
|
||
head.append(
|
||
button("b07-plan-tools__button", "확정 풀기", () => {
|
||
void run(() => invalidateDesignDrawing(projectId, drawing.id), edits);
|
||
}),
|
||
);
|
||
head.append(button("b07-plan-tools__button", "CAD 에서 열기", () => finish(drawing.id)));
|
||
preview.load(
|
||
response.drawing,
|
||
null,
|
||
false,
|
||
{},
|
||
{
|
||
recoveryScope: "b07-plan-preview",
|
||
readOnly: true,
|
||
viewOnly: true,
|
||
hostTitle: "평면도 미리보기",
|
||
ribbonCollapsed: true,
|
||
panelsCollapsed: true,
|
||
},
|
||
);
|
||
} catch (error) {
|
||
if (closed || picked !== drawing.id) return;
|
||
preview.fail(error instanceof Error ? error.message : "평면도를 불러오지 못했습니다.");
|
||
}
|
||
};
|
||
|
||
/** 종류 · 장 목록을 서버에서 다시 받아 왼쪽을 세우고 보던 장(없으면 첫 장)을 다시 봄. */
|
||
const refresh = async (): Promise<void> => {
|
||
try {
|
||
const [state, list] = await Promise.all([
|
||
fetchPlanKinds(projectId),
|
||
fetchDesignDrawingList(projectId),
|
||
]);
|
||
if (closed) return;
|
||
const sheets = list.drawings.filter((item) => item.kind === "plan");
|
||
side.replaceChildren(
|
||
kindList(state, sheets),
|
||
kindForm(state, edits),
|
||
marksForm(state, edits),
|
||
sheetList(sheets),
|
||
);
|
||
const all = groupPlanSheets(sheets).flatMap((group) => group.sheets);
|
||
const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0];
|
||
if (keep) void show(keep.drawing, keep.label);
|
||
else {
|
||
picked = undefined;
|
||
head.replaceChildren(el("span", "b07-plan-kind__name", "평면도 장이 없습니다."));
|
||
}
|
||
} catch (error) {
|
||
showToast(error instanceof Error ? error.message : "평면도 목록을 받지 못했습니다.", "error");
|
||
}
|
||
};
|
||
|
||
let closeModal = (): void => undefined;
|
||
/** 한 번만 — 미리보기 듣기를 멈추고 페이지에 알림. */
|
||
const finish = (openId?: string): void => {
|
||
if (closed) return;
|
||
closed = true;
|
||
preview.destroy();
|
||
closeModal();
|
||
controls.onClosed(changed, openId);
|
||
};
|
||
|
||
openModal({
|
||
title: "평면도 관리",
|
||
closeLabel: "닫기",
|
||
dialogClass: "b07-plan-manage",
|
||
beforeClose: () => {
|
||
finish();
|
||
return true;
|
||
},
|
||
mount: (body, close) => {
|
||
closeModal = close;
|
||
const grid = el("div", "b07-plan-manage__grid");
|
||
grid.append(side, view);
|
||
body.append(grid);
|
||
void refresh();
|
||
},
|
||
});
|
||
}
|