- 공용 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>
260 lines
10 KiB
TypeScript
260 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;
|
|
/** 머리 단추 — ⚙ 로 열 때 [원본에서 다시 그리기](280-5). */
|
|
actions?: HTMLElement[];
|
|
}
|
|
|
|
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): { close: () => 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",
|
|
actions: controls.actions,
|
|
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();
|
|
},
|
|
});
|
|
return { close: () => finish() };
|
|
}
|