feat(799-2): M01 로직 상세 · M02 단가산출 상세 옛 탭 넷(시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식) 걷음 — 보기는 수식 · 단가산출 상세 · 셈 구동 따로

- 셈 구동 떼기: 새 M01_MasterData_UI_Logic_Drive.ts(driveRun · previewText · autoFill · createDrive) — M01 상세는 숨은 시험 계산 칸 단추 대신 구동 함수 · 시험 계산 칸(Logic_Calc)은 로직 복사 화면이 쓰므로 남기고 같은 구동을 부름 · 재료 고름은 rerun 고리
- M02: 새 _Sangeun_Tabs.ts(수식 · 단가산출 상세 보기 고르기) · Detail 의 시험 계산 · 인력/재료/경비 걷음
- 걷은 파일(git rm): M01 LogicLab_Sheet · Logic_Sheet_Model · LogicLab_Formula(.ts .css) · FormulaTokens · LogicLab_Est / M02 Sangeun_Sheet(.ts .css) · SheetEdit · SheetFormula · logic Sheet_Model · Const_Model · Formula_Model · 부품 시트 줄(createCalcSheetRow) · Cover_Model.sheetGroups · Calc 의 M02 고리(answer · shown · held) · 죽은 CSS · 글
- 서버 길 · 문서 칸(변수줄 · 글줄 · 덮개)은 남김 — 금액 그대로
- 낡은 시험: test_790_1(시트 화면 전부) · test_714_3 · test_748_2(+ helper)는 화면이 없어져 지움 · test_794_1 · test_790_2 는 화면 · TS 거울만 덜고 서버 시험 남김 · test_714_2 · 717_2b · 721 · 722(원래 틀린 한 줄 고침) · 734 · 737 · 750_2 · 793_3b · 796_2 · 796_2b 는 보기 둘 · 새 파일에 맞춤 · 새 test_799_2_old_tabs_gone

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 12:57:30 +09:00
co-authored by Claude Sonnet 5.5
parent d13b5b1753
commit c9bdf6d891
49 changed files with 389 additions and 3817 deletions
@@ -1,163 +0,0 @@
/* =============================================================================
* 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;
}