feat(b09): 총괄설계내역서 맨 위 설계내역서 레벨 1 · 2 집계 · 누름 = 설계내역서 그 줄 빨강 (239-1)

- 총괄 표 맨 위 = 설계내역서 레벨 1 · 2 폴더 줄(관급 빼고 · 레벨 1 항목 포함) · 금액 = 설계내역서 셈 그대로 · 끝 = 순공사비 계
- 줄 누름 = 설계내역서 탭 그 줄로 · 이동해 온 줄 = is-pick 빨강 외곽선(고른 줄 상자와 따로)
- 새 시험 test_239_1 · 200_2 셸 배선 문구 갱신

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
2026-10-10 20:05:05 +09:00
co-authored by Claude Opus 5.5
parent be1c707da0
commit dd360094b3
4 changed files with 171 additions and 9 deletions
+10 -6
View File
@@ -61,7 +61,7 @@ import {
} from "./B09_Estimation_UI_Tabs_Summary";
import { buildCost, buildTotal } from "./B09_Estimation_UI_Tabs_Total";
import { buildSg, buildSgList } from "./B09_Estimation_UI_Tabs_Sangeun";
import { isGroupTab, lineHint, renderGroupPanel } from "./B09_Estimation_UI_Tabs_Groups";
import { isGroupTab, lineHint, pickClass, renderGroupPanel } from "./B09_Estimation_UI_Tabs_Groups";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
@@ -262,15 +262,19 @@ export function renderB09Estimation(root: HTMLElement): void {
);
}
// 200-2 설계내역서 = 고른 줄 상자(235-1) · 229-1 레벨 단추 = 보조 탭 줄 오른쪽 · 202-1 안내 줄 = 표 아래 · Enter · F2 = 입력
// 239-1 이동해 온 줄 = 빨강 외곽선(목록표 → 탭 길 is-pick · 고른 줄 상자와 따로)
const bill = (spec as BillTable).menu ? (spec as BillTable) : null;
const table = makeTable(bill ? (i) => pickedClass(bill.rows[i]) : undefined);
const at = focus === undefined ? -1 : rowIndex(spec.rows, focus);
const jump: [number, number] | null = at >= 0 ? [at, at] : null;
const table = makeTable(
bill
? (i) => [pickedClass(bill.rows[i]), pickClass(jump, i)].filter(Boolean).join(" ") || null
: undefined,
);
const level = bill ? levelBar(table.root, bill, (i) => table.edit(i)) : null;
if (level) bars.setTools(level.tools);
panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : []));
if (focus !== undefined) {
const index = rowIndex(spec.rows, focus);
if (index >= 0) requestAnimationFrame(() => table.focusRow(index));
}
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
};
const apply = (next: Row, fromServer: boolean): void => {
+41 -2
View File
@@ -2,7 +2,8 @@
* B09_Estimation_UI_Tabs_Total.ts
* B09 총괄설계내역서 · 공사원가계산서 탭(PLAN 145-4) — 실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Total`.
*
* 총괄 = 가. 순공사비계(설계내역서 비목 합 · 145-3) + 제비율 줄 + 소계(나~바)
* 총괄 = 맨 위 내역 줄(239-1 · 설계내역서 레벨 1 · 2 집계 + 순공사비 계 · 누름 = 설계내역서 그 줄 빨강) +
* 가. 순공사비계(설계내역서 비목 합 · 145-3) + 제비율 줄 + 소계(나~바)
* 맨 위 = 요율 고르는 값(추정가격 · 공사기간 · 환경보전비 공종 — 누르면 도로 ↔ 그 밖)(166-1)
* 요율 = 자동(M01 조달청 제비율 · 짝 셈이 고름) · 사용자가 적으면 앞섬 · 출처 칸 누름 = 자동으로
* 고칠 칸 = 요율 · 손입력(기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) ·
@@ -84,6 +85,42 @@ const nameLevel = (rows: DataRow[]) => ({
of: (i: number) => rows[i]?.level as number | undefined,
});
/** 239-1 맨 위 내역 줄 = 설계내역서 레벨 1 · 2 폴더 줄(관급 빼고 · 레벨 1 항목도 — 합이 순공사비) ·
* 금액 = 설계내역서 셈 그대로 · 끝 = 순공사비 계 · 누름 = 설계내역서 그 줄(`bill`) */
function billRows(bill: Row | undefined): DataRow[] {
const top = (bill?.rows || []).filter(
(r: Row) => r.cost === "direct" && (r.level === 1 || (r.level === 2 && r.kind !== "item")),
);
if (!top.length) return [];
const direct: Row = bill?.totals?.direct || {};
const amounts = (r: Row) => ({
amount: r.sum ?? null,
labor: r.labor ?? null,
material: r.material ?? null,
expense: r.expense ?? null,
});
const blank = direct.blank ? ` · 빈 줄 ${direct.blank} — 아는 금액만` : "";
return [
...top.map((r: Row) => ({
key: `bill:${r.key}`,
part: "내역",
name: r.no ? `${r.no} ${r.name}` : r.name,
...amounts(r),
note: "누르면 설계내역서 그 줄로",
level: r.level,
bill: r.key,
})),
{
key: "bill_sum",
part: "내역",
name: "순공사비 계",
...amounts(direct),
note: `설계내역서 레벨 1 합(관급 제외)${blank}`,
level: 1,
},
];
}
function rateText(formula: string, rate: Num): string {
return rate === null ? `${formula}(요율 빈칸)` : formula.replace("율", `${rate}%`);
}
@@ -139,6 +176,7 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
const sums: Row = t.sums || {};
const env = est.environment_kind === "road" ? "road" : "other";
const rows: DataRow[] = [
...billRows(ctx.sheets.bill),
row("in_price", "요율", "추정가격(원)", null, "일반관리비 · 이윤 · 고용보험 구간", {
hand: hand("in_price"),
}),
@@ -211,12 +249,13 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
rowClass: (i) => levelClass(rows[i]),
level: nameLevel(rows),
mergeSame: ["part"],
hint: "요율 칸에 적으면 사용자 값 · 지우거나 출처 칸을 누르면 자동 값",
hint: "맨 위 내역 줄을 누르면 설계내역서 그 줄로 · 요율 칸에 적으면 사용자 값 · 지우거나 출처 칸을 누르면 자동 값",
editable: (i, key) =>
(key === "rate" && RATE_KEYS.has(String(rows[i]?.key))) ||
(key === "hand" && String(rows[i]?.key) in HAND),
onEdit: (i, key, value) => editEstimate(ctx, String(rows[i]?.key), key, value),
onCell: (i, key) => clickEstimate(ctx, String(rows[i]?.key), key, rows[i]?.from),
link: (i) => (rows[i]?.bill ? { tab: "bill", key: String(rows[i].bill) } : null),
tip: (i, key) => {
const r = rows[i];
if (!r?.rt || (key !== "rate" && key !== "candidate")) return null;