Merge remote-tracking branches 'origin/sub_laptop_5' and 'origin/sub_laptop_6' into sub_laptop_7

This commit is contained in:
2026-10-31 13:06:38 +09:00
7 changed files with 116 additions and 59 deletions
+58 -26
View File
@@ -4,7 +4,8 @@
*
* [업데이트] = 단가를 지금 M01 값으로 새로 굳힘 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
* 연결 점검 = 「연결 없음」 줄 목록(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
* 연결 점검 = 「빈 줄 n」(설계내역서 금액 모르는 줄 · 안내 · 집계표와 같은 수 · 701-2) 목록 +
* 그 밖 확인 목록(일위대가 · 단가산출 · 재료 등 · 탭 · 키마다 하나)(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
* ========================================================================== */
import { attachHoverInfo } from "@ui/ui_template_hover_info";
@@ -12,7 +13,7 @@ import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import type { Row } from "./B09_Estimation_Calc_Base";
import { dateTimeText } from "../common_util/common_util_time";
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
import { blankRows, tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
export interface CheckItem {
link: Link;
@@ -21,19 +22,36 @@ export interface CheckItem {
why: string;
}
/** 빠진 연결 줄 · 알림 글(서버 알림 + M01 에 없는 키) */
export function checkItems(sheets: Row, payload: Row): { items: CheckItem[]; notes: string[] } {
const gaps: Row[] = sheets.gaps || [];
const items = gaps.map((g) => ({
link: { tab: g.tab as TabId, key: g.key },
tab: tabName(g.tab),
name: String(g.name ?? ""),
why: String(g.why ?? ""),
}));
const BLANK_WHY = "금액 모름 — 단가연결 칸 램프 · 이은 호표의 빈 줄을 확인";
/** 빈 줄(설계내역서 줄마다 하나 · 까닭 모음) · 그 밖 확인 줄(탭 · 키마다 하나) · 알림 글 */
export function checkItems(
sheets: Row,
payload: Row,
): { items: CheckItem[]; others: CheckItem[]; notes: string[] } {
const why = new Map<string, string[]>();
const order: string[] = [];
const named = new Map<string, Row>();
for (const g of (sheets.gaps || []) as Row[]) {
const id = `${g.tab}|${g.key}`;
if (!why.has(id)) (why.set(id, []), order.push(id), named.set(id, g));
const list = why.get(id)!;
if (g.why && !list.includes(String(g.why))) list.push(String(g.why));
}
const item = (tab: string, key: string, name: unknown, reasons?: string[]): CheckItem => ({
link: { tab: tab as TabId, key },
tab: tabName(tab),
name: String(name ?? ""),
why: reasons?.length ? reasons.join(" · ") : BLANK_WHY,
});
const items = blankRows(sheets).map((r) => item("bill", r.key, r.name, why.get(`bill|${r.key}`)));
const others = order
.filter((id) => !id.startsWith("bill|"))
.map((id) => item(named.get(id)!.tab, named.get(id)!.key, named.get(id)!.name, why.get(id)));
const notes: string[] = [...(payload.notes || [])];
const missing: string[] = payload.inputs?.missing || [];
if (missing.length) notes.push(`M01 에 없는 키 ${missing.length} — ${missing.join(" · ")}`);
return { items, notes };
return { items, others, notes };
}
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
@@ -54,25 +72,24 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
let payload: Row | null = null;
let items: CheckItem[] = [];
attachHoverInfo(refresh, () => refreshTip(payload));
const list = document.createElement("div");
list.className = "b09-check";
attachHoverInfo(list, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
const box = document.createElement("div");
box.className = "b09-check";
attachHoverInfo(box, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
const draw = (next: Row, sheets: Row): void => {
payload = next;
refresh.classList.toggle("is-pending", Boolean(next.stale));
const made = checkItems(sheets, next);
items = made.items;
const head = createText(`연결 없음 ${items.length}곳`, "muted");
head.classList.add("b09-check__head");
const head = (text: string): HTMLElement => {
const p = createText(text, "muted");
p.classList.add("b09-check__head");
return p;
};
const list = (group: CheckItem[], offset: number): HTMLElement => {
const rows = document.createElement("div");
rows.className = "ui-sidebar-list";
rows.append(
...items.map((item, i) => {
...group.map((item, n) => {
const row = document.createElement("button");
row.type = "button";
row.className = "ui-sidebar-row";
row.dataset.i = String(i);
row.dataset.i = String(offset + n);
const name = document.createElement("span");
name.textContent = item.name;
const tab = createBadge(item.tab, "neutral", { size: 18 });
@@ -82,12 +99,27 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
return row;
}),
);
return rows;
};
const draw = (next: Row, sheets: Row): void => {
payload = next;
refresh.classList.toggle("is-pending", Boolean(next.stale));
const made = checkItems(sheets, next);
items = [...made.items, ...made.others];
const blank = [
head(`빈 줄 ${made.items.length}`),
...(made.items.length ? [list(made.items, 0)] : []),
];
const others = made.others.length
? [head(`그 밖 확인 ${made.others.length}곳`), list(made.others, made.items.length)]
: [];
const notes = made.notes.map((text) => {
const p = createText(text, "muted");
p.classList.add("b09-check__note");
return p;
});
list.replaceChildren(head, ...(items.length ? [rows] : []), ...notes);
box.replaceChildren(...blank, ...others, ...notes);
};
return { list, draw };
return { list: box, draw };
}
+2 -9
View File
@@ -38,14 +38,7 @@ import { createCheckPanel } from "./B09_Estimation_UI_Check";
import { tableOptionsOf, tabTableOf } from "./B09_Estimation_UI_Builders";
import { createDock, linkMode } from "./B09_Estimation_UI_Dock";
import { closePopups, openRowPopup } from "./B09_Estimation_UI_Popup";
import {
DRAFT,
rowIndex,
TABS,
type Link,
type TabContext,
type TabId,
} from "./B09_Estimation_UI_Tabs";
import { DRAFT, rowIndex, type Link, type TabContext, type TabId } from "./B09_Estimation_UI_Tabs";
import { attachRowMenu, openMenuAt, type BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
import { jumpRange, jumpTo, levelBar, pickedClass } from "./B09_Estimation_UI_Tabs_Bill_Level";
import {
@@ -127,7 +120,7 @@ export function renderB09Estimation(root: HTMLElement): void {
let payload: Row | null = null;
let user: Row = {};
let sheets: Row = {};
let current: TabId = TABS[0][0];
let current: TabId = "bill"; // 701-2 첫 화면 = 설계내역서(빈 원가계산서 아님)
// 181-1 위 탭(md) · 아래 탭(sm) 두 줄 — 줄 그리기 = `…_UI_TabBar`
const bars = createTabBars((tab) => show(tab));
+6
View File
@@ -56,6 +56,12 @@ export const TABS: Array<[TabId, string]> = [
export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1] ?? id;
/** 빈 줄(701-2) = 설계내역서 금액 모르는 직접공사 항목 줄 — 안내 · 데이터 점검 · 집계표 · 원가계산서가 같은 수 */
export const blankRows = (sheets: Row): Row[] =>
((sheets?.bill?.rows || []) as Row[]).filter(
(r) => r.kind === "item" && r.cost === "direct" && r.sum === null,
);
/** 위 탭(181-1) — [위 탭 이름, 아래 탭들] · 아래 탭 하나뿐 = 위 탭 단독 · 첫 아래 탭 = 위 탭 키 */
export const TAB_GROUPS: Array<[string, TabId[]]> = [
["공사원가계산서", ["cost"]],
@@ -129,6 +129,7 @@ function foldCard(ctx: TabContext, row: Row, block: Row, close: () => void): HTM
frozen: 1,
empty: "",
editOnDouble: true,
onCellClick: () => undefined, // 줄 번호 표식(data-i) — 두 번 눌러 고침 · 우클릭 · 풍선 자리
editable: (i, key) => {
const at = lines[i];
if (!at?.cover || !(key in FIELD)) return false;
@@ -10,6 +10,7 @@
import type { DataRow } from "@ui/ui_template_data_table";
import type { Row } from "./B09_Estimation_Calc_Base";
import {
blankRows,
money,
qty,
text,
@@ -20,17 +21,17 @@ import {
const HINT = "줄을 누르면 목록표로 이동";
/** 안내 + 「연결 없음 n」(전개 안 한 내역 항목 — 조합 밖 토공 · 운반 산근 등) */
const hint = (payload: Row, extra = ""): string => {
const n = (payload.summary || {}).unlinked || 0;
return `${HINT}${extra}${n ? ` · 연결 없음 ${n} — 전개 안 함(좌측 데이터 점검)` : ""}`;
/** 안내 + 「빈 줄 n」(설계내역서와 같은 수 · 701-2 — 금액 모르는 줄은 집계에서 빠짐) */
const hint = (sheets: Row, extra = ""): string => {
const n = blankRows(sheets).length;
return `${HINT}${extra}${n ? ` · 빈 줄 ${n} — 집계에서 빠짐(좌측 데이터 점검)` : ""}`;
};
/** 줄이 없을 때 표 위 맨 앞 안내 */
const EXPENSE_NONE = "경비 자원 없음";
function resourceTab(id: string, list: TabId, unitPrice: string, none = ""): TabBuild {
return ({ payload }): TabTable => {
return ({ payload, sheets }): TabTable => {
const rows: DataRow[] = ((payload.summary || {})[id] || []).map((r: Row) => ({ ...r }));
const sum = rows.reduce((acc, r) => acc + (typeof r.amount === "number" ? r.amount : 0), 0);
return {
@@ -47,7 +48,7 @@ function resourceTab(id: string, list: TabId, unitPrice: string, none = ""): Tab
rows,
total: rows.length ? { name: "계", amount: sum } : null,
frozen: 2,
hint: !rows.length && none ? `${none} · ${hint(payload)}` : hint(payload),
hint: !rows.length && none ? `${none} · ${hint(sheets)}` : hint(sheets),
link: (i) => (rows[i] ? { tab: list, key: String(rows[i].key) } : null),
};
};
@@ -57,7 +58,7 @@ export const buildSumMaterial = resourceTab("sum_material", "material", "단가"
export const buildSumLabor = resourceTab("sum_labor", "labor", "단가");
export const buildSumExpense = resourceTab("sum_expense", "expense", "단가", EXPENSE_NONE);
export const buildSumMachine: TabBuild = ({ payload }): TabTable => {
export const buildSumMachine: TabBuild = ({ payload, sheets }): TabTable => {
const rows: DataRow[] = ((payload.summary || {}).sum_machine || []).map((r: Row) => ({ ...r }));
const add = (key: string) =>
rows.reduce((acc, r) => acc + (typeof r[key] === "number" ? (r[key] as number) : 0), 0);
@@ -85,7 +86,7 @@ export const buildSumMachine: TabBuild = ({ payload }): TabTable => {
}
: null,
frozen: 2,
hint: hint(payload),
hint: hint(sheets),
link: (i) => (rows[i] ? { tab: "machine", key: String(rows[i].key) } : null),
};
};