- 탭 넷(구조물 집계표 · 구조물도 · 토적표 · 수리집수면적표) — 1탭만 내용 · 나머지 빈 탭 - 줄 = 구조물 · 배관이 놓인 측점(같은 측점 한 줄 · 정렬) · 열 = 저장된 summary_item + group_items(같은 층 · 주인 · 키 한 열) - 머리 두 줄 = 조합 구분(M02 경로 첫 칸) · 조합 이름(나머지 경로) · M02 에 없으면 빨강 「M02 에 없음」 - 읽기만 — structures.json · pipe_points.json GET · 옛 B08 코드 안 가져옴 - 항목 고르기 목록 읽기(loadRows) 내보냄 · 시험 test_61_1_b08_summary_table.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_Summary_Data.ts
|
|
* 구조물 집계표의 줄 · 열 고르기(화면 없음 · PLAN 61-1).
|
|
*
|
|
* 줄 = 구조물 · 배관이 놓인 측점(같은 측점은 한 줄 · 오름차순)
|
|
* 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열
|
|
*
|
|
* 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다.
|
|
* 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
readGroupItems,
|
|
readSummaryItem,
|
|
type SummaryItem,
|
|
} from "../common_util/common_util_summary_item";
|
|
|
|
/** 구조물 · 관 지점 한 건 — 필요한 칸만. 구간형은 기준점(`chainage_m`) · 없으면 시점. */
|
|
export interface PlacedItem {
|
|
chainage_m?: number | null;
|
|
start_m?: number | null;
|
|
summary_item?: unknown;
|
|
group_items?: unknown;
|
|
}
|
|
|
|
/** M02 조합 정보에서 읽은 구분 · 이름 — 못 찾으면 null(저장된 이름으로 보임). */
|
|
export interface ComboLabel {
|
|
group: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface ComboColumn {
|
|
/** `{층}|{주인}|{키}` — 항목 고르기 `rowId` 와 같은 꼴 */
|
|
id: string;
|
|
item: SummaryItem;
|
|
group: string;
|
|
name: string;
|
|
/** M02 에 그 조합이 없음 */
|
|
missing: boolean;
|
|
}
|
|
|
|
export const comboId = (item: SummaryItem): string =>
|
|
`${item.layer}|${item.owner ?? ""}|${item.key}`;
|
|
|
|
function stationOf(thing: PlacedItem): number | null {
|
|
const value = thing.chainage_m ?? thing.start_m;
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
/** 놓인 측점 — 1cm 안이면 같은 측점 · 오름차순. */
|
|
export function summaryStations(things: readonly PlacedItem[]): number[] {
|
|
const seen = new Map<number, number>();
|
|
for (const thing of things) {
|
|
const station = stationOf(thing);
|
|
if (station !== null) seen.set(Math.round(station * 100), station);
|
|
}
|
|
return [...seen.values()].sort((a, b) => a - b);
|
|
}
|
|
|
|
/** 저장된 조합 열 — 구분 · 이름 차례(같은 구분은 붙어 머리 칸을 합침). */
|
|
export function summaryColumns(
|
|
things: readonly PlacedItem[],
|
|
describe: (item: SummaryItem) => ComboLabel | null,
|
|
): ComboColumn[] {
|
|
const found = new Map<string, SummaryItem>();
|
|
for (const thing of things) {
|
|
const own = readSummaryItem(thing.summary_item);
|
|
for (const item of [...(own ? [own] : []), ...Object.values(readGroupItems(thing.group_items))])
|
|
if (!found.has(comboId(item))) found.set(comboId(item), item);
|
|
}
|
|
const columns = [...found].map(([id, item]): ComboColumn => {
|
|
const label = describe(item);
|
|
return label
|
|
? { id, item, ...label, missing: false }
|
|
: { id, item, group: "", name: item.name || item.key, missing: true };
|
|
});
|
|
return columns.sort(
|
|
(a, b) =>
|
|
a.group.localeCompare(b.group, "ko") ||
|
|
a.name.localeCompare(b.name, "ko") ||
|
|
a.id.localeCompare(b.id),
|
|
);
|
|
}
|
|
|
|
/** 머리 첫 줄 — 이어진 같은 구분을 한 칸으로 `[구분, 열 수]`. */
|
|
export function groupSpans(columns: readonly ComboColumn[]): Array<[string, number]> {
|
|
const spans: Array<[string, number]> = [];
|
|
for (const column of columns) {
|
|
const last = spans[spans.length - 1];
|
|
if (last && last[0] === column.group) last[1] += 1;
|
|
else spans.push([column.group, 1]);
|
|
}
|
|
return spans;
|
|
}
|