- 줄 단추 = 로직 줄 · SK 참조 줄(Labor_Rows `sangeun`) · 프로젝트 복사본(셈 주입)엔 없음 - 모달(Labor_SangeunModal) = 일위대가 머리 「1 ㎡ 당」 · 이 줄 수량 · 1 단위 금액 · 보기 고르기(기본 인력·재료·경비 = 공용 계산서 calc_sheet · 상세 = 품 · 단가 칸 고침) - 고친 줄 = 표 부품 「바뀜」 + 원래 값 곁 + 「이 일위대가에서 고침」 꼬리표 · [적용] = 줄 칸 패치(화면 캐시 · 저장은 [저장]) · 적용 안 하고 닫으면 물음 - [단가산출로 저장] = 내 개인 층 새 SK(from-line) · 자기 개인 층 일위대가면 그 줄을 새 SK 참조 줄로 - 셈 = 서버 /sangeun/calc(저장 안 함 · 최신 응답만) · 화면 문서 = 서버 as_doc 꼴(Sangeun_Cover_Model · 고름 · 자재표시 cid) - 고침 표 · 셈 길 부름 = Sangeun_Cover(717-2b 도 씀) · normalizeLabor 가 줄 패치를 지킴(떨구면 [저장]에서 사라짐) - 시험 test_717_3 5 통과 · 고친 대상 166 통과(1 실패 = test_175 B09 Store_Combo._PER · 전부터 · 719-2 몫) · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
6.6 KiB
TypeScript
167 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* M02_Sangeun_Cover_Model.ts
|
|
* 단가산출 고친 칸(덮개) 순수 함수(PLAN 717-3 · 요구서 내역12 4장 4 · 16 · 24) — 화면 · fetch 없음.
|
|
* · `lineDoc` = 일위대가 줄 → 이름 없는 단가산출(SK) 문서(서버 `M02_Labor_Patch.as_doc` 꼴 · 셈 길 몸) ·
|
|
* 줄 `고름` · `자재표시`(호표 차례 열쇠)는 바탕 줄 정체(`ids` · 셈 답 `정체`)로 cid 열쇠로 바꿈
|
|
* · `setCover` · `cleanPatch` = 줄 칸 `패치 {덮개, 뺀줄, 더한줄}` 고침(숫자만 · `원값`은 서버가 저장 때 셈)
|
|
* · `sheetGroups` = 셈 답 줄 → 공용 계산서(`A00_Common/calc_sheet`) 묶음(값 = 서버 셈 그대로 · 다시 안 셈)
|
|
* 계산 자리 = 서버(`M02_Sangeun_Calc`) 단독 — 여기는 문서 모양만.
|
|
* ========================================================================== */
|
|
|
|
export const PARTS = ["노무비", "재료비", "경비"] as const;
|
|
export type Part = (typeof PARTS)[number];
|
|
/** 덮개 칸(서버 `M02_Sangeun_Doc.COVER_KEYS`) — qty = 품 · labor · material · expense = 줄 단가 */
|
|
export const PART_KEY = { 노무비: "labor", 재료비: "material", 경비: "expense" } as const;
|
|
export type CoverKey = "qty" | "배율" | (typeof PART_KEY)[Part];
|
|
export type Cover = Partial<Record<CoverKey, number>>;
|
|
export interface LinePatch {
|
|
덮개?: Record<string, Cover>;
|
|
뺀줄?: string[];
|
|
더한줄?: unknown[];
|
|
}
|
|
export type Money = Record<Part | "계", number>;
|
|
|
|
/** 셈 답 한 줄(M01 시험 계산 줄 + cid · 고친 칸 이름) */
|
|
export interface SkLine {
|
|
cid: string;
|
|
이름: string;
|
|
단위?: string;
|
|
수량: number;
|
|
단가?: number;
|
|
금액?: number;
|
|
비목?: Partial<Record<Part, number>>;
|
|
율?: number | null;
|
|
출처?: string;
|
|
덮개?: string[];
|
|
}
|
|
export interface SkCalc {
|
|
기준: Money | null;
|
|
줄: SkLine[];
|
|
까닭?: string | null;
|
|
"1단위"?: Money | null;
|
|
배?: number;
|
|
정체?: string[];
|
|
짝잃음?: { cid: string; 까닭: string }[];
|
|
머리?: string | null;
|
|
}
|
|
|
|
/** 줄 꼴(일위대가 `담은로직[]` 한 줄 · 쓰는 칸만) */
|
|
export interface PatchRow {
|
|
로직?: string;
|
|
참조?: { 층: string; 주인?: string | null; 키: string };
|
|
입력값?: Record<string, unknown>;
|
|
켜쌓기율?: number | null;
|
|
고름?: Record<string, unknown>;
|
|
자재표시?: Record<string, unknown>;
|
|
패치?: LinePatch;
|
|
}
|
|
|
|
const keyed = (value: Record<string, unknown> | undefined, ids: string[]) =>
|
|
Object.fromEntries(
|
|
Object.entries(value ?? {}).flatMap(([n, v]) =>
|
|
/^\d+$/.test(n) && Number(n) < ids.length ? [[ids[Number(n)], v]] : [],
|
|
),
|
|
);
|
|
|
|
/** 고름 · 자재표시가 있는 로직 줄 = 바탕 줄 정체가 먼저 있어야 함 */
|
|
export const needsIds = (row: PatchRow): boolean =>
|
|
!row.참조 &&
|
|
(Object.keys(row.고름 ?? {}).length > 0 || Object.keys(row.자재표시 ?? {}).length > 0);
|
|
|
|
/** 줄 → 이름 없는 SK 문서 — `patch` null = 그 일위대가 고침 없는 원래 값 */
|
|
export function lineDoc(
|
|
row: PatchRow,
|
|
ids: string[] | null,
|
|
patch: LinePatch | null,
|
|
): Record<string, unknown> {
|
|
const head = {
|
|
키: "SK-000000",
|
|
이름: "줄",
|
|
단위: "식",
|
|
덮개: patch?.덮개 ?? {},
|
|
뺀줄: patch?.뺀줄 ?? [],
|
|
더한줄: patch?.더한줄 ?? [],
|
|
};
|
|
if (row.참조) return { ...head, 바탕: { 참조: row.참조 } };
|
|
return {
|
|
...head,
|
|
...(row.켜쌓기율 != null ? { 켜쌓기율: row.켜쌓기율 } : {}),
|
|
바탕: { 로직: row.로직 ?? "" },
|
|
입력값: { ...(row.입력값 ?? {}) },
|
|
고름: ids ? keyed(row.고름, ids) : {},
|
|
자재표시: ids ? keyed(row.자재표시, ids) : {},
|
|
};
|
|
}
|
|
|
|
/** 단가를 고칠 수 있는 비목 — 금액이 한 비목에만 있는 줄만(덧줄 % 줄 = 율만 · 없음 = null) */
|
|
export function priceKey(line: SkLine): CoverKey | null {
|
|
if (line.율 != null || line.cid.startsWith("pct:")) return null;
|
|
const parts = PARTS.filter((p) => (line.비목?.[p] ?? 0) !== 0);
|
|
return parts.length === 1 ? PART_KEY[parts[0]] : null;
|
|
}
|
|
|
|
/** 품 칸 값 — 덧줄 = 율 · 그 밖 = 수량 */
|
|
export const qtyOf = (line: SkLine): number => line.율 ?? line.수량;
|
|
|
|
/** 빈 칸 · 원값 뺀 패치 — 다 비면 null(줄 칸 안 둠) */
|
|
export function cleanPatch(patch: LinePatch | null | undefined): LinePatch | null {
|
|
const covers = Object.fromEntries(
|
|
Object.entries(patch?.덮개 ?? {}).flatMap(([cid, c]) => {
|
|
const keep = Object.fromEntries(
|
|
Object.entries(c ?? {}).filter(([k, v]) => k !== "원값" && typeof v === "number"),
|
|
);
|
|
return Object.keys(keep).length ? [[cid, keep]] : [];
|
|
}),
|
|
);
|
|
const out: LinePatch = {
|
|
...(Object.keys(covers).length ? { 덮개: covers } : {}),
|
|
...(patch?.뺀줄?.length ? { 뺀줄: [...patch.뺀줄] } : {}),
|
|
...(patch?.더한줄?.length ? { 더한줄: [...patch.더한줄] } : {}),
|
|
};
|
|
return Object.keys(out).length ? out : null;
|
|
}
|
|
|
|
/** 칸 하나 고침 — `value` null = 그 칸 덮개 풂(원래 값) · 같은 값도 덮개로 둠(사용자가 적은 값) */
|
|
export function setCover(
|
|
patch: LinePatch | null,
|
|
cid: string,
|
|
key: CoverKey,
|
|
value: number | null,
|
|
): LinePatch | null {
|
|
const covers = { ...(patch?.덮개 ?? {}) };
|
|
const one: Cover = { ...(covers[cid] ?? {}) };
|
|
if (value === null) delete one[key];
|
|
else one[key] = value;
|
|
if (key === "qty") delete one.배율; // 품은 qty · 배율 중 하나(서버 400)
|
|
covers[cid] = one;
|
|
return cleanPatch({ ...patch, 덮개: covers });
|
|
}
|
|
|
|
const fmt = (v: number | null | undefined): string =>
|
|
v == null ? "-" : Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
|
|
|
/** 계산서 묶음 — 비목마다 그 비목 금액이 있는 줄 · 식 = 「단가 * 품 = 금액」(한 비목 줄) · 소계 = 서버 기준 */
|
|
export function sheetGroups(calc: SkCalc): {
|
|
비목: string;
|
|
소계: number | null;
|
|
줄: { 이름: string; 글: string; 금액: number | null }[];
|
|
}[] {
|
|
return PARTS.flatMap((p) => {
|
|
const lines = calc.줄.filter((x) => (x.비목?.[p] ?? 0) !== 0);
|
|
if (!lines.length) return [];
|
|
return [
|
|
{
|
|
비목: p,
|
|
소계: calc.기준?.[p] ?? null,
|
|
줄: lines.map((x) => {
|
|
const amount = x.비목?.[p] ?? null;
|
|
const single = priceKey(x) !== null && x.단가 != null;
|
|
const pct = x.율 != null ? " %" : ""; // 덧줄 = 율 %
|
|
const text = single ? `${fmt(x.단가)} * ${fmt(qtyOf(x))}` : `${fmt(qtyOf(x))}${pct}`;
|
|
return { 이름: x.이름, 글: `${text} = ${fmt(amount)}`, 금액: amount };
|
|
}),
|
|
},
|
|
];
|
|
});
|
|
}
|