Files
Aislo/B09_Estimation/B09_Estimation_UI_Check.ts
T
eomsangdonandClaude Opus 5.5 21bc4f856a feat(B09): 첫 화면 = 설계내역서 · 빈 줄 수 하나(PLAN 701-2)
- 첫 탭 = 설계내역서(빈 원가계산서 아님) · 엑셀 탭 차례는 그대로
- 빈 줄 = 설계내역서 금액 모르는 직접공사 항목 줄(blankRows) — 설계내역서 안내 · 데이터 점검 · 집계표 넷이 같은 수 · 같은 낱말
- 데이터 점검 = 「빈 줄 n」(줄마다 하나 · 까닭 모음) + 「그 밖 확인 n곳」(호표 · 재료 · 탭 · 키마다 하나)
- test_203_1 낡은 값(첫 탭 · 「연결 없음 n곳」) 갱신 · helper 에 593-1 시각 글 stub(dateTimeText 없어 깨져 있던 것)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:05 +09:00

126 lines
5.3 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Check.ts
* B09 좌측 「데이터 점검」 묶음(PLAN 203-1) — [업데이트] · 연결 점검(옛 「연결 점검」 탭 내용).
*
* [업데이트] = 단가를 지금 M01 값으로 새로 굳힘 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
* 연결 점검 = 「빈 줄 n」(설계내역서 금액 모르는 줄 · 안내 · 집계표와 같은 수 · 701-2) 목록 +
* 그 밖 확인 목록(일위대가 · 단가산출 · 재료 등 · 탭 · 키마다 하나)(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
* ========================================================================== */
import { attachHoverInfo } from "@ui/ui_template_hover_info";
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 { blankRows, tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
export interface CheckItem {
link: Link;
tab: string;
name: string;
why: string;
}
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, others, notes };
}
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
export function refreshTip(payload: Row | null): string {
const made = payload?.book?.made;
const when = made ? `단가 굳힘 ${dateTimeText(String(made))}` : "단가 아직 안 굳힘";
const stale = payload?.stale;
return `${when} · 누르면 지금 M01 값으로 새로 굳힘${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
}
export interface CheckPanel {
/** 묶음 안 연결 점검 자리 */
list: HTMLElement;
draw: (payload: Row, sheets: Row) => void;
}
export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => void): CheckPanel {
let payload: Row | null = null;
let items: CheckItem[] = [];
attachHoverInfo(refresh, () => refreshTip(payload));
const box = document.createElement("div");
box.className = "b09-check";
attachHoverInfo(box, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
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(
...group.map((item, n) => {
const row = document.createElement("button");
row.type = "button";
row.className = "ui-sidebar-row";
row.dataset.i = String(offset + n);
const name = document.createElement("span");
name.textContent = item.name;
const tab = createBadge(item.tab, "neutral", { size: 18 });
tab.classList.add("b09-check__tab");
row.append(name, tab);
row.addEventListener("click", () => goto(item.link));
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;
});
box.replaceChildren(...blank, ...others, ...notes);
};
return { list: box, draw };
}