- 줄 표 = 로직 표(차례 · 구분 · 이름 · 별칭 · 수량산출 · 결과 단위 · 노무비 · 재료비 · 경비 · 합계) + 자재 표(세 칸 · 가격 다섯) + 품 비교 · 단추 - 금액 칸 = 위 엑셀 · 아래 우리(줄마다 서버 calc 미리 셈 · 저장 안 함 · 최신 응답만) - 미리 셈 문서 꼴 = logic/M02_MasterTemplete_Hopyo_Draft.ts(정본 = M02_Hopyo_Build · 수량 = 엑셀 × 배 · 별도계상) - 드롭다운 글 추가 · 로직없음 · 사용안함(저장 값 그대로) · [엑셀에서 보기] → [엑셀 보기] · [사전조합 생성] - Check.ts 쪼갬(표 그리기 = Hopyo_CheckRows.ts) · 쓰지 않는 [← 호표 목록] 지움 - 시험 test_m02_hopyo_draft_calc(북부청 실물 줄 → calc 200 · 반영합 · 저장 안 함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
344 lines
14 KiB
TypeScript
344 lines
14 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_CheckRows.ts
|
|
* 확인 창 줄 표(PLAN 359-3) — 구조물 조합 일위대가 상세 꼴(`Labor_Rows` logicTable · matTable)에 품 비교 · 단추만 더함.
|
|
* · 로직 표 = [✓] · 차례 · 구분(엑셀 갈래 + 상태) · 이름(우리 로직 · 배 · 옵션 글) · 별칭(엑셀 품명 = 만들 때 별칭) ·
|
|
* 수량산출(위 엑셀 / 아래 우리 = 엑셀 × 배) · 결과 단위 · 노무비 · 재료비 · 경비 · 합계 · 품 비교 · 단추
|
|
* · 자재 표 = [✓] · 차례 · 코드 · 이름 · 규격 · 수량산출 · 단위 · 세 칸(자재구분 · 별도계상 · 운반비 · 글만) ·
|
|
* 가격 다섯 · 엑셀 / 우리 금액 · 단추
|
|
* · 금액 칸 = 위 작게 엑셀 · 아래 우리(서버 calc 미리 셈 · 저장 안 함) · 셈 중 「…」
|
|
* · 단추 = [고르기] · [원문 보기] · [엑셀 보기] · [옵션](로직 대상마다) · 드롭다운 추가 · 로직없음 · 사용안함
|
|
* (화면 글만 · 저장 값 = 빈 값 · 「우리에 없음」 · 「안 씀」 그대로)
|
|
* 줄 표 = `tableOf`(대상이 모두 자재 · 대상 없는 자재 · 별도계상 줄 = 자재 표).
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el } from "@ui/ui_template_elements";
|
|
import type { HopyoLine, HopyoPart } from "./M02_MasterTemplete_Hopyo_Api";
|
|
import {
|
|
checkable,
|
|
compareText,
|
|
isChecked,
|
|
partsOf,
|
|
type CheckDraft,
|
|
} from "./M02_MasterTemplete_Hopyo_Check";
|
|
import { inputsText, sourceText } from "./M02_MasterTemplete_Hopyo_Choose";
|
|
import type { LaborCalc } from "./M02_MasterTemplete_Labor_Api";
|
|
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
|
|
import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft";
|
|
|
|
/** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */
|
|
export const OUT_TEXT: [string, string][] = [
|
|
["", "추가"],
|
|
["우리에 없음", "로직없음"],
|
|
["안 씀", "사용안함"],
|
|
];
|
|
const COSTS = ["노무비", "재료비", "경비", "계"] as const;
|
|
const COST_HEAD = {
|
|
노무비: "Prev_Labor",
|
|
재료비: "Prev_Material",
|
|
경비: "Prev_Expense",
|
|
계: "Prev_Sum",
|
|
} as const;
|
|
|
|
/** 줄 하나 미리 셈 — 셈 중 = 둘 다 없음 */
|
|
export interface LinePrice {
|
|
calc?: LaborCalc;
|
|
fail?: string;
|
|
}
|
|
|
|
export interface RowsContext {
|
|
canWrite: boolean;
|
|
d: CheckDraft;
|
|
price: (line: HopyoLine) => LinePrice | null;
|
|
check: (line: HopyoLine, on: boolean) => void;
|
|
out: (line: HopyoLine, value: string) => void;
|
|
times: (line: HopyoLine, at: number, value: number | null) => void;
|
|
choose: (line: HopyoLine) => void;
|
|
source: (line: HopyoLine) => void;
|
|
sheet: (행: [number, number]) => void;
|
|
options: (line: HopyoLine, at: number) => void;
|
|
}
|
|
|
|
const num = (v: number): string =>
|
|
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
|
const small = (text: string, cls = "m02-labrow__note"): HTMLElement =>
|
|
el("small", { className: cls, text, attrs: { title: text } });
|
|
|
|
/** 금액 칸 — 위 엑셀(작게) · 아래 우리 */
|
|
function pairCell(
|
|
excel: number | null | undefined,
|
|
ours: number | null | undefined,
|
|
wait: boolean,
|
|
label: string,
|
|
): HTMLElement {
|
|
const td = moneyCell(excel, ours, wait, label);
|
|
td.querySelector(".m02-labrow__base")?.setAttribute("title", "엑셀");
|
|
td.querySelector(".m02-labrow__applied")?.setAttribute("title", "우리(미리 셈)");
|
|
return td;
|
|
}
|
|
|
|
/** 수량산출 — 위 엑셀 수량 · 아래 우리(엑셀 × 배 · 대상마다) */
|
|
function qtyCell(line: HopyoLine, parts: HopyoPart[]): HTMLElement {
|
|
const ours = parts
|
|
.map((p) => (p.배 === null || line.수량 === null ? "—" : num(line.수량 * p.배)))
|
|
.join(" · ");
|
|
return el("td", {
|
|
className: "m02-labrow__qty",
|
|
attrs: { "data-label": tc("Labor_QtyCol") },
|
|
children: [
|
|
el("small", {
|
|
className: "m02-labrow__base",
|
|
text: line.수량 === null ? "—" : `${num(line.수량)} ${line.단위}`.trim(),
|
|
attrs: { title: "엑셀" },
|
|
}),
|
|
el("span", { className: "m02-labrow__applied", text: ours || "—", attrs: { title: "우리" } }),
|
|
],
|
|
});
|
|
}
|
|
|
|
const stateChip = (line: HopyoLine): HTMLElement => {
|
|
const m = line.맞춤;
|
|
const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : "";
|
|
const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n");
|
|
return el("span", {
|
|
className: `m02-hopyo__state is-${m.상태.replace(" ", "")}`,
|
|
text: `${m.상태}${alias}`,
|
|
attrs: { title: tags },
|
|
});
|
|
};
|
|
|
|
/** 대상마다 한 줄 — 키 · 이름 · × 배 칸 · 몫 */
|
|
function partLines(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement[] {
|
|
return parts.map((p, at) => {
|
|
const times = el("input", {
|
|
className: "ui-input ui-input--sm m02-hopyo__times",
|
|
attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) },
|
|
}) as HTMLInputElement;
|
|
times.title = "배 — 우리 수량 = 엑셀 수량 × 배";
|
|
times.disabled = !ctx.canWrite;
|
|
times.addEventListener("change", () =>
|
|
ctx.times(line, at, times.value === "" ? null : Number(times.value)),
|
|
);
|
|
const key = p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : "");
|
|
return el("div", {
|
|
className: "m02-hopyo__part",
|
|
attrs: { title: p.어디 ?? "" },
|
|
children: [
|
|
el("span", { className: "m02-labrow__key", text: key }),
|
|
el("strong", { text: p.이름 ?? "" }),
|
|
times,
|
|
...(p.몫 != null ? [small(`몫 ${p.몫}`)] : []),
|
|
],
|
|
});
|
|
});
|
|
}
|
|
|
|
/** 이름 칸 덧글 — 옵션(칸=값 · 출처) · 못 가름 · 셈 못 함 까닭 */
|
|
function nameNotes(line: HopyoLine, parts: HopyoPart[], price: LinePrice | null): HTMLElement[] {
|
|
const opts = parts
|
|
.filter((p) => p.로직)
|
|
.map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처))
|
|
.filter(Boolean);
|
|
if (line.맞춤.갈리는칸?.length) opts.push(`⚠ 못 가름: ${line.맞춤.갈리는칸.join(", ")}`);
|
|
const why = [
|
|
price?.fail,
|
|
...(price?.calc?.줄 ?? []).map((l) => l.까닭),
|
|
...(price?.calc?.자재 ?? []).map((m) => m.까닭),
|
|
].filter(Boolean) as string[];
|
|
return [
|
|
...opts.map((t) => small(t, "m02-labrow__note m02-hopyo__opts")),
|
|
...why.map((t) => small(`셈 못 함: ${t}`, "m02-labrow__empty")),
|
|
...(parts.some((p) => p.호표 !== undefined) ? [small("하위 호표 = 만든 뒤 셈")] : []),
|
|
];
|
|
}
|
|
|
|
function checkCell(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
|
const box = el("input", {
|
|
attrs: { type: "checkbox", "data-check": String(line.차례) },
|
|
}) as HTMLInputElement;
|
|
box.checked = isChecked(line, ctx.d) && checkable(line, ctx.d);
|
|
box.disabled = !ctx.canWrite || !checkable(line, ctx.d);
|
|
box.addEventListener("change", () => ctx.check(line, box.checked));
|
|
return el("td", { children: [box] });
|
|
}
|
|
|
|
function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement {
|
|
const m = line.맞춤;
|
|
const ghost = (label: string, run: () => void): HTMLElement =>
|
|
createButton({ label, variant: "ghost", size: "sm", onClick: run });
|
|
const out = el("select", {
|
|
className: "ui-input ui-input--sm",
|
|
attrs: { title: "로직없음 · 사용안함 — 사전조합에 안 넣고 장부에 남김", "data-out": "" },
|
|
children: OUT_TEXT.map(([value, text]) => el("option", { text, attrs: { value } })),
|
|
}) as HTMLSelectElement;
|
|
out.value = ctx.d.뺌[line.차례] ?? "";
|
|
out.disabled = !ctx.canWrite || m.상태 === "%";
|
|
out.addEventListener("change", () => ctx.out(line, out.value));
|
|
const options = parts.flatMap((p, at) => {
|
|
if (!p.로직) return [];
|
|
const b = ghost("옵션", () => ctx.options(line, at));
|
|
b.title = `${p.로직} 옵션`;
|
|
return [b];
|
|
});
|
|
return el("td", {
|
|
className: "m02-hopyo__acts",
|
|
children: [
|
|
...(ctx.canWrite && m.상태 !== "%" ? [ghost("고르기", () => ctx.choose(line))] : []),
|
|
ghost("원문 보기", () => ctx.source(line)),
|
|
...(line.근거 ? [ghost("엑셀 보기", () => ctx.sheet(line.근거!.행))] : []),
|
|
...options,
|
|
out,
|
|
],
|
|
});
|
|
}
|
|
|
|
const rowOf = (line: HopyoLine, ctx: RowsContext, cells: HTMLElement[]): HTMLElement => {
|
|
const on = isChecked(line, ctx.d) && checkable(line, ctx.d);
|
|
return el("tr", {
|
|
className: `m02-labrow__row m02-hopyo__line is-${on ? "in" : "out"}`,
|
|
attrs: { "data-line": String(line.차례), "data-state": line.맞춤.상태 },
|
|
children: [checkCell(line, ctx), ...cells, actsCell(line, partsOf(line, ctx.d), ctx)],
|
|
});
|
|
};
|
|
|
|
function logicRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
|
const parts = partsOf(line, ctx.d);
|
|
const price = ctx.price(line);
|
|
const wait = !!price && !price.calc && !price.fail;
|
|
const ours = price?.calc?.반영합;
|
|
const unit = price?.calc?.줄[0]?.분모 ?? parts[0]?.단위 ?? line.단위;
|
|
const compare = compareText(ctx.d.품[line.차례] ?? line.맞춤.품);
|
|
return rowOf(line, ctx, [
|
|
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
|
|
el("td", { className: "m02-labrow__kind", children: [line.갈래, stateChip(line)] }),
|
|
el("td", {
|
|
className: "m02-labrow__name m02-hopyo__parts",
|
|
children: [
|
|
...(parts.length ? partLines(line, parts, ctx) : [small("—", "m02-labrow__muted")]),
|
|
...nameNotes(line, parts, price),
|
|
],
|
|
}),
|
|
el("td", {
|
|
className: "m02-labrow__alias",
|
|
attrs: { title: `${line.이름} ${line.규격}`.trim() },
|
|
children: [line.이름, ...(line.규격 ? [small(line.규격)] : [])],
|
|
}),
|
|
qtyCell(line, parts),
|
|
el("td", { className: "m02-labrow__unit", text: unit ?? "" }),
|
|
...COSTS.map((c) =>
|
|
pairCell(line.금액[c], price?.calc ? (ours?.[c] ?? null) : null, wait, tc(COST_HEAD[c])),
|
|
),
|
|
el("td", { className: "m02-hopyo__compare", text: compare.split(" · ").join("\n") }),
|
|
]);
|
|
}
|
|
|
|
function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
|
const parts = partsOf(line, ctx.d);
|
|
const price = ctx.price(line);
|
|
const wait = !!price && !price.calc && !price.fail;
|
|
const mats = price?.calc?.자재 ?? [];
|
|
const first = mats[0];
|
|
const ours = mats.length ? mats.reduce((s, m) => s + (m.반영 ?? 0), 0) : null;
|
|
const show = (v: number | null | undefined): string =>
|
|
wait ? "…" : v === null || v === undefined ? "—" : v.toLocaleString("ko-KR");
|
|
const p = parts[0];
|
|
return rowOf(line, ctx, [
|
|
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
|
|
el("td", {
|
|
className: "m02-labrow__code",
|
|
text: parts.map((x) => x.자재 ?? "").join(" · ") || "—",
|
|
}),
|
|
el("td", {
|
|
className: "m02-labrow__name m02-hopyo__parts",
|
|
children: [
|
|
stateChip(line),
|
|
...(parts.length ? partLines(line, parts, ctx) : []),
|
|
small(`엑셀 ${line.이름}`.trim()),
|
|
...nameNotes(line, parts, price),
|
|
],
|
|
}),
|
|
el("td", {
|
|
className: "m02-labrow__spec",
|
|
text: p?.규격 ?? line.규격,
|
|
attrs: { title: p?.규격 ?? line.규격 },
|
|
}),
|
|
qtyCell(line, parts),
|
|
el("td", { className: "m02-labrow__unit", text: p?.단위 ?? line.단위 }),
|
|
...markCells({ 별도계상: line.갈래 === "별도계상" ? "별도계상" : "직접계산" }),
|
|
...PRICE_SLOTS.map((slot) =>
|
|
el("td", {
|
|
className: `m02-labrow__num m02-labrow__price${first?.기준칸 === slot ? " is-base-price" : ""}`,
|
|
text: show(first?.가격?.[slot]),
|
|
attrs: { "data-label": slot, "data-price-slot": slot },
|
|
}),
|
|
),
|
|
pairCell(line.금액.계, price?.calc ? ours : null, wait, tc("Labor_AppliedSum")),
|
|
]);
|
|
}
|
|
|
|
const grid = (heads: string[], rows: HTMLElement[], kind: string): HTMLElement =>
|
|
el("div", {
|
|
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
|
children: [
|
|
el("table", {
|
|
className: "ui-general-block__table m02-labrow m02-hopyo__table m02-hopyo__lines",
|
|
attrs: { "data-hopyo-table": kind },
|
|
children: [
|
|
el("thead", {
|
|
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
|
}),
|
|
el("tbody", { children: rows }),
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
|
|
/** 줄 표 둘 — 로직 표 · 자재 표(줄 없는 표는 안 그림) */
|
|
export function checkTables(lines: HopyoLine[], ctx: RowsContext): HTMLElement[] {
|
|
const logic = lines.filter((l) => tableOf(l, partsOf(l, ctx.d)) === "logic");
|
|
const mat = lines.filter((l) => !logic.includes(l));
|
|
const out: HTMLElement[] = [];
|
|
if (logic.length)
|
|
out.push(
|
|
grid(
|
|
[
|
|
"",
|
|
tc("Col_Order"),
|
|
tc("Sub"),
|
|
tc("Col_Name"),
|
|
tc("Col_Alias"),
|
|
tc("Labor_QtyCol"),
|
|
tc("Col_Unit"),
|
|
...COSTS.map((c) => tc(COST_HEAD[c])),
|
|
"품 비교",
|
|
"",
|
|
],
|
|
logic.map((l) => logicRow(l, ctx)),
|
|
"logic",
|
|
),
|
|
);
|
|
if (mat.length)
|
|
out.push(
|
|
el("h4", { className: "m02-hopyo__subhead", text: "자재" }),
|
|
grid(
|
|
[
|
|
"",
|
|
tc("Col_Order"),
|
|
tc("Labor_Code"),
|
|
tc("Col_Name"),
|
|
tc("Labor_Spec"),
|
|
tc("Labor_QtyCol"),
|
|
tc("Labor_Unit"),
|
|
...markHeads(),
|
|
...PRICE_SLOTS,
|
|
`엑셀 / 우리 ${tc("Labor_AppliedSum")}`,
|
|
"",
|
|
],
|
|
mat.map((l) => matRow(l, ctx)),
|
|
"mat",
|
|
),
|
|
);
|
|
return out;
|
|
}
|