feat(M02): 구조물 조합 상단 표 「일위대가 합계」 읽기 전용 줄 — 반영합 · 없으면 기준합(기준) · 카드 저장 뒤 다시 읽기(86-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
2026-10-08 12:11:47 +09:00
co-authored by Claude Sonnet 5.5
parent cd8a226323
commit 142194a1e5
12 changed files with 136 additions and 16 deletions
@@ -175,6 +175,9 @@ export interface StructureNumbers {
면적번: string;
/** 일위대가 `SU-######`(39장) */
일위대가번: string;
/** 일위대가 합계(86-3) — 반영합 · 없으면 기준합(`합계기준` 참) · 계산값 없으면 칸 없음 */
일위대가합계?: number;
합계기준?: boolean;
}
/** `{열 id: {도번, 면적번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
@@ -24,7 +24,6 @@ import {
readTemplate,
saveTemplate,
type StructureDoc,
type StructureNumbers,
} from "./M02_MasterTemplete_Api_Fetch";
import {
cardShell,
@@ -48,7 +47,7 @@ export type EditKind = ItemPart;
/** 카드 종류 — 열 문서 셋 + 일위대가(39장 · 보기 갈래 카드 · 고침은 일위대가 상세) */
export type CardKind = EditKind | "labor";
export type SheetKind = Exclude<EditKind, "structure">;
export const PART_FIELD: Record<CardKind, keyof StructureNumbers> = {
export const PART_FIELD: Record<CardKind, "도번" | "면적번" | "일위대가번"> = {
structure: "도번",
labor: "일위대가번",
area: "면적번",
@@ -100,6 +99,8 @@ export interface ItemCardsOptions {
extraCards?: HTMLElement[];
/** 카드 가운데 하나라도 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4 · 31-7) */
onDirty?: (dirty: boolean) => void;
/** 카드 [저장] 이 끝남 — 번호 · 일위대가 합계를 다시 읽는 자리(86-3) */
onSaved?: () => void;
/** 면적산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */
sheetEdit?: SheetEditOptions;
/** 「가져오기」(39-5 · 43-9b) — 다른 항목의 이 칸을 골라 덮어씀 · 빈 카드도 같음 · 없으면 단추 없음 */
@@ -195,7 +196,14 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const no = el("span", { className: "m02-struct__no" });
const name = el("span", { className: "m02-struct__title" });
const shell = cardShell({ title, body, sub: [no, name] });
const saver = saveControl(shell, save, () => opts.onDirty?.(anyDirty()));
const saver = saveControl(
shell,
async () => {
await save();
opts.onSaved?.();
},
() => opts.onDirty?.(anyDirty()),
);
const help = helpButton(shell, title, CARD_HELP[kind]); // 제목 바로 옆(47-1)
const edit = createButton({
label: TEXT.edit,
+29 -3
View File
@@ -226,13 +226,39 @@ def _codes() -> dict[str, str]:
return codes
def structure_numbers() -> dict[str, dict[str, str]]:
"""`{열 id: {도번, 면적번}}` — 집계표 줄 · 좌측 줄이 한 번에 받음(없는 쪽은 빈 글)."""
out: dict[str, dict[str, str]] = {}
def _total(sums: Any) -> float | None:
"""`계산값` 합 칸의 `계` — 수 아닌 값 · 없음 = None."""
total = sums.get("계") if isinstance(sums, dict) else None
if total is None or isinstance(total, bool):
return None
try:
return float(total)
except (TypeError, ValueError):
return None
def _labor_sum(doc: dict[str, Any]) -> dict[str, Any]:
"""일위대가 합계 칸(86-3) — 반영합 `계`(0 아님) · 없으면 기준합 `계` + `합계기준` 참 ·
담은 로직 0 · 계산값 없음 = 빈 칸 (목록 `labor` 합계와 같은 규칙)."""
if not doc.get("담은로직"):
return {}
calc = doc.get("계산값") or {}
if total := _total(calc.get("반영합")):
return {"일위대가합계": total, "합계기준": False}
base = _total(calc.get("기준합"))
return {} if base is None else {"일위대가합계": base, "합계기준": True}
def structure_numbers() -> dict[str, dict[str, Any]]:
"""`{열 id: {도번, 일위대가번, 면적번, (일위대가합계, 합계기준)}}` — 집계표 줄 · 좌측 줄이 한 번에
받음(번호 없는 쪽은 빈 글 · 합계는 일위대가 계산값이 있을 때만)."""
out: dict[str, dict[str, Any]] = {}
for kind, key, _, _ in PARTS:
for name, doc in _docs(kind):
row = out.setdefault(name, {k: "" for _, k, _, _ in PARTS})
row[key] = str(doc.get(key) or "")
if kind == "labor":
row.update(_labor_sum(doc))
return out
@@ -58,6 +58,7 @@ const TEXT = {
noPills: "이 열 일위대가에 단가산출 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
pillsFailed: "일위대가 알약을 못 받음 — {value}",
discardCards: "카드(도면 · 면적산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
baseNote: "(기준)",
};
/** 집계표 줄 열쇠 → 문서 */
@@ -73,6 +74,13 @@ const fill = (text: string, value: string): string => text.replace("{value}", va
/** `{열 id: {도번, 면적번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */
export type ColumnNumbers = Record<string, StructureNumbers>;
/** 집계표 「일위대가 합계」 줄 글(86-3) — 반영합 · 수량이 없으면 기준합 + 「(기준)」 · 계산값 없음 = 빈 글(「—」) */
export function laborSumText(numbers: StructureNumbers | undefined): string {
if (typeof numbers?.일위대가합계 !== "number") return "";
const text = numbers.일위대가합계.toLocaleString("ko-KR", { maximumFractionDigits: 0 });
return numbers.합계기준 ? `${text} ${TEXT.baseNote}` : text;
}
/** 집계표 열 — 머리 글(제목행 종류 · 분류1 · 분류2 · 분류3) · 꼴만 씀 */
export interface StructureColumn {
id: string;
@@ -183,6 +191,8 @@ export interface StructurePanelOptions {
/** [도면 수정] · [면적산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶
* (페이지의 `open`) · label 은 화면 제목에 씀 */
onEdit: (column: string, label: string, kind: CardKind) => void;
/** 번호 · 일위대가 합계를 다시 읽음(카드 [저장] 뒤 · 86-3) — 집계표 줄 글을 새로 그림 */
onNumbers?: () => void;
/** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */
confirmLeave: () => Promise<boolean>;
/** 카드 고침을 버리지 않고 머묾 — 표의 고른 열을 되돌림(31-3) */
@@ -221,6 +231,15 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
editable: () => opts.isAdmin,
onEdit: (kind) => void openEdit(kind),
numberOf: (kind, id) => numbers[id]?.[PART_FIELD[kind]],
// 카드 [저장] 뒤 — 서버가 일위대가 계산값을 다시 적었을 수 있어 번호 · 합계를 다시 읽음(86-3)
onSaved: () =>
void fetchStructureNumbers().then(
(got) => {
numbers = got;
opts.onNumbers?.();
},
() => undefined,
),
// 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」
warnOnFail: (id) => !!numbers[id],
// 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 집계표 줄 「조회 후 선택」 과 같은 길
@@ -22,6 +22,7 @@ import {
comboMerge,
comboView,
createStructurePanel,
laborSumText,
pillsFor,
STRUCTURE_TABLE,
type StructureColumn,
@@ -262,6 +263,7 @@ export function createMain(
isAdmin,
confirmLeave,
keepColumn: (colId) => sheet?.selectColumn(colId), // 카드 고침을 안 버림 — 고른 열 되돌림
onNumbers: () => sheet?.recalc(), // 카드 [저장] 뒤 번호 · 일위대가 합계 줄을 새로(86-3)
// 같은 페이지에서 그 열을 CAD·스프레드시트 전체 편집으로 엶
onEdit: (column, label, kind) => {
void open({ layer, projectId: null, kind, name: column, label });
@@ -357,6 +359,7 @@ export function createMain(
laborLabel: (colId: string) => struct.numbers()[colId]?.일위대가번 ?? "",
drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "",
areaLabel: (colId: string) => struct.numbers()[colId]?.면적번 ?? "",
laborSumLabel: (colId: string) => laborSumText(struct.numbers()[colId]), // 86-3
combo: {
...(readOnly || !extras.onHeadRename
? {}