Files
Aislo/B08_Quantity/B08_Quantity_UI_Summary.ts
T
eomsangdonandClaude Opus 5.5 5f056ec661 feat(b08): 4탭 홍수량 계산서 — 배수지점 측점 보조 탭 · 견본 모양 한 장 · 서버 계산(PLAN 62-2 · 62-3)
- 측점 보조 탭 = 저장된 pipe_points 측점만 읽음 · 입력은 이 탭 자기 기본값(B05/B06 · 배수유역 값과 안 이음)
- 1-1 합리식(Qd = Q × 할증) · 1-2 Kirpich 도달시간(최소 5분) · 1-3 rt = (R24/24)·(24/T)^0.557
- 2 단면검토 — 관 단면 그림 · 단면적 · 윤변 · 경심 · 경사 · n · Manning 유속 · Q = A·V · OK/NG 점선 상자 · 유출율
- 선택지(할증 2.0/1.5/1.2 · 만관/240°/유효 70 % · 관 종류별 n) · 기본값 한 파일(B08_Quantity_Hydro_Defaults.py)
- 계산 = 서버 단독(B08_Quantity_Engine_Hydro · /quantity/hydro · /quantity/hydro/calc · 최신 응답만) · 입력은 캐시만 · 어떤 파일에도 안 씀
- 흰 종이 계산서(입력 빨강 · 계산 파랑 · 주요 입력 노랑) · 쪽 표시 · 시험 test_62_2_b08_hydro_engine.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
2026-10-06 18:15:29 +09:00

126 lines
5.1 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Summary.ts
* 1탭 구조물 집계표(PLAN 61-1) — 줄 = 측점 · 열 = 저장된 구조물 조합 · 칸은 아직 비움.
*
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
* 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) — `GET /api/m02/items?all=1`
* ========================================================================== */
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { loadRows as loadPickerRows } from "@ui/ui_template_item_picker";
import { pathOf, rowId } from "@ui/ui_template_item_picker_data";
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 { stationLabel } from "../common_util/common_util_station";
import { summaryTooltip } from "../common_util/common_util_summary_item";
import {
comboId,
groupSpans,
summaryColumns,
summaryStations,
type ComboColumn,
type ComboLabel,
} 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 cell(tag: "th" | "td", text = "", className = ""): HTMLTableCellElement {
const node = document.createElement(tag);
node.textContent = text;
if (className) node.className = className;
return node;
}
function note(text: string): HTMLElement {
const node = document.createElement("p");
node.className = "ui-general-block__empty";
node.textContent = text;
return node;
}
function buildTable(stations: number[], columns: ComboColumn[], interval: number): HTMLElement {
const head = cell("th", L("B08_Quantity_Summary_Station"), "b08-summary__station");
head.rowSpan = 2;
const groups = document.createElement("tr");
groups.append(head);
for (const [group, span] of groupSpans(columns)) {
const th = cell("th", group || L("B08_Quantity_Summary_MissingGroup"), "b08-summary__group");
th.colSpan = span;
groups.append(th);
}
const names = document.createElement("tr");
for (const column of columns) {
const th = cell("th", column.name, "b08-summary__name");
th.dataset.b08Combo = column.id;
th.title = summaryTooltip(column.item);
if (column.missing) th.classList.add("is-missing");
names.append(th);
}
const thead = document.createElement("thead");
thead.append(groups, names);
const tbody = document.createElement("tbody");
for (const station of stations) {
const tr = document.createElement("tr");
tr.dataset.b08Station = String(station);
tr.append(cell("th", stationLabel(station, interval), "b08-summary__station"));
for (const _ of columns) tr.append(cell("td"));
tbody.append(tr);
}
const table = document.createElement("table");
table.className = "ui-general-block__table b08-summary__table";
table.dataset.b08Summary = "table";
table.append(thead, tbody);
const wrap = document.createElement("div");
wrap.className = "ui-general-block__table-wrap b08-summary__wrap";
wrap.append(table);
return wrap;
}
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
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: Parameters<typeof comboId>[0]): 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 = [...structures.structures, ...pipes];
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)
: 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}`));
}
}