Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
241 lines
10 KiB
TypeScript
241 lines
10 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_UI_Drawing.ts
|
||
* 구조물 집계표 행 → 구조물도 모달(PLAN 95-3) — 그 측점에 놓인 조합 전부를 세로로 쌓음.
|
||
* 조합 한 개 = M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로 — 도면(도각 포함 · 보기 전용 CAD ·
|
||
* 여러 장이면 장 탭) · 일위대가 · 면적산출 = 공용 카드 셋을 한 줄 세 칸(156-4 · 창 = 공용 wide).
|
||
*
|
||
* 원천 = 그 구조물의 프로젝트 사본만(연결된 사본 · PLAN 143-1) — 없으면 「사본 없음」
|
||
* 카드 [수정](163-3) = 모달 몸통이 사본 전용 편집 화면(고르기 모달과 같은 `createEditView`)으로 바뀜 ·
|
||
* 도면 · 면적산출 = 그 부분 · 일위대가 카드 = 넣을 값만(담은로직 · 자재는 원본 층에서 · 서버 400 · 142-1) ·
|
||
* [뒤로] = 카드 다시 읽음 · 저장했으면 닫을 때 집계표 다시 셈 · 판 다르면 서버 409 그대로
|
||
* 조합 목록 = 집계표와 같음(`B08_Quantity_Summary_Data` `combosAt`)
|
||
* 시설이 있으면 맨 위 「조합 연결」 칸(`B08_Quantity_UI_Link` · PLAN 154-2 · 156-1) — 저장 · 끊기면 닫고 다시 셈
|
||
* 카드 · CAD · 스프레드시트는 모달을 열 때만 `import()` · 닫을 때 카드를 모두 정리
|
||
* ========================================================================== */
|
||
|
||
import "./B08_Quantity_UI_Drawing.css";
|
||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||
import { openModal } from "@ui/ui_template_modal";
|
||
import { showToast } from "@ui/ui_template_elements";
|
||
import { PART_OF, projectItems, withCopyValues } from "@ui/ui_template_item_picker_project";
|
||
import type { EditView } from "@ui/ui_template_item_picker_edit";
|
||
import type { LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
|
||
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
|
||
import type {
|
||
ComboLabel,
|
||
LinkedSnapshot,
|
||
StationCombo,
|
||
UnlinkedFacility,
|
||
} from "./B08_Quantity_Summary_Data";
|
||
import { linkSection } from "./B08_Quantity_UI_Link";
|
||
|
||
function L(key: keyof typeof ui_locales): string {
|
||
return ui_locales[key][currentLanguageIndex];
|
||
}
|
||
|
||
function note(text: string): HTMLElement {
|
||
const node = document.createElement("p");
|
||
node.className = "ui-general-block__empty";
|
||
node.textContent = text;
|
||
return node;
|
||
}
|
||
|
||
export interface SheetsRequest {
|
||
projectId: string;
|
||
/** 모달 제목에 붙는 측점 글 */
|
||
station: string;
|
||
combos: StationCombo[];
|
||
/** 연결된 사본 `{구조물 id: 사본}`(집계 응답 · 143-1) */
|
||
linked: Record<string, LinkedSnapshot>;
|
||
describe: (item: SummaryItem) => ComboLabel | null;
|
||
/** 그 측점 시설 칸 전부(154 · 156-1) */
|
||
facilities: UnlinkedFacility[];
|
||
/** 바로 연결 · 끊기를 저장한 뒤 — 집계표 다시 셈 */
|
||
onLinked: () => void;
|
||
}
|
||
|
||
/** 사본 폴더 읽기 — 그 부분 파일이 없으면 던짐(카드가 「없음」) ·
|
||
* 일위대가 = 사본 `일위대가` · 입력값 · 고른자재 그대로(142-1 고르기 모달과 같은 길 · 161-1). */
|
||
function sourceOf(projectId: string, folder: string): ItemSource {
|
||
const items = projectItems(projectId);
|
||
return {
|
||
layer: "project",
|
||
projectId,
|
||
read: async (part) => {
|
||
const doc = await items.read(folder, PART_OF[part]);
|
||
if (doc == null) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
|
||
return { 문서: doc };
|
||
},
|
||
labor: {
|
||
read: async () => {
|
||
const [labor, values, picks] = await Promise.all([
|
||
items.read(folder, "일위대가") as Promise<{ 줄?: LaborDoc } | null>,
|
||
items.read(folder, "입력값") as Promise<ValueDoc | null>,
|
||
items.read(folder, "고른자재") as Promise<Record<string, unknown> | null>,
|
||
]);
|
||
if (!labor?.줄) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
|
||
return { 문서: withCopyValues(labor.줄, values, picks) };
|
||
},
|
||
calc: () => items.calcLabor(folder),
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 카드 [수정] — 사본 · 고칠 부분(일위대가 카드 = 넣을 값) · 카드 다시 읽기 */
|
||
type OpenEdit = (
|
||
copy: LinkedSnapshot,
|
||
name: string,
|
||
kind: EditKind | "inputs",
|
||
reload: () => void,
|
||
) => void;
|
||
|
||
/** 조합 한 칸 — 제목줄(이름 · 원천) + 안내 + 카드. */
|
||
function comboSection(
|
||
request: SheetsRequest,
|
||
combo: StationCombo,
|
||
createItemCards: typeof import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards").createItemCards,
|
||
handles: ItemCardsHandle[],
|
||
openEdit: OpenEdit,
|
||
): HTMLElement {
|
||
const label = request.describe(combo.item);
|
||
const name = label
|
||
? [label.group, label.name].filter(Boolean).join(" ")
|
||
: combo.item.name || combo.item.key;
|
||
const copy = combo.copyId ? request.linked[combo.copyId] : undefined;
|
||
const section = document.createElement("section");
|
||
section.className = "b08-sheets__combo";
|
||
section.dataset.b08Combo = combo.id;
|
||
const title = document.createElement("h4");
|
||
title.className = "b08-sheets__title";
|
||
title.textContent = name;
|
||
title.title = summaryTooltip(combo.item);
|
||
section.append(title);
|
||
if (!copy) {
|
||
section.append(note(L("B08_Quantity_Drawing_Missing")));
|
||
return section;
|
||
}
|
||
const source = sourceOf(request.projectId, copy.폴더);
|
||
const from = document.createElement("span");
|
||
from.className = "b08-sheets__from";
|
||
from.textContent = L("B08_Quantity_Sheets_FromCopy");
|
||
title.append(from);
|
||
if (!copy.부분.includes("도면")) section.append(note(L("B08_Quantity_Drawing_NoDrawing")));
|
||
const load = (): void => void cards.load({ id: copy.폴더, label: name });
|
||
// 카드 안 [저장]은 없음(자료 자리에 write 없음) — [수정]만 사본 편집 화면으로(163-3)
|
||
const cards = createItemCards({
|
||
source: () => source,
|
||
editable: () => true,
|
||
onEdit: (kind) => openEdit(copy, name, kind, load),
|
||
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
|
||
labor: () => openEdit(copy, name, "inputs", load), // 일위대가 카드(161-1) [수정] = 넣을 값만
|
||
});
|
||
handles.push(cards);
|
||
section.append(cards.root);
|
||
load();
|
||
return section;
|
||
}
|
||
|
||
/** 모달을 엶 — 조합이 없으면 안내 한 줄 · 불러오다 실패하면 까닭 한 줄. */
|
||
export function openStructureSheets(request: SheetsRequest): void {
|
||
const handles: ItemCardsHandle[] = [];
|
||
let closed = false;
|
||
let edited = false; // 사본을 고쳐 저장함 — 닫을 때 집계표 다시 셈(163-3)
|
||
let editView: EditView | null = null;
|
||
let modalBody: HTMLElement | null = null;
|
||
/** 카드 [수정] — 몸통을 사본 편집 화면으로 · [뒤로] = 몸통 되돌리고 그 카드 다시 읽음 */
|
||
const openEdit: OpenEdit = (copy, name, kind, reload) => {
|
||
const body = modalBody;
|
||
if (!body || editView) return;
|
||
void (async () => {
|
||
try {
|
||
const [{ createEditView }] = await Promise.all([
|
||
import("@ui/ui_template_item_picker_edit"),
|
||
import("@ui/ui_template_item_picker.css"),
|
||
]);
|
||
if (closed) return;
|
||
const kept = [...body.children];
|
||
const host = document.createElement("div");
|
||
host.className = "ui-item-picker__edit-host";
|
||
const view = createEditView(
|
||
{
|
||
kind,
|
||
row: { key: copy.항목.key, 열: copy.항목.key, 경로: [name] },
|
||
info: { 폴더: copy.폴더, 원본: copy.항목 },
|
||
items: projectItems(request.projectId),
|
||
projectId: request.projectId,
|
||
},
|
||
(saved) => {
|
||
view.destroy();
|
||
editView = null;
|
||
body.replaceChildren(...kept);
|
||
edited ||= saved;
|
||
if (saved) reload();
|
||
},
|
||
);
|
||
editView = view;
|
||
host.append(
|
||
...(kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))] : []),
|
||
view.root,
|
||
);
|
||
body.replaceChildren(host);
|
||
view.start();
|
||
} catch (error) {
|
||
showToast(`${L("B08_Quantity_Drawing_Failed")} — ${String(error)}`, "error");
|
||
}
|
||
})();
|
||
};
|
||
openModal({
|
||
title: `${request.station} ${L("B08_Quantity_Sheets_Title")}`,
|
||
closeLabel: L("Common_Btn_Close"),
|
||
wide: true, // 공용 큰 창(96vw × 94vh · 156-4)
|
||
dialogClass: "b08-sheets",
|
||
beforeClose: async () => {
|
||
if (editView && !(await editView.leave())) return false; // 저장 안 한 고침 — 버릴지 물음
|
||
editView?.destroy();
|
||
closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음
|
||
handles.forEach((cards) => cards.destroy());
|
||
if (edited) request.onLinked(); // 사본을 고쳤으면 집계표 다시 셈(연결 저장과 같은 꼴)
|
||
return true;
|
||
},
|
||
mount: (body, close) => {
|
||
modalBody = body;
|
||
const linking = request.facilities.length
|
||
? [
|
||
linkSection(request.projectId, request.facilities, () => {
|
||
closed = true;
|
||
handles.forEach((cards) => cards.destroy());
|
||
close();
|
||
request.onLinked();
|
||
}),
|
||
]
|
||
: [];
|
||
if (!request.combos.length)
|
||
return void body.append(
|
||
...linking,
|
||
...(linking.length ? [] : [note(L("B08_Quantity_Summary_NoCombo"))]),
|
||
);
|
||
body.append(...linking, note(L("B08_Quantity_Drawing_Loading")));
|
||
void (async () => {
|
||
try {
|
||
const { createItemCards } =
|
||
await import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards");
|
||
if (closed) return;
|
||
body.replaceChildren(
|
||
...linking,
|
||
...request.combos.map((combo) =>
|
||
comboSection(request, combo, createItemCards, handles, openEdit),
|
||
),
|
||
);
|
||
} catch (error) {
|
||
if (closed) return;
|
||
const reason = error instanceof Error ? error.message : String(error);
|
||
body.replaceChildren(...linking, note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
|
||
}
|
||
})();
|
||
},
|
||
});
|
||
}
|