Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.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

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() };
}