Files
Aislo/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts
eomsangdonandClaude Opus 5.5 9b92d67189 feat(793-3b): 근거 제안 세 단을 793-3a 길로 이음 · 이 줄에 잇기 고른 것 = 옮길 층 SK · 환산 계수 넣을 때 서버가 다시 바꿈
- 가져온 식 만들 때 SK 가져온.근거후보 = 793-3a 블록 제안 근거후보(San_Basis_Store.block · 못 셈 = 빈 칸)
- 머리 판 후보 = 원문번호 · 옵션 · 품검산 · 입력필요 n · 까닭 [표지 글] · [우리 기초로 바꾸기] 입력 필요 = 바인딩 + 후보 입력필요 + 환산
- 환산 계수 = 가져온 식에서 to-base 다시(바꿈 전 · 후 서버 정본) · 계수 없을 땐 시트 안 엶(셈 길 400 없음)
- 단산 줄 고른 것 층 · 주인 뺌 — 층을 주면 옮기기가 SK 를 못 찾아 고정 줄로 옮겨짐
- 시험 test_793_3b 9(근거후보 채움 · SK 참조 옮기기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:41:43 +09:00

146 lines
5.6 KiB
TypeScript

/* =============================================================================
* logic/M02_Hopyo_Imported_Model.ts
* 불러오기 단산 줄 = 가져온 식 단가산출(PLAN 793-3b · 요구 646 · 647) — 꼴 · 읽기 · 바꾸기만.
* · SK `가져온` 덧칸 꼴(793-2 `M02_Sangeun_Imported.py` 머리) · 근거 제안 세 단(확정 · 근접 · 사람 고름 · 793-3a 가 채움)
* · 단산 줄 고른 것 = {갈래 단가산출, 키 SK-…, 필요(입력 필요), 표지(결과 칩)} — 층 = 옮길 층 — 필요가 비어야 확인 · 옮김
* · 줄 결과 칩 = 가져온 식 있음 · 근거 확정 · 근접 · 사람 고름 · 입력 필요 n(「2차 필요」 대신)
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
* ========================================================================== */
import type { DraftPick } from "./M02_MasterTemplete_Hopyo_Picks";
export type Money4 = Record<"노무비" | "재료비" | "경비" | "계", number>;
export type Tier = "확정" | "근접" | "사람";
export const TIERS: Tier[] = ["확정", "근접", "사람"];
/** 근거 후보 하나(793-3a `제안.근거후보`) — 까닭 = [[표지(판 차이 · 현장 값 · 감률 · 할증 배수 · 단위 기준 차이 · 그 밖), 글]] */
export interface BasisCand {
기초: string;
이름?: string;
원문번호?: string;
옵션?: Record<string, unknown>;
/** 글에 없는 칸(감률 · 할증 포함) — 사용자 입력 */
입력필요?: string[];
품검산?: "같음" | "다름" | "짝 없음" | null;
단: Tier;
까닭?: [string, string][];
}
/** 근거 후보 까닭 글(「표지 글」 · 줄마다) */
export const reasonText = (c: BasisCand): string =>
(c.까닭 ?? []).map(([tag, text]) => `${tag} ${text}`.trim()).join(" · ");
export interface CheckLine {
자리: string;
행?: number | null;
셈?: number | null;
엑셀?: number | null;
같음: boolean;
까닭?: string;
}
/** SK 문서 덧칸 `가져온` */
export interface ImportedMeta {
로직?: { 이름?: string; 호표?: { 이름?: string; 이음?: string | null }[] };
단가기준?: "엑셀" | "우리";
원본?: {
파일?: string;
시트?: string;
행?: number[];
갈래?: string;
엑셀금액?: Money4;
글줄?: { 행?: number | null; 글?: string; 자리?: string; 값?: number | null }[];
};
검산?: {
같음: boolean;
까닭?: string | null;
셈?: Money4 | null;
엑셀?: Money4;
줄: CheckLine[];
};
근거후보?: BasisCand[];
바꿈?: { 전?: Money4 | null; 후?: Money4 | null; 차이?: Money4 | null };
}
export interface ImportedDoc {
이름?: string;
단위?: string;
바탕?: { 로직?: string } | null;
입력값?: Record<string, unknown>;
/** SK 단위 환산(기초 결과단위 성격이 다를 때 · 728-2) */
환산?: { 계수?: number | string };
가져온?: ImportedMeta;
[k: string]: unknown;
}
/** 가져온 식(아직 우리 기초로 안 바꿈) */
export const isImported = (d: ImportedDoc | null | undefined): boolean => !!d?.가져온?.로직;
/** 근거 후보 세 단 */
export function tiers(list: BasisCand[] | undefined): Record<Tier, BasisCand[]> {
const out: Record<Tier, BasisCand[]> = { 확정: [], 근접: [], 사람: [] };
for (const c of list ?? []) if (TIERS.includes(c.단)) out[c.단].push(c);
return out;
}
/** 검산 다른 줄 */
export const wrongLines = (d: ImportedDoc): CheckLine[] =>
(d.가져온?.검산?.줄 ?? []).filter((x) => !x.같음);
const blank = (v: unknown): boolean => v === undefined || v === null || String(v).trim() === "";
/** 입력 필요 칸 값 — 입력값 + 「환산」 = 환산 계수 */
export const needValues = (d: ImportedDoc): Record<string, unknown> => ({
...(d.입력값 ?? {}),
환산: d.환산?.계수,
});
/** 입력 필요 중 아직 빈 칸 */
export const stillNeed = (need: string[], values: Record<string, unknown> | undefined): string[] =>
need.filter((k) => blank(values?.[k]));
/** 단산 줄 고른 것 — 그 SK(필요 = 빈 입력 필요 칸 · `missing` 이 막음) · 층 · 주인 안 둠 = 옮길 층(SK 를 만든 층)의 SK */
export function skPick(
sk: string,
doc: ImportedDoc,
need: string[],
marks: string[],
): DraftPick & { 표지: string[] } {
const values = needValues(doc);
return {
갈래: "단가산출",
키: sk,
이름: String(doc.이름 ?? sk),
단위: String(doc.단위 ?? ""),
입력값: Object.fromEntries(need.map((k) => [k, values[k] ?? ""])),
출처: {},
몫: null,
필요: need,
표지: marks,
};
}
/** 결과 표지 — 가져온 식 있음 | 우리 기초(+ 근거 단) */
export function marksOf(doc: ImportedDoc, tier: Tier | null): string[] {
if (isImported(doc)) return ["가져온 식 있음"];
return tier ? [`근거 ${tier}`] : ["사람 고름"];
}
/** 단산 줄 결과 칩 글 — SK 고른 것이 있으면 그 표지 + 입력 필요 n · 없으면 null(옛 판정 칩) */
export function rowChips(picks: (DraftPick & { 표지?: string[] })[]): string[] | null {
const sk = picks.find((p) => p.갈래 === "단가산출" && /^SK-/.test(String(p.키 ?? "")));
if (!sk) return null;
const need = stillNeed(sk.필요 ?? [], sk.입력값);
return [...(sk.표지 ?? ["가져온 식 있음"]), ...(need.length ? [`입력 필요 ${need.length}`] : [])];
}
/** 금액 차이 글(후 − 전) */
export function gapText(
before: Money4 | null | undefined,
after: Money4 | null | undefined,
): string {
if (!before || !after) return "—";
const d = after.계 - before.계;
return `${d > 0 ? "+" : ""}${d.toLocaleString("ko-KR")}`;
}