Files
Aislo/B08_Quantity/B08_Quantity_Summary_Data.ts
T
eomsangdonandClaude Opus 5.5 be5ec734b0 feat(B08): 구조물 집계표 서버 종류 조합 열 빼기 · 열 폭 맨 아래 단 44~100 · 머리 줄바꿈(120-1 · 120-2)
- 화면 — 서버가 펼친 종류(types)를 조합 열에서 뺌
- 열 폭 — 맨 아래 단 글 · 칸 글만 잼(한글 12 · 그 밖 7 + 16) · 최소 44 · 최대 100 · 측점 열도
- B08 범위 CSS 로 집계표 머리 줄바꿈(공용 표 CSS 그대로)
- 시험 — 새 test_120(BOX · 세월교 · 물넘이 · 개거 열 · 값 · 폭 수치 · 서버 Node 박스 길이) · test_98 223 기대 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
2026-10-08 18:32:59 +09:00

135 lines
5.1 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`) — 같은 층 · 주인 · 키는 한 열
*
* 읽기만 — 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 {
/** 복사본 폴더를 찾는 구조물 id(관 지점은 `pipe@측점`) — 없으면 복사본 없이 원본 */
structure_id?: string | null;
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 interface StationCombo {
id: string;
item: SummaryItem;
structureId: 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);
for (const item of [
...(own ? [own] : []),
...Object.values(readGroupItems(thing.group_items)),
]) {
const structureId = thing.structure_id ?? null;
const key = `${structureId}
${comboId(item)}`;
if (!seen.has(key)) {
seen.add(key);
found.push({ id: comboId(item), item, structureId });
}
}
}
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;
}