- 탭 = 기번3 공통 엑셀 보이는 탭 차례(목차 · 안내 = 연결 점검) · 탭 하나 = 공용 표 하나 · 호표 칸 누름 = 그 탭 그 줄로 이동 - 저장 = 프로젝트 B09_Estimation/v1(사용자 값 · 굳힌 단가 한 벌) · 결과는 GET 때 서버 셈 · [저장] · [단가 갱신] 길 - 줄 = 수요 따라(연결된 사본 원천 자리 · 기계 → 운전사 · 연료 · B08 총괄 자재) · 연결 없으면 빈칸 + 연결 점검 목록 - 셈 = 파이썬 · TS 짝 + 거울(운전사 시간 노임 · 자재 최저 자동 + 사용자 고침 · 관급 · 별도 · 중기 손료 + 운전사 + 연료 + 잡품 · 엑셀 끝수) - 공용 표 = 칸 누름 · 줄로 이동(강조) 더함 - 시험 = test_144_b09_skeleton · test_144_b09_base_calc(엑셀 중기 126,000 · 76,436 · 96,829 · 40,659 · 운전사 55,700 · 47,231 · 33,571) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Tabs.ts
|
|
* B09 탭 목록 · 표 짜기 공용(PLAN 144-1) — 탭 하나 = 화면 표 하나(실무 엑셀 탭 차례).
|
|
*
|
|
* 탭 = 연결 점검(목차 · 안내) · 원가 · 총괄 · 내역 · 대가 둘 · 산근 둘 · 기초 · 중기 둘 · 단가 넷 · 집계표 넷
|
|
* 표 = 공용 `createDataTable` · 「연결 없음」 칸 = 빈칸 · 호표 칸 누름 = 그 탭 그 줄로 이동(`link`)
|
|
* 탭별 표 = `…_UI_Tabs_Base`(144) · `…_UI_Tabs_Cost`(145) · 여기는 차례 · 꼴 · 연결 점검만
|
|
* ========================================================================== */
|
|
|
|
import type { DataColumn, DataRow } from "@ui/ui_template_data_table";
|
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
|
|
|
export type TabId =
|
|
| "check"
|
|
| "cost"
|
|
| "total"
|
|
| "bill"
|
|
| "dg_list"
|
|
| "dg"
|
|
| "sg_list"
|
|
| "sg"
|
|
| "basis"
|
|
| "machine"
|
|
| "machine_blocks"
|
|
| "material"
|
|
| "labor"
|
|
| "expense"
|
|
| "compare"
|
|
| "sum_material"
|
|
| "sum_labor"
|
|
| "sum_expense"
|
|
| "sum_machine";
|
|
|
|
/** 엑셀 탭 차례 — [id, 탭 이름] */
|
|
export const TABS: Array<[TabId, string]> = [
|
|
["check", "연결 점검"],
|
|
["cost", "공사원가계산서"],
|
|
["total", "총괄설계내역서"],
|
|
["bill", "설계내역서"],
|
|
["dg_list", "일위대가목록표"],
|
|
["dg", "일위대가표"],
|
|
["sg_list", "단가산출근거목록표"],
|
|
["sg", "단가산출근거"],
|
|
["basis", "환율및기초자료"],
|
|
["machine", "중기목록표"],
|
|
["machine_blocks", "중기사용료"],
|
|
["material", "재료비목록표"],
|
|
["labor", "노무비목록표"],
|
|
["expense", "경비목록표"],
|
|
["compare", "자재단가대비표"],
|
|
["sum_material", "재료비수량금액집계표"],
|
|
["sum_labor", "노무비수량금액집계표"],
|
|
["sum_expense", "경비수량금액집계표"],
|
|
["sum_machine", "중기시간금액집계표"],
|
|
];
|
|
|
|
export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1] ?? id;
|
|
|
|
/** 다른 탭 줄 — 키가 같은 줄로 이동 */
|
|
export interface Link {
|
|
tab: TabId;
|
|
key: string;
|
|
}
|
|
|
|
/** 페이지가 탭 짜기에 주는 것 */
|
|
export interface TabContext {
|
|
sheets: Row;
|
|
payload: Row;
|
|
user: Row;
|
|
/** 사용자 값 고침 → 캐시 · 바로 다시 셈 */
|
|
setUser: (user: Row) => void;
|
|
}
|
|
|
|
/** 탭 한 장 = 표 하나 */
|
|
export interface TabTable {
|
|
columns: DataColumn[];
|
|
rows: DataRow[];
|
|
total?: DataRow | null;
|
|
frozen?: number;
|
|
/** 표 위 안내 글(근거 · 끝수) */
|
|
hint?: string;
|
|
/** 칸 누름 → 갈 줄(호표 칸) */
|
|
link?: (index: number, key: string) => Link | null;
|
|
editable?: (index: number, key: string) => boolean;
|
|
onEdit?: (index: number, key: string, text: string) => void;
|
|
/** 칸 누름 → 그 자리 동작(예 관급 · 별도 표시 바꿈) */
|
|
onCell?: (index: number, key: string) => void;
|
|
}
|
|
|
|
export type TabBuild = (ctx: TabContext) => TabTable | string;
|
|
|
|
/* ── 열 꼴 ────────────────────────────────────────────────────────────── */
|
|
|
|
export const text = (key: string, label: string, width?: number, group?: string[]): DataColumn => ({
|
|
key,
|
|
label,
|
|
width,
|
|
group,
|
|
});
|
|
|
|
export const money = (key: string, label: string, group?: string[], decimals = 0): DataColumn => ({
|
|
key,
|
|
label,
|
|
group,
|
|
kind: "number",
|
|
decimals,
|
|
width: 96,
|
|
});
|
|
|
|
export const qty = (key: string, label: string, decimals = 4, group?: string[]): DataColumn => ({
|
|
key,
|
|
label,
|
|
group,
|
|
kind: "number",
|
|
decimals,
|
|
trim: true,
|
|
width: 76,
|
|
});
|
|
|
|
/** 글 → 값 · 빈칸 = null · 수가 아니면 undefined(무시) */
|
|
export function parseNumber(value: string): number | null | undefined {
|
|
const t = value.replace(/,/g, "").trim();
|
|
if (!t) return null;
|
|
const v = Number(t);
|
|
return Number.isFinite(v) ? v : undefined;
|
|
}
|
|
|
|
/** 줄 키로 줄 번호 찾기(이동) */
|
|
export function rowIndex(rows: DataRow[], key: string): number {
|
|
return rows.findIndex((row) => row.key === key);
|
|
}
|
|
|
|
/* ── 연결 점검(목차 · 안내) ───────────────────────────────────────────── */
|
|
|
|
export const buildCheck: TabBuild = ({ sheets, payload }): TabTable => {
|
|
const gaps: Row[] = sheets.gaps || [];
|
|
const rows: DataRow[] = gaps.map((g, i) => ({
|
|
no: i + 1,
|
|
key: `${g.tab}|${g.key}|${i}`,
|
|
tab: tabName(g.tab),
|
|
name: g.name,
|
|
why: g.why,
|
|
}));
|
|
const notes: string[] = [...(payload.notes || [])];
|
|
const missing: string[] = payload.inputs?.missing || [];
|
|
if (missing.length) notes.push(`M01 에 없는 키 ${missing.length} — ${missing.join(" · ")}`);
|
|
return {
|
|
columns: [
|
|
qty("no", "번호", 0),
|
|
text("tab", "탭", 150),
|
|
text("name", "명칭", 260),
|
|
text("why", "까닭", 320),
|
|
],
|
|
rows,
|
|
frozen: 1,
|
|
hint:
|
|
`연결 없음 ${gaps.length}곳 — 줄을 누르면 그 탭 그 줄로 · 빈칸 = 값을 낸 원천이 없음(0 을 채우지 않음)` +
|
|
(notes.length ? ` · ${notes.join(" · ")}` : ""),
|
|
link: (i) => {
|
|
const g = gaps[i];
|
|
return g ? { tab: g.tab as TabId, key: g.key } : null;
|
|
},
|
|
};
|
|
};
|