Files
Aislo/B08_Quantity/B08_Quantity_Summary_Data.ts
T
eomsangdonandClaude Opus 5.5 b40a129cb5 feat(B08): 연결된 사본 원천 함수 · 집계표 사본 있는 칸만 · 조합 이름 · 구조물도 사본에서(143-1)
- M02_Item_Snapshot.linked_snapshots — 지정 ∩ 같은 항목 사본 {구조물 id: 항목 · 폴더 · 부분 · 경로} · B08 · B09 공용
- 집계표 서버 — 잇기 목록을 사본으로 거름 · 조합 모두 없는 칸은 안 셈 · 세월교 날 잇기 · 관 몫 본체 잇기 · 「조합 사본 없는 시설 n곳」 알림 · 응답 linked
- 화면 — 조합 이름 = 사본 경로(층 목록 GET 없음) · 측점 · 조합 열 = 사본 있는 것만 · 구조물도 모달 = 사본만(없으면 사본 없음)
- 시험 — test_98 · 120 · 131 픽스처 잇기 · test_61_2 사본만 · 새 test_143_1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-09 14:51:50 +09:00

168 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B08_Quantity_Summary_Data.ts
* 구조물 집계표의 줄 · 열 고르기(화면 없음 · PLAN 61-1).
*
* 줄 = 구조물 · 배관이 놓인 측점(같은 측점은 한 줄 · 오름차순)
* 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열
* 조합 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · 서버 `linked_snapshots` · PLAN 143-1)
*
* 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다.
* 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림.
* ========================================================================== */
import {
GROUP_JOIN,
readGroupItems,
readSummaryItem,
type SummaryItem,
} from "../common_util/common_util_summary_item";
/** 구조물 · 관 지점 한 건 — 필요한 칸만. 구간형은 기준점(`chainage_m`) · 없으면 시점. */
export interface PlacedItem {
/** 복사본 폴더를 찾는 구조물 id(관 지점은 `pipe@측점`) — 없으면 복사본 없이 원본 */
structure_id?: string | null;
chainage_m?: number | null;
start_m?: number | null;
summary_item?: unknown;
group_items?: unknown;
}
/** 연결된 사본 한 건(서버 `M02_Item_Snapshot.linked_snapshots` · 143-1) — `경로` = 가져온 때 M02 경로. */
export interface LinkedSnapshot {
항목: SummaryItem;
폴더: string;
/** 있는 부분(도면 · 면적 · 일위대가) */
부분: string[];
경로: string[];
}
/** 사본 있는 조합만 남김 — 본체 = `{구조물 id}` · 그룹 = `{구조물 id}__{그룹}` 사본 · 하나도 없으면 뺌. */
export function linkedOnly<T extends PlacedItem>(
things: readonly T[],
linked: Readonly<Record<string, LinkedSnapshot>>,
): T[] {
const out: T[] = [];
for (const thing of things) {
const id = thing.structure_id ?? "";
const own = id in linked ? readSummaryItem(thing.summary_item) : null;
const groups = Object.fromEntries(
Object.entries(readGroupItems(thing.group_items)).filter(
([gid]) => `${id}${GROUP_JOIN}${gid}` in linked,
),
);
if (own || Object.keys(groups).length)
out.push({ ...thing, summary_item: own, group_items: groups });
}
return out;
}
/** 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),
);
}
/** 한 측점에 놓인 조합 한 건 — 어느 구조물 것인지 · 사본 id(본체 = 구조물 id · 그룹 = `{id}__{그룹}`). */
export interface StationCombo {
id: string;
item: SummaryItem;
structureId: string | null;
copyId: string | null;
}
/** 측점(1cm 안 같은 측점)에 놓인 조합들 — 구조물마다 본체 → 그룹 차례 · 같은 구조물의 같은 조합은 한 건. */
export function combosAt(things: readonly PlacedItem[], station: number): StationCombo[] {
const seen = new Set<string>();
const found: StationCombo[] = [];
for (const thing of things) {
const at = stationOf(thing);
if (at === null || Math.round(at * 100) !== Math.round(station * 100)) continue;
const own = readSummaryItem(thing.summary_item);
const structureId = thing.structure_id ?? null;
for (const [gid, item] of [
...(own ? [["", own] as const] : []),
...Object.entries(readGroupItems(thing.group_items)),
]) {
const key = `${structureId}
${comboId(item)}`;
const copyId = structureId && (gid ? `${structureId}${GROUP_JOIN}${gid}` : structureId);
if (!seen.has(key)) {
seen.add(key);
found.push({ id: comboId(item), item, structureId, copyId });
}
}
}
return found;
}
/** 글 폭 추정 — 한글 12px · 그 밖 7px + 여백 16(글 12px · 칸 여백 8 × 2). */
export const textWidth = (text: string): number =>
[...text].reduce((sum, ch) => sum + (/[ㄱ-ㆎ가-힣]/.test(ch) ? 12 : 7), 16);
/** 열 폭 — 맨 아래 단 글 · 칸 글 중 가장 긴 것 · 44~100(넘는 머리는 줄바꿈 · 120-2). */
export const widthOf = (...texts: string[]): number =>
Math.min(100, Math.max(44, ...texts.map(textWidth)));
/** 머리 첫 줄 — 이어진 같은 구분을 한 칸으로 `[구분, 열 수]`. */
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;
}