- 빌더 scripts/build_로직_개정.py — 원문 절 머리 「('08, '20, 25년 보완)」 연도 목록 · 산림 [2026] 제정 · 연도 없는 머리는 윗절 · 글 꼴 그대로 출처 뒤에 끼움 · 채움 1,260 / 1,516(빈칸 256 = 머리에 연도 없음 246 · 못 찾음 10)
- 빌더 scripts/build_로직_잔토처리.py — 건설 공통 3-2-1 비고 ㎥당 보통인부 0.2인 · 호표 QC000172 「잔토처리 보통인부」(GC000083 옵션과 같은 표) · 키 GC001194
- _틀.md 7 장 열 차례 · 칸 설명 · _갱신절차.md 로직 빌더 줄
- M01 셈 지문 1,515 전후 같음 · fixture 다시 뜸(새 줄 하나 더함 1,516)
- M01 화면 읽기만 — 기본정보 「개정」 칸 · 시트 근거 줄 「개정 '08 · '20 · '25」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.8 KiB
TypeScript
164 lines
6.8 KiB
TypeScript
/* =============================================================================
|
||
* M01_MasterData/logic/M01_Logic_Sheet_Model.ts
|
||
* M01 로직 줄 식 시트 꼴(PLAN 794-1 · 요구 650~652 · 요구서_652 1 장 6) — M02 단가산출 시트와 같은 꼴
|
||
* 「이름 | 식 | 값 | 비목」 · 줄 갈래 = 근거(출처 · 개정 · 비고 · 끝수) · 옵션(입력) · 계산(중간) · 자원(호표) ·
|
||
* 율(덧줄) · 단계 · 소계 · 합계 · 식 글 = 서버 이름 꼴(`/logic/sheet` · 없으면 엔진 식 「*」 → 「×」)
|
||
* 값 = 시험 계산 답(줄 금액 = 호표 → 덧줄 차례 · 중간 값 · 비목 소계)
|
||
* 고침 = 줄 사본 식 칸 하나(`setLabFormula`) — 저장은 화면이 기존 M01 [저장] 길로
|
||
* 짝 = 서버 `M01_MasterData_Sheet.py`(자리 = 갈래 · 차례 꼴만 같음) · 화면 · fetch · sessionStorage 없음
|
||
* ========================================================================== */
|
||
|
||
import type {
|
||
CalcAnswer,
|
||
CalcLine,
|
||
LogicInput,
|
||
LogicRow,
|
||
NamedFormula,
|
||
} from "../M01_MasterData_UI_Logic_Api";
|
||
|
||
export type LabPart = "중간" | "호표" | "덧줄";
|
||
export type LabKind = "근거" | "옵션" | "계산" | "자원" | "율" | "단계" | "소계" | "합계";
|
||
|
||
/** 줄마다 식 칸 이름(갈래 → 칸) — 서버 `FIELD` 와 같음 */
|
||
export const FIELD: Record<LabPart, "식" | "수량"> = { 중간: "식", 호표: "수량", 덧줄: "식" };
|
||
|
||
export interface LabNames {
|
||
중간: string[];
|
||
호표: string[];
|
||
덧줄: string[];
|
||
이름표?: { 글: string; 갈래: string }[];
|
||
}
|
||
|
||
export interface LabItem {
|
||
kind: LabKind;
|
||
이름: string;
|
||
/** 이름 꼴 식 글 · 빈 글 = 식 없음 */
|
||
식: string;
|
||
값: number | string | null;
|
||
/** 비목 줄임(노 · 재 · 경) */
|
||
비목: string;
|
||
/** 식 칸 자리(중간 · 호표 · 덧줄) */
|
||
part?: LabPart;
|
||
at?: number;
|
||
input?: LogicInput;
|
||
/** 금액번지(791-1) */
|
||
번지?: string;
|
||
/** 저장본과 식이 다름 */
|
||
changed: boolean;
|
||
}
|
||
|
||
const SHORT: Record<string, string> = { 노무비: "노", 재료비: "재", 경비: "경" };
|
||
const short = (v: unknown): string => (typeof v === "string" ? (SHORT[v] ?? v) : "");
|
||
|
||
/** 엔진 식 → 보기 글(이름 꼴이 아직 없을 때) */
|
||
export const plain = (s: string): string => s.replace(/\s*\*\s*/g, " × ");
|
||
|
||
export const spotKey = (part: LabPart, at: number): string => `${part}#${at}`;
|
||
|
||
/** 줄 식 칸 값(엔진 식) */
|
||
export function formulaAt(row: LogicRow, part: LabPart, at: number): string {
|
||
const list = (row[part] ?? []) as unknown as Record<string, unknown>[];
|
||
return String(list[at]?.[FIELD[part]] ?? "");
|
||
}
|
||
|
||
/** 줄 식 칸 하나를 엔진 식으로 바꿈(줄 객체 그대로 고침) · 없는 자리 = false */
|
||
export function setLabFormula(row: LogicRow, part: LabPart, at: number, engine: string): boolean {
|
||
const list = row[part] as unknown as Record<string, unknown>[] | undefined;
|
||
if (!list || at < 0 || at >= list.length) return false;
|
||
list[at][FIELD[part]] = engine;
|
||
return true;
|
||
}
|
||
|
||
/** 끝수 글 — 글자면 그대로 · 묶음이면 「대상 방법 자리」 */
|
||
export function cutText(rule: unknown): string {
|
||
if (rule == null || rule === "") return "";
|
||
if (typeof rule === "string") return rule;
|
||
const r = rule as Record<string, unknown>;
|
||
return [r.대상, r.방법, r.자리 != null ? `${String(r.자리)} 자리` : ""]
|
||
.filter((x) => x !== undefined && x !== "")
|
||
.join(" · ");
|
||
}
|
||
|
||
const valueOf = (v: unknown): number | string | null =>
|
||
typeof v === "number" || typeof v === "string" ? v : null;
|
||
|
||
/** 원문 절 머리 개정 연도 글 — [2008, 2020, 2025] → 「'08 · '20 · '25」(794-2b · 없으면 빈 글) */
|
||
export const revisionText = (row: LogicRow): string =>
|
||
(row.개정 ?? []).map((y) => `'${String(y).slice(-2)}`).join(" · ");
|
||
|
||
/** 시트 줄 — 근거 · 옵션 · 계산 · 자원 · 율 · 단계 · 소계 · 합계 */
|
||
export function labItems(o: {
|
||
row: LogicRow;
|
||
/** 저장본 줄(새 로직 = null) — 식이 다르면 고친 줄 */
|
||
original: LogicRow | null;
|
||
values: Record<string, string>;
|
||
names: LabNames | null;
|
||
answer: CalcAnswer | null;
|
||
}): LabItem[] {
|
||
const { row, original } = o;
|
||
const out: LabItem[] = [];
|
||
const item = (x: Partial<LabItem> & { kind: LabKind; 이름: string }): void =>
|
||
void out.push({ 식: "", 값: null, 비목: "", changed: false, ...x });
|
||
if (row.출처) item({ kind: "근거", 이름: row.원문번호 || "", 식: row.출처 });
|
||
if (row.개정?.length) item({ kind: "근거", 이름: "개정", 식: revisionText(row) });
|
||
for (const line of (row.비고 ?? "").split(/\r?\n/).filter((x) => x.trim()))
|
||
item({ kind: "근거", 이름: "", 식: line.trim() });
|
||
for (const input of row.입력 ?? [])
|
||
item({ kind: "옵션", 이름: input.이름, 값: o.values[input.이름] ?? "", input });
|
||
const ok = o.answer?.ok ? o.answer : null;
|
||
const mid = ok?.middle ?? {};
|
||
const shown = (part: LabPart, at: number): string =>
|
||
o.names?.[part]?.[at] ?? plain(formulaAt(row, part, at));
|
||
const changed = (part: LabPart, at: number): boolean =>
|
||
!!original && formulaAt(original, part, at) !== formulaAt(row, part, at);
|
||
const spot = (part: LabPart, at: number) => ({
|
||
part,
|
||
at,
|
||
식: shown(part, at),
|
||
changed: changed(part, at),
|
||
});
|
||
(row.중간 ?? []).forEach((f: NamedFormula, at) =>
|
||
item({ kind: "계산", 이름: f.이름, 값: valueOf(mid[f.이름]), ...spot("중간", at) }),
|
||
);
|
||
const ho = row.호표 ?? [];
|
||
const extra = row.덧줄 ?? [];
|
||
const lines: (CalcLine | undefined)[] =
|
||
ok?.lines && ok.lines.length === ho.length + extra.length ? ok.lines : [];
|
||
ho.forEach((h, at) => {
|
||
const b = (h as { 번지?: string }).번지;
|
||
item({
|
||
kind: "자원",
|
||
이름: h.단위 ? `${h.이름 ?? ""} (${h.단위})` : (h.이름 ?? ""),
|
||
값: lines[at]?.금액 ?? null,
|
||
비목: short(h.비목),
|
||
...(b ? { 번지: b } : {}),
|
||
...spot("호표", at),
|
||
});
|
||
});
|
||
extra.forEach((x, at) => {
|
||
const b = (x as { 번지?: string }).번지;
|
||
item({
|
||
kind: "율",
|
||
이름: x.이름,
|
||
값: lines[ho.length + at]?.금액 ?? null,
|
||
비목: short(x.비목),
|
||
...(b ? { 번지: b } : {}),
|
||
...spot("덧줄", at),
|
||
});
|
||
});
|
||
const steps = (row.단계 ?? []) as { 이름?: string; 줄?: unknown }[];
|
||
for (const s of Array.isArray(steps) ? steps : [])
|
||
item({
|
||
kind: "단계",
|
||
이름: String(s.이름 ?? ""),
|
||
식: Array.isArray(s.줄) ? s.줄.map(String).join(" + ") : "",
|
||
});
|
||
const cut = cutText(row.끝수);
|
||
if (cut) item({ kind: "근거", 이름: "끝수", 식: cut });
|
||
const sums = ok?.sums ?? null;
|
||
for (const k of Object.keys(SHORT))
|
||
if (sums && k in sums) item({ kind: "소계", 이름: k, 값: sums[k], 비목: SHORT[k] });
|
||
if (sums && "계" in sums) item({ kind: "합계", 이름: "계", 값: sums.계 });
|
||
return out;
|
||
}
|