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
+9 -87
View File
@@ -2,28 +2,25 @@
* M01_MasterData_UI_Logic_Calc.ts
* 시험 계산 — 받을 값을 넣고 [계산] → 줄별 금액 · 비목 합 · 멈춘 까닭(엔진 글 그대로)
* 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀
* 셈 구동(입력 → 서버 → 답) = `_Logic_Drive.ts` — 로직 상세는 이 칸 없이 그 구동만 씀(799-2) · 이 칸은 로직 복사 화면이 씀
* ========================================================================== */
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
import {
fetchAuto,
pctView,
runCalc,
runText,
searchElements,
type CalcAnswer,
type CalcLine,
type ElementBrief,
type TextAnswer,
type LogicRow,
} from "./M01_MasterData_UI_Logic_Api";
import {
isMaterialKeyInput,
machineControl,
materialRows,
pickBody,
showItems,
} from "./M01_MasterData_UI_Logic_CalcPick";
import { autoFill, driveRun } from "./M01_MasterData_UI_Logic_Drive";
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { formatLineMoney, formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
@@ -52,12 +49,6 @@ async function keyName(key: string): Promise<string | null> {
return name;
}
/** (M02 750-2) SK 셈 답으로 그리는 시험 계산 — 답 + 호표 차례 줄(재료 칸 잡힌 품목 글용) */
export interface CalcShown {
answer: CalcAnswer;
items?: (CalcLine | undefined)[];
}
export interface CalcContext {
/** 저장된 키 — 새 로직은 null */
savedKey: string | null;
@@ -70,16 +61,6 @@ export interface CalcContext {
/** 로직마다 넣은 값 — 다시 그려도 남음 */
values: Record<string, string>;
onLines?: (lines: CalcLine[] | null) => void;
/** 794-1 셈 답 통째(중간 값 · 소계) — M01 줄 식 시트 값 */
onAnswer?: (answer: CalcAnswer) => void;
/** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */
onText?: (answer: TextAnswer) => void;
/** (M02 750-2) 있으면 [계산] = 이 셈(SK 셈 답 하나) · `/calc` · `/text` · 미리보기 안 부름 */
answer?: () => Promise<CalcShown | null>;
/** (M02 750-2) 이미 받은 SK 셈 답 — 그릴 때 바로 결과로 */
shown?: CalcShown | null;
/** (M02 750-2) 재료 고름 = SK 문서 `고름` 에도 */
held?: (i: number, it: { ref: string; label: string }) => void;
}
const grid = (children: HTMLElement[]): HTMLElement =>
@@ -87,12 +68,9 @@ const grid = (children: HTMLElement[]): HTMLElement =>
const SUMS = ["노무비", "재료비", "경비", "계"];
/** 로직마다 자동값을 한 번만 받음(다시 그려도 안 부름) */
/** 로직마다 첫 셈을 한 번만(다시 그려도 안 부름) */
const autoAsked = new WeakSet<Record<string, string>>();
/** 로직을 열 때마다 새로 부르는 미리보기 — 늦게 온 응답이 새 로직을 덮지 않게 순번으로 거름 */
let previewSeq = 0;
export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
// 그릇 폭 280px 고정 칸은 옛 테스트 컨테이너(UI_Test*, 걷힘)의 죽은 CSS 규칙 —
// 지금 쓰는 LogicLab 화면은 그 칸에 안 걸려 폭이 이미 넓음(실측 730px) · 따로 안 풂
@@ -147,78 +125,23 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
children: [el("span", { text: label }), control],
});
});
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again, ctx.held);
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again);
const out = el("div", { className: "m01-logic__calc-out" });
const mySeqAtOpen = ctx.onText && !ctx.answer ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
let manualRun = false; // [계산] 이 이미 답했으면 뒤늦게 오는 미리보기로 안 덮음
if (ctx.onText && !ctx.answer) {
const mySeq = ++previewSeq;
const draft = ctx.dirty() || ctx.savedKey === null;
const body = {
key: ctx.savedKey ?? ctx.row.키,
inputs: {},
...(draft ? { row: ctx.row, file: ctx.file } : {}),
};
void runText(body).then((answer) => {
if (manualRun || mySeq !== previewSeq) return; // [계산] 을 눌렀거나 다른 로직이 이미 열림
(answer as TextAnswer & { 계산됨?: boolean }).계산됨 = false;
ctx.onText?.(answer);
});
}
if (!fields.length && !autoAsked.has(ctx.values)) {
autoAsked.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 계산
queueMicrotask(() => {
if (previewSeq === mySeqAtOpen) again();
});
queueMicrotask(again);
} else if (ctx.savedKey !== null && !autoAsked.has(ctx.values)) {
autoAsked.add(ctx.values);
void fetchAuto(ctx.savedKey).then((auto) => {
const names = (ctx.row.입력 ?? [])
.map((spec) => spec.이름)
.filter((n) => auto[n] !== undefined && auto[n] !== null && !(ctx.values[n] ?? "").trim());
if (!names.length || previewSeq !== mySeqAtOpen) return; // 값이 없거나 다른 로직이 이미 열림
for (const n of names) ctx.values[n] = String(auto[n]);
buildCalc(host, ctx); // 채운 값을 새로 그리고 바로 계산(결과 · 텍스트 수식까지)
void autoFill(ctx).then((filled) => {
if (!filled) return;
buildCalc(host, ctx); // 채운 값을 새로 그리고 바로 계산
again();
});
}
const money = (ctx.row.결과단위 ?? "").startsWith("원");
const paint = (s: CalcShown): void => {
out.replaceChildren(...answerView(s.answer, false, money));
if (s.answer.ok) showItems(host, ctx.values, s.items ?? s.answer.lines ?? []);
};
const run = async (): Promise<void> => {
manualRun = true;
if (ctx.answer) {
try {
const got = await ctx.answer();
if (got) paint(got);
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
}
return;
}
const inputs: Record<string, unknown> = {};
for (const spec of ctx.row.입력 ?? []) {
const raw = (ctx.values[spec.이름] ?? "").trim();
if (raw === "") continue; // 빈 칸은 안 보냄 — 엔진이 「입력 없음」 으로 멈춤
// 고르기는 원래 값(수면 수) 그대로 — 글 "35" 로 보내면 「고르기 밖」
const option = spec.고르기?.find((o) => String(o) === raw);
inputs[spec.이름] =
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
}
const draft = ctx.dirty() || ctx.savedKey === null;
try {
const body = {
key: ctx.savedKey ?? ctx.row.키,
inputs,
...pickBody(ctx.values),
...(draft ? { row: ctx.row, file: ctx.file } : {}),
};
const answer = await runCalc(body);
if (ctx.onText) ctx.onText(await runText(body));
ctx.onAnswer?.(answer);
ctx.onLines?.(answer.ok ? (answer.lines ?? null) : null);
const { answer, draft } = await driveRun(ctx);
if (answer.ok && answer.lines) showItems(host, ctx.values, answer.lines);
out.replaceChildren(...answerView(answer, draft, money));
} catch (error) {
@@ -240,7 +163,6 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
runButton,
out,
);
if (ctx.shown) paint(ctx.shown);
}
/** 중간 값 이름이 돈 꼴(경비 · 노무비 · 공구손료 · 금액 · 단가 · 노임) — 1,000 미만이어도 돈 꼴 */