Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts
T
eomsangdonandClaude Sonnet 5.5 52c8692fdd feat(738-2 · 740-2): 단가산출 · 일위대가 상세 머리 [초기화] [삭제] · 넣은 줄 출처 꼬리표
- 머리 단추 차례 … [새로고침] [초기화] [삭제] [저장] — 새 Main_Actions(쓰기 권한 있는 층만 보임)
- [삭제] = 735 경고 창(쓰는 곳 표 · 키) → 나무 지우기 → 목록으로 · 쓰는 곳 없음 = 한 줄 물음
- [초기화] = 서버 -reset 미리 보기(걷힐 고침 수 · 뺀 줄 되살림 수) → 물음 → 그 층에 씀 → 다시 그림 ·
  저장 안 한 고침은 먼저 물음 · 옵션 · 기본정보 그대로
- 출처 꼬리표(본뜬) — 순수 logic/M02_Born_Tag · 단가산출 = 이름 옆 글 꼬리표(공용 Ho 꼬리표 칸을
  이름 줄 안으로 · 첫 줄 보임 · 둘째 줄 풍선) · 일위대가표 격자 = 비고 칸 글 + 풍선 · 줄 표 = 이름 끝
- 시험: 새 test_740_2 · 낡은 시험 4 고침(test_727 · 717_2b · 704_3 · 704_4 — 모델 · 격자 파일을
  이어 붙여 도는 시험이라 새 꼬리표 글 파일을 목록에 더함)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 20:36:15 +09:00

153 lines
6.4 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Labor_Grid.ts
* 일위대가 엑셀 꼴 격자(704-3) — 문서 · 서버 calc 답 → 공용 격자(`ui_template_hopyo_grid` · B09 일위대가표와 같은 꼴).
* 일위대가 카드 · 상세(`Labor_Detail`)가 위에 붙임. 값은 calc 답 그대로(여기서 셈하지 않음):
* 머리 = 일위대가번 · 단위 · 1 단위 합계(`반영합`)
* 로직 줄 = 이름(별칭 앞섬) · 규격(옵션 값 · 켜쌓기) · 수량 · 분모 · 단가 = 기준 ÷ 배 · 금액 = 반영 · 꼬리표 품셈
* 자재 줄 = 이름 · 규격 · 수량 · 단위 · 재료비 단가 = 기준단가 · 금액 = 반영 · 별도계상은 비고 「합계 밖」
* 빈 줄(금액 모름 · 수량 없음) = 합계 줄 「빈 줄 n」 붉게 — 701-4 경고와 같은 줄
* 규격 열(704-4 · `Labor_Spec`) = 수량 칸 열마다 · 고칠 때 격자 위 규격 줄 + 수량 칸 고침
* 안 이은 고정 줄(729-3 · `Labor_Unlinked`) = 격자 위 띠 「M01 연결 없음 n 줄」 · 고칠 때 [연결]
* ========================================================================== */
import {
createHopyoGrid,
HOPYO_TAG,
type HopyoBlock,
type HopyoLine,
type HopyoMoney,
} from "@ui/ui_template_hopyo_grid";
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
import type { LaborCalc, LaborCost, LaborDoc } from "./M02_MasterTemplete_Labor_Api";
import { bornNote } from "./logic/M02_Born_Tag";
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import { valueLine } from "./M02_Labor_Values_Model";
import { specBar, specCells, withSpecs, type SpecEdit } from "./M02_MasterTemplete_Labor_Spec";
import { pasteInto } from "./M02_MasterTemplete_Labor_Paste";
import { unlinkedBar } from "./M02_MasterTemplete_Labor_Unlinked";
const money = (m: LaborCost | null | undefined, times = 1): HopyoMoney =>
m
? {
sum: m.계 / times,
labor: m.노무비 / times,
material: m.재료비 / times,
expense: m.경비 / times,
}
: {};
/** 층 낱말 — 번호(키)는 화면 글에 안 냄 */
const layerWord = (layer: string): string =>
layer === "personal" ? tc("Grid_Mine") : tc(layer === "company" ? "Lib_Company" : "Lib_System");
/** 일위대가번 — 번호꼴(SU-000011 · SU01010101)은 사람 글이 아니라 뺌 */
const humanNo = (no?: string): string => (no && !/^S[A-Z]\d?-?\d{6,8}$/.test(no) ? no : "");
/** 문서 + calc 답 → 호표 하나 — calc 없으면(계산 중) 금액 빈 칸 · 빈 줄 0 */
export function laborBlock(
doc: LaborDoc,
calc: LaborCalc | null,
logics: ReadonlyMap<string, ItemLogic>,
): HopyoBlock {
const logicLines = doc.담은로직.map((row): HopyoLine => {
const id = lineId(row) ?? "";
const line = calc?.줄.find((l) => lineId(l) === id);
const logic = logics.get(row.로직);
const layer =
row.켜쌓기율 != null ? tc("Labor_Layer").replace("{n}", String(row.켜쌓기율)) : "";
const qty = line?.수량 ?? null;
const fx = row.고정; // 705-1 고정 줄 — 이름 · 규격 · 단위 = 줄 것 · 비고 = 이음 · 손 단가
if (fx)
return {
name: fx.이름,
spec: fx.규격 ?? "",
qty,
unit: fx.비율 ? "%" : (fx.단위 ?? ""),
prices: money(line?.기준, line?.배 || 1),
amounts: money(line?.반영),
note:
line?.까닭 ??
(Object.values(fx.이음 ?? {}).some(Boolean) ? tc("Grid_Linked") : tc("Grid_Hand")),
tag: HOPYO_TAG.fix,
};
return {
name: row.별칭 || ((row.참조 ? line?.이름 : logic?.이름) ?? row.참조?.키 ?? row.로직),
spec: [valueLine(logic?.입력 ?? [], row.입력값).text, layer].filter(Boolean).join(" · "),
qty,
unit: line?.분모 ?? (row.참조 ? "" : (logic?.결과단위 ?? "")),
prices: money(line?.기준, line?.배 || 1),
amounts: money(line?.반영),
note:
line?.까닭 ??
(bornNote(row.본뜬) || // 740-2 넣은 줄 = 출처 글
(calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직)),
tag: HOPYO_TAG.pum,
};
});
const matLines = doc.자재.map((mat): HopyoLine => {
const line = calc?.자재.find((m) => m.코드 === mat.코드);
const qty = line?.수량 ?? null;
const price = line?.기준단가 ?? null;
const cost = line?.반영 ?? null;
return {
name: mat.이름 ?? mat.코드,
spec: mat.규격 ?? "",
qty,
unit: mat.단위 ?? "",
prices: { sum: price, material: price },
amounts: { sum: cost, material: cost },
note:
line?.까닭 ??
(calc && qty === null
? tc("Grid_NoQty")
: mat.별도계상 !== "직접계산"
? tc("Grid_Apart") // 짝 `settle` 의 aside 와 같은 규칙
: bornNote(mat.본뜬) || mat.코드),
};
});
const lines = [...logicLines, ...matLines];
const block: HopyoBlock = {
key: doc.열,
name: humanNo(doc.일위대가번) || (doc as { 이름?: string }).이름 || tc("Grid_Head"),
unit: doc.단위 ?? "",
total: money(calc?.반영합),
lines,
...(calc ? {} : { blank: 0 }),
};
return withSpecs(block, doc, calc);
}
/** 격자 하나 — `edit` 있으면 Ctrl+V 붙여넣기(705-2 · `Labor_Paste`) · `spec` 이면 위에 규격 줄 · 규격 수량 칸 고침(704-4) */
export const laborGrid = (
doc: LaborDoc,
calc: LaborCalc | null,
logics: ReadonlyMap<string, ItemLogic>,
edit?: SpecEdit | false,
spec = true,
): HTMLElement => {
const grid = gridOf(doc, calc, logics, edit, spec);
const bar = unlinkedBar(doc, edit);
if (!bar) return grid;
const box = document.createElement("div");
box.append(bar, grid);
return box;
};
const gridOf = (
doc: LaborDoc,
calc: LaborCalc | null,
logics: ReadonlyMap<string, ItemLogic>,
edit?: SpecEdit | false,
spec = true,
): HTMLElement => {
const block = laborBlock(doc, calc, logics);
if (!edit) return createHopyoGrid([block]).root;
const paste = (text: string): void => void pasteInto(doc, text, () => edit(false));
if (!spec) return createHopyoGrid([block], {}, paste).root;
const grid = createHopyoGrid([block], specCells(block, doc, edit), paste).root;
const box = document.createElement("div");
box.append(specBar(doc, calc, edit), grid);
return box;
};