- 맨 위 [종류 ▼] 상세 폼 · × 대신 폼 안 [지우기] - 평면도 장 = 종류마다 한 줄 [≡] 이름 [1][2] · 장 단추 누르면 미리보기 + 드롭다운 따라감 - 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 PUT plan-kinds-order(reorder_kinds) · 번호 바뀐 장 확정 풂 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* B07_DesignDetail_UI_PlanManage.ts
|
|
* [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음.
|
|
*
|
|
* 왼쪽 = [종류 ▼] 상세 폼(추가 · 업데이트 · 지우기) · 평면도 장(종류마다 한 줄 [≡] 이름 [1][2] ·
|
|
* 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 저장 140-2) · 대피소 · 사토장 자리
|
|
* 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 캔버스만 `bare`) · 확정 배지 · [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 {
|
|
fetchDesignDrawing,
|
|
fetchDesignDrawingList,
|
|
fetchPlanKinds,
|
|
reorderPlanKinds,
|
|
type CadDrawing,
|
|
type DesignDrawingItem,
|
|
type PlanKindsState,
|
|
} from "./B07_DesignDetail_Api_Fetch";
|
|
import {
|
|
groupPlanSheets,
|
|
kindForm,
|
|
marksForm,
|
|
run,
|
|
type KindForm,
|
|
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;
|
|
/** 드롭다운 · 줄 강조의 종류 — 장 단추를 누르면 그 장의 종류로 따라감. */
|
|
let pickedKind = "";
|
|
let form: KindForm | undefined;
|
|
let sheets: DesignDrawingItem[] = [];
|
|
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 kindSheets = (kindId: string) =>
|
|
groupPlanSheets(sheets.filter((item) => item.plan_kind === kindId))[0]?.sheets ?? [];
|
|
|
|
/** 장 단추 → 미리보기 + 드롭다운이 그 종류로. */
|
|
const pickSheet = (drawing: DesignDrawingItem, label: string): void => {
|
|
pickedKind = drawing.plan_kind ?? "";
|
|
form?.pick(pickedKind);
|
|
void show(drawing, label);
|
|
};
|
|
|
|
/** 「평면도 장」 한 블록 — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서(놓는 즉시). */
|
|
const sheetBlock = (state: PlanKindsState): HTMLElement => {
|
|
const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets");
|
|
list.append(el("strong", "", "평면도 장"));
|
|
const ids = state.kinds.map((kind) => kind.id);
|
|
let dragId: string | null = null;
|
|
for (const kind of state.kinds) {
|
|
const row = el("div", "b07-plan-kind__head b07-plan-manage__row");
|
|
row.dataset.planKind = kind.id;
|
|
row.draggable = true;
|
|
const grip = el("span", "b07-plan-manage__grip", "≡");
|
|
grip.title = "끌어 놓아 종류 순서 바꾸기";
|
|
const group = kindSheets(kind.id);
|
|
// 한 장이면 이름이 곧 장 단추 · 여러 장이면 이름 + 작은 장 단추
|
|
const name =
|
|
group.length === 1
|
|
? button("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () =>
|
|
pickSheet(group[0].drawing, group[0].label),
|
|
)
|
|
: el("span", "b07-plan-kind__name", kind.name);
|
|
if (group.length === 1) {
|
|
name.dataset.drawingId = group[0].drawing.id;
|
|
name.dataset.confirmed = String(group[0].drawing.confirmed);
|
|
}
|
|
row.append(grip, name);
|
|
if (group.length > 1)
|
|
group.forEach(({ drawing, label }, index) => {
|
|
const item = button("b07-plan-manage__sheet", String(index + 1), () =>
|
|
pickSheet(drawing, label),
|
|
);
|
|
item.title = label;
|
|
item.dataset.drawingId = drawing.id;
|
|
item.dataset.confirmed = String(drawing.confirmed);
|
|
row.append(item);
|
|
});
|
|
// 끌기 꼴 = 공용 탭 줄(`cad_host_tabs.ts`) — 세로라 위 · 아래 반으로 앞 · 뒤
|
|
row.addEventListener("dragstart", (event) => {
|
|
dragId = kind.id;
|
|
event.dataTransfer?.setData("text/plain", kind.id);
|
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
});
|
|
row.addEventListener("dragend", () => (dragId = null));
|
|
row.addEventListener("dragover", (event) => {
|
|
if (!dragId) return;
|
|
event.preventDefault();
|
|
const box = row.getBoundingClientRect();
|
|
row.dataset.drop = event.clientY > box.top + box.height / 2 ? "after" : "before";
|
|
});
|
|
row.addEventListener("dragleave", () => delete row.dataset.drop);
|
|
row.addEventListener("drop", (event) => {
|
|
event.preventDefault();
|
|
const after = row.dataset.drop === "after";
|
|
delete row.dataset.drop;
|
|
if (!dragId || dragId === kind.id) return;
|
|
const order = ids.filter((id) => id !== dragId);
|
|
order.splice(order.indexOf(kind.id) + (after ? 1 : 0), 0, dragId);
|
|
dragId = null;
|
|
if (order.join() !== ids.join()) void run(() => reorderPlanKinds(projectId, order), edits);
|
|
});
|
|
list.append(row);
|
|
}
|
|
return list;
|
|
};
|
|
|
|
const markActive = (): void => {
|
|
side
|
|
.querySelectorAll<HTMLElement>(".b07-plan-manage__sheet")
|
|
.forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked)));
|
|
side
|
|
.querySelectorAll<HTMLElement>(".b07-plan-manage__row")
|
|
.forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind)));
|
|
};
|
|
|
|
/** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */
|
|
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);
|
|
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,
|
|
bare: 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;
|
|
sheets = list.drawings.filter((item) => item.kind === "plan");
|
|
const all = groupPlanSheets(sheets).flatMap((group) => group.sheets);
|
|
const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0];
|
|
if (keep) pickedKind = keep.drawing.plan_kind ?? "";
|
|
// 드롭다운으로 고른 종류 → 그 종류 첫 장 미리보기 + 줄 강조
|
|
form = kindForm(state, edits, pickedKind, (kindId) => {
|
|
pickedKind = kindId;
|
|
const first = kindSheets(kindId)[0];
|
|
if (first) void show(first.drawing, first.label);
|
|
else markActive();
|
|
});
|
|
side.replaceChildren(form.root, sheetBlock(state), marksForm(state, edits));
|
|
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();
|
|
},
|
|
});
|
|
}
|