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:
@@ -0,0 +1,136 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Logic_Drive.ts
|
||||
* 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)과 로직 상세(`LogicLab`)가 같이 씀.
|
||||
* `driveRun` = 한 번 셈(`/calc` + `/text`) · `previewText` = 열 때 읽는 식 미리보기 · `autoFill` = 자동값 채움
|
||||
* `createDrive` = 로직 상세용 묶음 — 열 때 미리보기 + 첫 셈(입력 없으면 바로 · 있으면 자동값 채운 뒤) · `run` 은 칸 없이 다시 셈
|
||||
* 셈 자리 = 서버(CLAUDE.md 5 장 ③) · 화면 · sessionStorage 없음 · 고른 재료(`CalcPick`) 는 값 객체에 딸림
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchAuto,
|
||||
runCalc,
|
||||
runText,
|
||||
type CalcAnswer,
|
||||
type CalcLine,
|
||||
type LogicRow,
|
||||
type TextAnswer,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { isMaterialKeyInput, pickBody } from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
|
||||
export interface DriveContext {
|
||||
/** 저장된 키 — 새 로직은 null */
|
||||
savedKey: string | null;
|
||||
/** 저장 전 새 초안이면 비움 — 서버가 임시 자리에 얹어 셈 */
|
||||
file?: string;
|
||||
row: LogicRow;
|
||||
dirty: () => boolean;
|
||||
/** 로직마다 넣은 값 — 다시 그려도 남음 */
|
||||
values: Record<string, string>;
|
||||
onLines?: (lines: CalcLine[] | null) => void;
|
||||
/** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */
|
||||
onText?: (answer: TextAnswer) => void;
|
||||
}
|
||||
|
||||
/** 칸에 든 값 → 엔진 입력 — 빈 칸은 안 보냄(엔진이 「입력 없음」 으로 멈춤) · 고르기는 원래 값 그대로 */
|
||||
function inputsOf(row: LogicRow, values: Record<string, string>): Record<string, unknown> {
|
||||
const inputs: Record<string, unknown> = {};
|
||||
for (const spec of row.입력 ?? []) {
|
||||
const raw = (values[spec.이름] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const option = spec.고르기?.find((o) => String(o) === raw); // 글 "35" 로 보내면 「고르기 밖」
|
||||
inputs[spec.이름] =
|
||||
option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw);
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
const draftBody = (ctx: DriveContext): { row?: LogicRow; file?: string } =>
|
||||
ctx.dirty() || ctx.savedKey === null ? { row: ctx.row, file: ctx.file } : {};
|
||||
|
||||
/** 한 번 셈 — 답을 `onText` → `onLines` 차례로 알리고 돌려줌(`draft` = 저장 전 고침으로 셈) */
|
||||
export async function driveRun(
|
||||
ctx: DriveContext,
|
||||
inputs = inputsOf(ctx.row, ctx.values),
|
||||
): Promise<{ answer: CalcAnswer; draft: boolean }> {
|
||||
const draft = ctx.dirty() || ctx.savedKey === null;
|
||||
const body = {
|
||||
key: ctx.savedKey ?? ctx.row.키,
|
||||
inputs,
|
||||
...pickBody(ctx.values),
|
||||
...draftBody(ctx),
|
||||
};
|
||||
const answer = await runCalc(body);
|
||||
if (ctx.onText) ctx.onText(await runText(body));
|
||||
ctx.onLines?.(answer.ok ? (answer.lines ?? null) : null);
|
||||
return { answer, draft };
|
||||
}
|
||||
|
||||
/** 열 때 읽는 식 미리보기 — [계산] 전에도 이름 식 칸이 비치게 · 늦게 온 응답은 `live()` 로 거름 */
|
||||
export function previewText(ctx: DriveContext, live: () => boolean): void {
|
||||
if (!ctx.onText) return;
|
||||
const body = { key: ctx.savedKey ?? ctx.row.키, inputs: {}, ...draftBody(ctx) };
|
||||
void runText(body).then((answer) => {
|
||||
if (!live()) return;
|
||||
(answer as TextAnswer & { 계산됨?: boolean }).계산됨 = false;
|
||||
ctx.onText?.(answer);
|
||||
});
|
||||
}
|
||||
|
||||
/** 자동값으로 빈 칸 채움 — 채웠으면 true */
|
||||
export async function autoFill(ctx: DriveContext): Promise<boolean> {
|
||||
if (ctx.savedKey === null) return false;
|
||||
const auto = await fetchAuto(ctx.savedKey);
|
||||
const names = (ctx.row.입력 ?? [])
|
||||
.map((spec) => spec.이름)
|
||||
.filter((n) => auto[n] !== undefined && auto[n] !== null && !(ctx.values[n] ?? "").trim());
|
||||
for (const n of names) ctx.values[n] = String(auto[n]);
|
||||
return names.length > 0;
|
||||
}
|
||||
|
||||
/** 로직마다 첫 셈을 한 번만(다시 그려도 안 부름) */
|
||||
const first = new WeakSet<Record<string, string>>();
|
||||
|
||||
/** 로직 상세용 구동 — `start` = 로직을 열거나 입력 칸이 바뀔 때 · `run` = 지금 값으로 다시 셈 */
|
||||
export function createDrive(): {
|
||||
start: (ctx: DriveContext) => void;
|
||||
run: () => Promise<void>;
|
||||
stop: () => void;
|
||||
} {
|
||||
let seq = 0; // 로직을 바꾸면 늦게 온 응답을 거름
|
||||
let now: DriveContext | null = null;
|
||||
let ran = false; // 셈 답이 이미 왔으면 뒤늦은 미리보기로 안 덮음
|
||||
const run = async (): Promise<void> => {
|
||||
const ctx = now;
|
||||
if (!ctx) return;
|
||||
ran = true;
|
||||
try {
|
||||
await driveRun(ctx);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
};
|
||||
const start = (ctx: DriveContext): void => {
|
||||
now = ctx;
|
||||
ran = false;
|
||||
const mine = ++seq;
|
||||
previewText(ctx, () => !ran && mine === seq);
|
||||
if (first.has(ctx.values)) return;
|
||||
if (!(ctx.row.입력 ?? []).some((spec) => !isMaterialKeyInput(spec))) {
|
||||
first.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 셈
|
||||
queueMicrotask(() => mine === seq && void run());
|
||||
} else if (ctx.savedKey !== null) {
|
||||
first.add(ctx.values);
|
||||
void autoFill(ctx).then((filled) => filled && mine === seq && void run());
|
||||
}
|
||||
};
|
||||
return {
|
||||
start,
|
||||
run,
|
||||
stop: () => {
|
||||
now = null;
|
||||
seq++;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user