Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -595,7 +595,7 @@ def pipe_summary(
|
||||
notes = []
|
||||
missing = sheet.unlinked | {key for key, found in others.items() if not found}
|
||||
if missing:
|
||||
notes.append(f"조합 사본 없는 시설 {len(missing)}곳 — 「미연결」 줄을 눌러 조합 연결")
|
||||
notes.append(f"조합 사본 없는 시설 {len(missing)}곳 — 노랑 램프 줄을 눌러 조합 연결")
|
||||
if no_length:
|
||||
notes.append(f"관 연장 없음 {no_length}곳 — B06 횡단 [저장] 뒤 채워짐")
|
||||
if no_basin:
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* 줄 = 구조물 · 배관이 놓인 측점(같은 측점은 한 줄 · 오름차순)
|
||||
* 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열
|
||||
* 조합 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · 서버 `linked_snapshots` · PLAN 143-1)
|
||||
* 미연결 시설 = 서버 `unlinked`(셈한 칸 기준) — 그 측점도 줄(값 빈칸 · 「미연결」 · PLAN 154-1)
|
||||
* 「미지정」 열 글 · 같은 조합 열 칸 = 빈 조합 칸(`unassignedText` · `unassignedCombos` · 160-2)
|
||||
* 미연결 시설 = 서버 `unlinked`(셈한 칸 기준) — 그 측점도 줄(값 빈칸 · 노랑 램프 · PLAN 154-1 · 198-1)
|
||||
* 램프 호버 글 = 빈 조합 칸(`unassignedText` · 160-2)
|
||||
*
|
||||
* 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다.
|
||||
* 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림.
|
||||
@@ -114,7 +114,7 @@ export interface ComboColumn {
|
||||
export const comboId = (item: SummaryItem): string =>
|
||||
`${item.layer}|${item.owner ?? ""}|${item.key}`;
|
||||
|
||||
/** 측점 「미지정」 칸 글 — 시설마다 「이름: 빈 칸 · 빈 칸」(칸 이름 = 그룹 · 본체 = `body` · 160-2). */
|
||||
/** 측점 램프 호버 글 —시설마다 「이름: 빈 칸 · 빈 칸」(칸 이름 = 그룹 · 본체 = `body` · 160-2). */
|
||||
export const unassignedText = (facilities: readonly UnlinkedFacility[], body: string): string =>
|
||||
facilities
|
||||
.map((f) => {
|
||||
@@ -123,17 +123,6 @@ export const unassignedText = (facilities: readonly UnlinkedFacility[], body: st
|
||||
})
|
||||
.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;
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 구조물 집계표 미연결 램프(198-1) — 측점 칸 왼쪽 경고 노랑 점 하나(177-1 미연결 노랑 토큰) · 연결 줄은 없음 */
|
||||
[data-b08-summary="table"] td.b08-summary__lamp::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-warning);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 좌측 패널 옵션(122) — 연암 · 경암 두 칸 나란히 · 설명 한 줄 */
|
||||
.b08-options__row {
|
||||
display: grid;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* 구조물 = GET …/route/structures · 배관 = GET …/drainage/pipe-points (읽기만)
|
||||
* 머리 두 줄 = 조합 구분(경로 첫 칸) · 조합 이름(나머지 경로) — 사본 manifest 경로(집계 응답 `linked`)
|
||||
* 조합 · 측점 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · PLAN 143-1) — 미연결 시설은 그 측점
|
||||
* 줄만 「· 미연결」(값 빈칸 · 서버 `unlinked` · PLAN 154-1) · 측점 옆 「미지정」 열 = 그 측점 빈 조합 칸
|
||||
* · 같은 조합 열이 있으면 그 칸에도 「미지정」(160-2 · 화면만)
|
||||
* 줄만 측점 칸 왼쪽 노랑 램프(값 빈칸 · 서버 `unlinked` · PLAN 154-1 · 198-1)
|
||||
* · 램프 호버(공용 `ui_template_hover_info`) = 그 측점 빈 조합 칸(160-2 · 화면만)
|
||||
* 횡단배수 열 · 칸 = GET …/quantity/summary(서버 단독 · PLAN 98 · 120) — 머리 구조물 · 형식 · 규격
|
||||
* 3단 + 단위 줄 · 「관 공」 맨 윗 머리 = 관에 이은 조합 경로(있으면) · 서버가 펼친 종류(`types`)는
|
||||
* 조합 열에서 뺌 · 다른 구조물 열은 조합 그대로
|
||||
@@ -24,6 +24,7 @@ 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 { createDataTable, type DataColumn, type DataRow } from "@ui/ui_template_data_table";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { pipeStructureId } from "@ui/ui_template_item_picker_project";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
type LinkedSnapshot,
|
||||
type PlacedItem,
|
||||
type UnlinkedFacility,
|
||||
unassignedCombos,
|
||||
unassignedText,
|
||||
unlinkedAt,
|
||||
widthOf,
|
||||
@@ -98,6 +98,9 @@ async function fetchPipeSummary(projectId: string): Promise<PipeSummary> {
|
||||
}
|
||||
|
||||
const PIPE_TOP = "관 공";
|
||||
/** 미연결 램프 칸 class(`B08_Quantity_UI_Page.css` · 198-1) · 램프 자리 폭(12px 글 기준) */
|
||||
const LAMP_CLASS = "b08-summary__lamp";
|
||||
const LAMP_W = 14;
|
||||
const stationKey = (station: number): number => Math.round(station * 100);
|
||||
/** 열 소수 자리 — 칸 · 계 값이 쓰는 만큼(최대 2). */
|
||||
const placesOf = (values: number[]): number =>
|
||||
@@ -150,29 +153,14 @@ function buildTable(
|
||||
? { station: L("B08_Quantity_Summary_Total"), ...pipe.total }
|
||||
: null;
|
||||
const labels = stations.map((station) => stationLabel(station, interval));
|
||||
// 미연결 시설 측점 = 「· 미연결」(값 빈칸 · 누르면 바로 연결 · 154)
|
||||
const shown = stations.map((station, i) =>
|
||||
unlinkedAt(unlinked, station).length
|
||||
? `${labels[i]} · ${L("B08_Quantity_Summary_Unlinked")}`
|
||||
: 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]),
|
||||
);
|
||||
// 198-1 — 미연결 시설 측점 = 측점 칸 왼쪽 노랑 램프(값 빈칸 · 누르면 바로 연결 · 154) · 호버 = 빈 조합 칸(160-2)
|
||||
const missing = stations.map((station) =>
|
||||
unassignedText(unlinkedAt(unlinked, station), L("B08_Quantity_Link_Body")),
|
||||
);
|
||||
const table = createDataTable({
|
||||
columns: [
|
||||
{ key: "station", label: stationHead, width: widthOf(stationHead, ...shown) },
|
||||
...(hasMissing
|
||||
? [{ key: "unassigned", label: missingHead, width: widthOf(missingHead, ...missing) }]
|
||||
: []),
|
||||
{ key: "station", label: stationHead, width: widthOf(stationHead, ...labels) + LAMP_W },
|
||||
...columns.map((column) => ({
|
||||
key: column.id,
|
||||
label: column.name,
|
||||
@@ -185,19 +173,27 @@ function buildTable(
|
||||
],
|
||||
// 조합 열 칸은 아직 비움 — 사용자가 채울 자리라 「-」 대신 빈칸 · 배관 칸 = 서버 값
|
||||
rows: stations.map((station, i) => ({
|
||||
station: shown[i],
|
||||
...(hasMissing ? { unassigned: missing[i] } : {}),
|
||||
...markOf(here[i]),
|
||||
station: labels[i],
|
||||
...values.get(stationKey(station)),
|
||||
})),
|
||||
total,
|
||||
onRowClick: (i) => onRowClick(stations[i], labels[i]),
|
||||
frozen: hasMissing ? 2 : 1,
|
||||
cellClass: (i, key) => (key === "station" && missing[i] ? LAMP_CLASS : null),
|
||||
frozen: 1,
|
||||
empty: "",
|
||||
caption: L("B08_Quantity_Tab_StructureSummary"),
|
||||
fillViewport: true,
|
||||
});
|
||||
table.root.dataset.b08Summary = "table";
|
||||
// 줄을 다시 그려도(보이는 줄만) 떠 있는 root 하나에 걸어 둠 — 줄 번호 = 줄 `data-i`
|
||||
attachHoverInfo(
|
||||
table.root,
|
||||
(cell) => {
|
||||
const why = missing[Number(cell.closest("tr")?.dataset.i)];
|
||||
return why && `${L("B08_Quantity_Summary_LampTip")} — ${why}`;
|
||||
},
|
||||
`td.${LAMP_CLASS}`,
|
||||
);
|
||||
return table.root;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user