Files
Aislo/B08_Quantity/B08_Quantity_UI_Summary.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

140 lines
5.8 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Summary.ts
* 1탭 구조물 집계표(PLAN 61-1) — 줄 = 측점 · 열 = 저장된 구조물 조합 · 칸은 아직 비움.
* 표는 공용 많은 자료 표(`ui_template_data_table` · PLAN 70-2) — 머리 · 측점 열 고정.
*
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
* 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) — `GET /api/m02/items?all=1`
* 줄을 누르면 그 측점의 구조물도 모달(PLAN 95-3 · `B08_Quantity_UI_Drawing` 은 누를 때 `import()`)
* ========================================================================== */
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { loadRows as loadPickerRows } from "@ui/ui_template_item_picker";
import { pathOf, rowId, type PickerRow } from "@ui/ui_template_item_picker_data";
import { showToast } from "@ui/ui_template_elements";
import { fetchStructures } from "../B05_Profile/B05_Profile_Api_Structures";
import { fetchLatestRoute, readRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { createDataTable } from "@ui/ui_template_data_table";
import { pipeStructureId } from "@ui/ui_template_item_picker_project";
import { stationLabel } from "../common_util/common_util_station";
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
import {
combosAt,
comboId,
summaryColumns,
summaryStations,
type ComboColumn,
type ComboLabel,
type PlacedItem,
type StationCombo,
} from "./B08_Quantity_Summary_Data";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
/** 측점 간격 — 노선 설정값 · 못 읽으면 20m. */
export async function stationInterval(projectId: string): Promise<number> {
const latest = readRouteLatestCache(projectId) ?? (await fetchLatestRoute(projectId));
return latest.route_params?.station_interval_m || 20;
}
function note(text: string): HTMLElement {
const node = document.createElement("p");
node.className = "ui-general-block__empty";
node.textContent = text;
return node;
}
/** 머리 글 길이에 맞춘 열 폭 — 칸은 비어 있어 글이 폭을 정함. */
const widthOf = (label: string): number => Math.min(200, Math.max(96, label.length * 13 + 24));
function buildTable(
stations: number[],
columns: ComboColumn[],
interval: number,
onRowClick: (station: number, label: string) => void,
): HTMLElement {
const table = createDataTable({
columns: [
{ key: "station", label: L("B08_Quantity_Summary_Station"), width: 96 },
...columns.map((column) => ({
key: column.id,
label: column.name,
group: [column.group || L("B08_Quantity_Summary_MissingGroup")],
title: summaryTooltip(column.item),
className: column.missing ? "is-missing" : undefined,
width: widthOf(column.name),
})),
],
// 칸은 아직 비움 — 사용자가 채울 자리라 「-」 대신 빈칸
rows: stations.map((station) => ({ station: stationLabel(station, interval) })),
onRowClick: (i) => onRowClick(stations[i], stationLabel(stations[i], interval)),
frozen: 1,
empty: "",
caption: L("B08_Quantity_Tab_StructureSummary"),
});
table.root.dataset.b08Summary = "table";
return table.root;
}
/** 행 누름 — 카드 · CAD 묶음은 이때 처음 받음. */
async function openSheets(
projectId: string,
station: string,
combos: StationCombo[],
rows: Map<string, PickerRow>,
describe: (item: SummaryItem) => ComboLabel | null,
): Promise<void> {
try {
const { openStructureSheets } = await import("./B08_Quantity_UI_Drawing");
openStructureSheets({ projectId, station, combos, rows, describe });
} catch (error) {
showToast(`${L("B08_Quantity_Drawing_Failed")} — ${String(error)}`, "error");
}
}
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
export async function renderStructureSummary(host: HTMLElement, projectId: string): Promise<void> {
host.replaceChildren(note(L("B08_Quantity_Summary_Loading")));
try {
const [structures, pickerRows, pipes, interval] = await Promise.all([
fetchStructures(projectId),
loadPickerRows(),
// 배수 분석 전이면 관이 없음 — 구조물만으로 그림
fetchDetailPipePoints(projectId).then(
(response) => response.pipe_points,
() => [],
),
stationInterval(projectId).catch(() => 20),
]);
const m02 = new Map(pickerRows.map((row) => [rowId(row), row]));
const describe = (item: SummaryItem): ComboLabel | null => {
const row = m02.get(comboId(item));
if (!row) return null;
const [group = "", ...rest] = pathOf(row);
return { group, name: rest.join(" ") || group || row.key };
};
const things: PlacedItem[] = [
...structures.structures,
...pipes.map((pipe) => ({ ...pipe, structure_id: pipeStructureId(pipe.chainage_m) })),
];
const stations = summaryStations(things);
const columns = summaryColumns(things, describe);
const parts: HTMLElement[] = [];
if (!columns.length) parts.push(note(L("B08_Quantity_Summary_NoCombo")));
parts.push(
stations.length
? buildTable(stations, columns, interval, (station, label) =>
openSheets(projectId, label, combosAt(things, station), m02, describe),
)
: note(L("B08_Quantity_Summary_NoStation")),
);
host.replaceChildren(...parts);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
host.replaceChildren(note(`${L("B08_Quantity_Summary_Failed")} — ${reason}`));
}
}