feat(b08): 구조물 집계표 「미지정」 열 — 측점 빈 조합 칸 이름 · 같은 조합 열 칸 표기(160-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
* 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열
|
||||
* 조합 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · 서버 `linked_snapshots` · PLAN 143-1)
|
||||
* 미연결 시설 = 서버 `unlinked`(셈한 칸 기준) — 그 측점도 줄(값 빈칸 · 「미연결」 · PLAN 154-1)
|
||||
* 「미지정」 열 글 · 같은 조합 열 칸 = 빈 조합 칸(`unassignedText` · `unassignedCombos` · 160-2)
|
||||
*
|
||||
* 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다.
|
||||
* 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림.
|
||||
@@ -102,6 +103,26 @@ export interface ComboColumn {
|
||||
export const comboId = (item: SummaryItem): string =>
|
||||
`${item.layer}|${item.owner ?? ""}|${item.key}`;
|
||||
|
||||
/** 측점 「미지정」 칸 글 — 시설마다 「이름: 빈 칸 · 빈 칸」(칸 이름 = 그룹 · 본체 = `body` · 160-2). */
|
||||
export const unassignedText = (facilities: readonly UnlinkedFacility[], body: string): string =>
|
||||
facilities
|
||||
.map((f) => {
|
||||
const slots = f.slots.filter((slot) => !slot.linked).map((slot) => slot.group ?? body);
|
||||
return slots.length ? `${f.name}: ${slots.join(" · ")}` : f.name;
|
||||
})
|
||||
.join(" / ");
|
||||
|
||||
/** 빈 칸에 고른 조합 id — 같은 조합 열이 있으면 그 칸에 「미지정」(160-2). */
|
||||
export function unassignedCombos(facilities: readonly UnlinkedFacility[]): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const f of facilities)
|
||||
for (const slot of f.slots) {
|
||||
const item = slot.linked ? null : readSummaryItem(slot.item);
|
||||
if (item) ids.add(comboId(item));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function stationOf(thing: PlacedItem): number | null {
|
||||
const value = thing.chainage_m ?? thing.start_m;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
|
||||
* 머리 두 줄 = 조합 구분(경로 첫 칸) · 조합 이름(나머지 경로) — 사본 manifest 경로(집계 응답 `linked`)
|
||||
* 조합 · 측점 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · PLAN 143-1) — 미연결 시설은 그 측점
|
||||
* 줄만 「· 미연결」(값 빈칸 · 서버 `unlinked` · PLAN 154-1)
|
||||
* 줄만 「· 미연결」(값 빈칸 · 서버 `unlinked` · PLAN 154-1) · 측점 옆 「미지정」 열 = 그 측점 빈 조합 칸
|
||||
* · 같은 조합 열이 있으면 그 칸에도 「미지정」(160-2 · 화면만)
|
||||
* 횡단배수 열 · 칸 = GET …/quantity/summary(서버 단독 · PLAN 98 · 120) — 머리 구조물 · 형식 · 규격
|
||||
* 3단 + 단위 줄 · 「관 공」 맨 윗 머리 = 관에 이은 조합 경로(있으면) · 서버가 펼친 종류(`types`)는
|
||||
* 조합 열에서 뺌 · 다른 구조물 열은 조합 그대로
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
type LinkedSnapshot,
|
||||
type PlacedItem,
|
||||
type UnlinkedFacility,
|
||||
unassignedCombos,
|
||||
unassignedText,
|
||||
unlinkedAt,
|
||||
widthOf,
|
||||
} from "./B08_Quantity_Summary_Data";
|
||||
@@ -153,9 +156,22 @@ function buildTable(
|
||||
: labels[i],
|
||||
);
|
||||
const stationHead = L("B08_Quantity_Summary_Station");
|
||||
// 160-2 — 빈 조합 칸 이름 · 같은 조합 열 칸에도 「미지정」(미연결 시설이 있을 때만 열)
|
||||
const missingHead = L("B08_Quantity_Summary_Unassigned");
|
||||
const here = stations.map((station) => unlinkedAt(unlinked, station));
|
||||
const missing = here.map((at) => unassignedText(at, L("B08_Quantity_Link_Body")));
|
||||
const hasMissing = missing.some(Boolean);
|
||||
const comboIds = new Set(columns.map((column) => column.id));
|
||||
const markOf = (at: readonly UnlinkedFacility[]): Record<string, string> =>
|
||||
Object.fromEntries(
|
||||
[...unassignedCombos(at)].filter((id) => comboIds.has(id)).map((id) => [id, missingHead]),
|
||||
);
|
||||
const table = createDataTable({
|
||||
columns: [
|
||||
{ key: "station", label: stationHead, width: widthOf(stationHead, ...shown) },
|
||||
...(hasMissing
|
||||
? [{ key: "unassigned", label: missingHead, width: widthOf(missingHead, ...missing) }]
|
||||
: []),
|
||||
...columns.map((column) => ({
|
||||
key: column.id,
|
||||
label: column.name,
|
||||
@@ -169,11 +185,13 @@ function buildTable(
|
||||
// 조합 열 칸은 아직 비움 — 사용자가 채울 자리라 「-」 대신 빈칸 · 배관 칸 = 서버 값
|
||||
rows: stations.map((station, i) => ({
|
||||
station: shown[i],
|
||||
...(hasMissing ? { unassigned: missing[i] } : {}),
|
||||
...markOf(here[i]),
|
||||
...values.get(stationKey(station)),
|
||||
})),
|
||||
total,
|
||||
onRowClick: (i) => onRowClick(stations[i], labels[i]),
|
||||
frozen: 1,
|
||||
frozen: hasMissing ? 2 : 1,
|
||||
empty: "",
|
||||
caption: L("B08_Quantity_Tab_StructureSummary"),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""160-2 B08 구조물 집계표 「미지정」 — 측점 옆 열 글 · 같은 조합 열 칸 표기(화면만).
|
||||
|
||||
`B08_Quantity_Summary_Data.ts` 를 `common_util_summary_item.ts` 와 이어 붙여 node 로 돌림(61-1 시험과 같은 꼴).
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from test_61_1_b08_summary_table import _item, _run
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _slot(copy: str, group, item=None, linked=False) -> dict:
|
||||
return {"copy": copy, "group": group, "item": item, "linked": linked}
|
||||
|
||||
|
||||
FACILITIES = [
|
||||
{
|
||||
"id": "pipe@100",
|
||||
"type_id": "pipe",
|
||||
"name": "배관",
|
||||
"facility": "pipe",
|
||||
"chainage_m": 100.0,
|
||||
"slots": [
|
||||
_slot("pipe@100", None),
|
||||
_slot("pipe@100__gpipe", "관", _item("SS2")),
|
||||
_slot("pipe@100__gin", "관 보호공(유입)", _item("SS3"), linked=True),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "s1",
|
||||
"type_id": "ford_bridge",
|
||||
"name": "세월교",
|
||||
"facility": None,
|
||||
"chainage_m": 100.004,
|
||||
"slots": [_slot("s1__gfordout", "유출 구조", {"key": "깨짐"})], # 모양 틀림 = 조합 없음
|
||||
},
|
||||
{
|
||||
"id": "s2",
|
||||
"type_id": "box",
|
||||
"name": "BOX",
|
||||
"facility": None,
|
||||
"chainage_m": 160.0,
|
||||
"slots": [_slot("s2", None, _item("SS1"), linked=True)], # 빈 칸 없음 = 이름만
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_미지정_글과_조합_칸():
|
||||
out = _run(
|
||||
f"\nconst f = {json.dumps(FACILITIES, ensure_ascii=False)};"
|
||||
"\nconst at = (s) => unlinkedAt(f, s);"
|
||||
"\nconsole.log(JSON.stringify({"
|
||||
" t100: unassignedText(at(100), '본체'), t160: unassignedText(at(160), '본체'),"
|
||||
" t0: unassignedText(at(0), '본체'),"
|
||||
" c100: [...unassignedCombos(at(100))], c160: [...unassignedCombos(at(160))] }));"
|
||||
)
|
||||
assert out["t100"] == "배관: 본체 · 관 / 세월교: 유출 구조"
|
||||
assert out["t160"] == "BOX" and out["t0"] == ""
|
||||
assert out["c100"] == ["system||SS2"] # 연결된 칸 · 고른 조합 없는 칸은 뺌
|
||||
assert out["c160"] == []
|
||||
|
||||
|
||||
def test_화면_연결():
|
||||
summary = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
assert (
|
||||
'unassignedText(at, L("B08_Quantity_Link_Body"))' in summary
|
||||
) # 칸 이름 = 모달과 같은 규칙
|
||||
assert (
|
||||
'key: "unassigned", label: missingHead' in summary
|
||||
and "frozen: hasMissing ? 2 : 1" in summary
|
||||
)
|
||||
assert "...markOf(here[i])," in summary and "filter((id) => comboIds.has(id))" in summary
|
||||
assert '${L("B08_Quantity_Summary_Unlinked")}' in summary # 「· 미연결」 측점 표기 그대로
|
||||
locale = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
|
||||
assert 'B08_Quantity_Summary_Unassigned: ["미지정", "Unassigned"]' in locale
|
||||
@@ -96,5 +96,5 @@ def test_집계표는_공용_표():
|
||||
# PLAN 70-2 — 공용 많은 자료 표 · 측점 열 고정 · 칸은 빈칸(「—」 아님)
|
||||
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
assert "createDataTable(" in ui and "@ui/ui_template_data_table" in ui
|
||||
assert "frozen: 1" in ui and 'empty: ""' in ui and "onRowClick" in ui
|
||||
assert "frozen: hasMissing ? 2 : 1" in ui and 'empty: ""' in ui and "onRowClick" in ui
|
||||
assert 'document.createElement("table")' not in ui
|
||||
|
||||
Reference in New Issue
Block a user