Files
Aislo/B08_Quantity/B08_Quantity_UI_Drawing.ts
T
eomsangdonandClaude Sonnet 5.5 c3af85113c feat(B08): 탭 구조물 · 토공 · 수리 + 구조물 보조 넷 · 구조물도 탭 지움 · 보조 탭 위 간격 8px(95-1 · 95-2)
구조물도 모달(집계표 행 누름 · 그 측점 조합 전부 · 복사본 먼저) · 공용 표 onRowClick opt-in(95-3) ·
수리 상세 폭 제한 풂 · 카드 높이 화면 따라 · @container(95-4). 화면 검증 전.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
2026-10-08 13:46:09 +09:00

173 lines
6.3 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Drawing.ts
* 구조물 집계표 행 → 구조물도 모달(PLAN 95-3) — 그 측점에 놓인 조합 전부를 세로로 쌓음.
* 조합 한 개 = M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로 — 도면(도각 포함 · 보기 전용 CAD ·
* 여러 장이면 장 탭) + 면적산출.
*
* 원천 = 그 구조물의 프로젝트 복사본(같은 항목일 때) · 없으면 M02 원본 층 — 읽기만
* 조합 목록 = 집계표와 같음(`B08_Quantity_Summary_Data` `combosAt`)
* 카드 · 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 { layerOf, type PickerRow } from "@ui/ui_template_item_picker_data";
import {
PART_OF,
projectItems,
sameItem,
structureFolder,
type ProjectItemInfo,
} from "@ui/ui_template_item_picker_project";
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
import type { 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, StationCombo } from "./B08_Quantity_Summary_Data";
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[];
/** `comboId` → M02 줄 */
rows: Map<string, PickerRow>;
describe: (item: SummaryItem) => ComboLabel | null;
}
/** 그 구조물의 복사본 목록 — 프로젝트 복사본을 못 읽으면 원본만(모달은 그대로 열림). */
const loadCopies = (projectId: string): Promise<ProjectItemInfo[]> =>
projectItems(projectId)
.list()
.catch(() => []);
function sourceOf(
request: SheetsRequest,
combo: StationCombo,
row: PickerRow,
copies: ProjectItemInfo[],
): { source: ItemSource; copy: boolean } {
const own = combo.structureId
? copies.find((info) => info.폴더 === structureFolder(combo.structureId!))
: undefined;
if (own && sameItem(own, row)) {
const items = projectItems(request.projectId);
return {
copy: true,
source: {
layer: "project",
projectId: request.projectId,
read: async (part) => {
const doc = await items.read(own.폴더, PART_OF[part]);
if (doc == null) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
return { 문서: doc };
},
},
};
}
const layer = layerOf(row);
return {
copy: false,
source: {
layer,
projectId: null,
read: (part, column) => readTemplate(layer, part, column, null),
},
};
}
/** 조합 한 칸 — 제목줄(이름 · 원천) + 안내 + 카드. */
function comboSection(
request: SheetsRequest,
combo: StationCombo,
copies: ProjectItemInfo[],
createItemCards: typeof import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards").createItemCards,
handles: ItemCardsHandle[],
): HTMLElement {
const label = request.describe(combo.item);
const name = label
? [label.group, label.name].filter(Boolean).join(" ")
: combo.item.name || combo.item.key;
const row = request.rows.get(combo.id) ?? null;
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 (!row?.열) {
section.append(note(L("B08_Quantity_Drawing_Missing")));
return section;
}
const picked = sourceOf(request, combo, row, copies);
const from = document.createElement("span");
from.className = "b08-sheets__from";
from.textContent = L(
picked.copy ? "B08_Quantity_Sheets_FromCopy" : "B08_Quantity_Sheets_FromMaster",
);
title.append(from);
if (!row.도번) section.append(note(L("B08_Quantity_Drawing_NoDrawing")));
const cards = createItemCards({
source: () => picked.source,
editable: () => false,
onEdit: () => undefined,
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
});
handles.push(cards);
section.append(cards.root);
void cards.load({ id: row.열, label: name });
return section;
}
/** 모달을 엶 — 조합이 없으면 안내 한 줄 · 불러오다 실패하면 까닭 한 줄. */
export function openStructureSheets(request: SheetsRequest): void {
const handles: ItemCardsHandle[] = [];
let closed = false;
openModal({
title: `${request.station} ${L("B08_Quantity_Sheets_Title")}`,
closeLabel: L("Common_Btn_Close"),
dialogClass: "b08-sheets",
beforeClose: () => {
closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음
handles.forEach((cards) => cards.destroy());
return true;
},
mount: (body) => {
if (!request.combos.length) return void body.append(note(L("B08_Quantity_Summary_NoCombo")));
body.append(note(L("B08_Quantity_Drawing_Loading")));
void (async () => {
try {
const [{ createItemCards }, copies] = await Promise.all([
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
loadCopies(request.projectId),
]);
if (closed) return;
body.replaceChildren(
...request.combos.map((combo) =>
comboSection(request, combo, copies, createItemCards, handles),
),
);
} catch (error) {
if (closed) return;
const reason = error instanceof Error ? error.message : String(error);
body.replaceChildren(note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
}
})();
},
});
}