- 탭 = 기번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
276 lines
9.1 KiB
TypeScript
276 lines
9.1 KiB
TypeScript
/* =============================================================================
|
||
* B09_Estimation_UI_Tabs_Base.ts
|
||
* B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 ·
|
||
* 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`).
|
||
*
|
||
* 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈
|
||
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
|
||
* ========================================================================== */
|
||
|
||
import type { DataRow } from "@ui/ui_template_data_table";
|
||
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
|
||
import {
|
||
money,
|
||
parseNumber,
|
||
qty,
|
||
text,
|
||
type TabBuild,
|
||
type TabId,
|
||
type TabTable,
|
||
} from "./B09_Estimation_UI_Tabs";
|
||
|
||
const LINK_HINT = "줄을 누르면 단가를 낸 탭 · 줄로 이동";
|
||
|
||
/** 단가 줄 표(노무비 · 경비목록표) — 호표 · 명칭 · 규격 · 단위 · 단가 · 비고(출처) */
|
||
function priceList(rows: Row[], unitPrice: string, hint: string): TabTable {
|
||
return {
|
||
columns: [
|
||
qty("no", "호표", 0),
|
||
text("name", "명칭", 200),
|
||
text("spec", "규격", 140),
|
||
text("unit", "단위", 56),
|
||
money("price", unitPrice),
|
||
text("source", "비고", 220),
|
||
],
|
||
rows: rows.map((r) => ({ ...r })),
|
||
frozen: 2,
|
||
hint,
|
||
};
|
||
}
|
||
|
||
export const buildLabor: TabBuild = ({ sheets, payload }) =>
|
||
priceList(
|
||
sheets.labor || [],
|
||
"단가(원/인)",
|
||
`노임 = 굳힌 단가(${madeText(payload)}) · M01 인력 · 바꾸려면 [단가 갱신]`,
|
||
);
|
||
|
||
export const buildExpense: TabBuild = ({ sheets, payload }) =>
|
||
priceList(
|
||
sheets.expense || [],
|
||
"기준가(천원)",
|
||
`기계 손료 기준가 = 굳힌 단가(${madeText(payload)}) · M01 기계 가격 · 바꾸려면 [단가 갱신]`,
|
||
);
|
||
|
||
function madeText(payload: Row): string {
|
||
return payload.book?.made ? `${String(payload.book.made).slice(0, 10)} 굳힘` : "아직 안 굳힘";
|
||
}
|
||
|
||
export const buildBasis: TabBuild = ({ sheets }): TabTable => {
|
||
const basis: Row = sheets.basis || { fx: [], drivers: [] };
|
||
const rows: DataRow[] = [
|
||
...basis.fx.map((f: Row) => ({
|
||
part: "1. 환율",
|
||
key: f.key,
|
||
name: f.name,
|
||
spec: f.date,
|
||
unit: f.unit,
|
||
day: null,
|
||
factor: "",
|
||
value: f.value,
|
||
})),
|
||
...basis.drivers.map((d: Row) => ({
|
||
part: "2. 인건비",
|
||
key: d.key,
|
||
name: d.name,
|
||
spec: `직종 ${d.key}`,
|
||
unit: "원/시간",
|
||
day: d.day,
|
||
factor: d.factor,
|
||
value: d.hour,
|
||
labor_key: d.labor_key,
|
||
})),
|
||
];
|
||
return {
|
||
columns: [
|
||
text("part", "구분", 90),
|
||
text("name", "명칭", 160),
|
||
text("spec", "규격 · 기준일", 120),
|
||
text("unit", "단위", 80),
|
||
money("day", "일당(원)"),
|
||
text("factor", "시간 환산", 160),
|
||
money("value", "값", undefined, 2),
|
||
],
|
||
rows,
|
||
frozen: 2,
|
||
hint: "운전사 시간 노임 = ROUNDDOWN(일당 × 1/8 × 16/12 × 25/20, 0)(M01 CC000336 · 건협기술-2230) · 환율 = M01 한국은행 · 인건비 줄 누름 = 노무비목록표",
|
||
link: (i) => {
|
||
const row = rows[i];
|
||
return row?.labor_key ? { tab: "labor", key: String(row.labor_key) } : null;
|
||
},
|
||
};
|
||
};
|
||
|
||
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: null };
|
||
|
||
export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||
const rows: Row[] = sheets.compare || [];
|
||
const out: DataRow[] = rows.map((r) => {
|
||
const row: DataRow = {
|
||
no: r.no,
|
||
key: r.key,
|
||
name: r.name,
|
||
spec: r.spec,
|
||
unit: r.unit,
|
||
lowest: r.lowest,
|
||
applied: r.applied,
|
||
applied_from: r.applied_from === "사용자" ? "사용자" : (r.applied_from ?? ""),
|
||
supply: r.supply ?? "",
|
||
};
|
||
for (const s of SOURCES) {
|
||
row[`p:${s}`] = r.prices[s];
|
||
row[`g:${s}`] = r.pages[s] ?? "";
|
||
}
|
||
return row;
|
||
});
|
||
const mine = (key: string): Row => ({ ...((user.materials || {})[key] || {}) });
|
||
const put = (key: string, cell: Row): void => {
|
||
const materials = { ...(user.materials || {}) };
|
||
if (Object.keys(cell).length) materials[key] = cell;
|
||
else delete materials[key];
|
||
setUser({ ...user, materials });
|
||
};
|
||
return {
|
||
columns: [
|
||
qty("no", "번호", 0),
|
||
text("name", "품명", 180),
|
||
text("spec", "규격", 140),
|
||
text("unit", "단위", 56),
|
||
...SOURCES.flatMap((s) => [money(`p:${s}`, "단가", [s]), text(`g:${s}`, "면", 64, [s])]),
|
||
money("lowest", "최저"),
|
||
money("applied", "적용 단가", ["적용"]),
|
||
text("applied_from", "출처", 80, ["적용"]),
|
||
text("supply", "관급 · 별도", 80),
|
||
],
|
||
rows: out,
|
||
frozen: 2,
|
||
hint: "적용 단가 = 출처 중 최저(0 · 빈칸 뺌) 자동 · 칸에 넣으면 사용자 값 · 지우면 다시 최저 · 「관급 · 별도」 칸 누름 = 없음 → 관급 → 별도",
|
||
editable: (_i, key) => key === "applied",
|
||
onEdit: (i, key, value) => {
|
||
const row = out[i];
|
||
if (!row || key !== "applied") return;
|
||
const v = parseNumber(value);
|
||
if (v === undefined) return;
|
||
const cell = mine(String(row.key));
|
||
if (v === null) delete cell.applied;
|
||
else cell.applied = v;
|
||
put(String(row.key), cell);
|
||
},
|
||
onCell: (i, key) => {
|
||
const row = out[i];
|
||
if (!row || key !== "supply") return;
|
||
const cell = mine(String(row.key));
|
||
const next = SUPPLY_NEXT[String(row.supply ?? "")] ?? null;
|
||
if (next) cell.supply = next;
|
||
else delete cell.supply;
|
||
put(String(row.key), cell);
|
||
},
|
||
};
|
||
};
|
||
|
||
export const buildMaterial: TabBuild = ({ sheets }): TabTable => {
|
||
const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({
|
||
...r,
|
||
supply: r.supply ?? "",
|
||
}));
|
||
return {
|
||
columns: [
|
||
qty("no", "호표", 0),
|
||
text("name", "명칭", 200),
|
||
text("spec", "규격", 160),
|
||
text("unit", "단위", 56),
|
||
money("price", "단가"),
|
||
text("supply", "관급 · 별도", 80),
|
||
text("origin", "비고", 90),
|
||
],
|
||
rows,
|
||
frozen: 2,
|
||
hint: `단가 = 자재단가대비표 적용 단가 · ${LINK_HINT}`,
|
||
link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null),
|
||
};
|
||
};
|
||
|
||
export const buildMachine: TabBuild = ({ sheets }): TabTable => {
|
||
const rows: DataRow[] = (sheets.machine || []).map((r: Row) => ({ ...r }));
|
||
return {
|
||
columns: [
|
||
qty("no", "호표", 0),
|
||
text("name", "명칭", 200),
|
||
text("spec", "규격", 120),
|
||
text("unit", "단위", 56),
|
||
money("sum", "합계"),
|
||
money("labor", "노무비"),
|
||
money("material", "재료비"),
|
||
money("expense", "경비"),
|
||
],
|
||
rows,
|
||
frozen: 2,
|
||
hint: `시간당 = 중기사용료 블록 합계 · ${LINK_HINT}`,
|
||
link: (i) => (rows[i] ? { tab: "machine_blocks", key: String(rows[i].key) } : null),
|
||
};
|
||
};
|
||
|
||
const KIND_TAB: Record<string, TabId> = {
|
||
expense: "expense",
|
||
labor: "basis",
|
||
material: "compare",
|
||
};
|
||
|
||
export const buildMachineBlocks: TabBuild = ({ sheets }): TabTable => {
|
||
const rows: DataRow[] = [];
|
||
const links: Array<{ tab: TabId; key: string } | null> = [];
|
||
(sheets.machine_blocks || []).forEach((b: Row, n: number) => {
|
||
rows.push({ key: b.key, name: `제 ${n + 1} 호표 ${b.name}`, spec: b.spec, unit: b.unit });
|
||
links.push(null);
|
||
for (const ln of b.lines) {
|
||
const at = (part: string): number | null => (ln.kind === part ? ln.price : null);
|
||
const amt = (part: string): number | null => (ln.kind === part ? ln.amount : null);
|
||
rows.push({
|
||
key: `${b.key}|${ln.name}`,
|
||
name: ` ${ln.name}`,
|
||
spec: ln.spec,
|
||
qty: ln.qty,
|
||
unit: ln.unit,
|
||
sum_price: ln.price,
|
||
sum: ln.amount,
|
||
labor_price: at("labor"),
|
||
labor: amt("labor"),
|
||
material_price: at("material"),
|
||
material: amt("material"),
|
||
expense_price: at("expense"),
|
||
expense: amt("expense"),
|
||
});
|
||
links.push(ln.ref?.key ? { tab: KIND_TAB[ln.kind] ?? "compare", key: ln.ref.key } : null);
|
||
}
|
||
rows.push({
|
||
key: `${b.key}|합계`,
|
||
name: " 합 계",
|
||
sum: b.sum,
|
||
labor: b.labor,
|
||
material: b.material,
|
||
expense: b.expense,
|
||
});
|
||
links.push(null);
|
||
});
|
||
const pair = (key: string, label: string) => [
|
||
money(`${key}_price`, "단가", [label], 1),
|
||
money(key, "금액", [label], 1),
|
||
];
|
||
return {
|
||
columns: [
|
||
text("name", "명칭", 220),
|
||
text("spec", "규격", 120),
|
||
qty("qty", "수량"),
|
||
text("unit", "단위", 56),
|
||
...pair("sum", "합계"),
|
||
...pair("labor", "노무비"),
|
||
...pair("material", "재료비"),
|
||
...pair("expense", "경비"),
|
||
],
|
||
rows,
|
||
frozen: 1,
|
||
hint: "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가 · 잡품 = 연료 금액 × % · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 · 줄 누름 = 단가를 낸 탭",
|
||
link: (i) => links[i] ?? null,
|
||
};
|
||
};
|