feat(M01): 로직 화면 — 자동값 채움 · 서버 조각 알약 · 재료 고르기 모달 · 돈 모양 한 벌 (PLAN 3-4 화면)
- 시험 계산: 로직을 열 때 GET /logic/auto 값을 빈 입력에 채우고 「견본」 딱지(사용자가 고치면 빠짐) 뒤 바로 계산 · 텍스트 수식까지. 서버 길이 없으면 안 채움 - 단가산출 상세 수량 칸: 화면 쪽 식 풀이(qtyFragments) 걷고 /text 줄의 조각으로 그림 — 알약 글 = 조각 글 · 깊이 있는 조각은 흐리게 · 묶음별 줄 수가 안 맞으면 식 글자 그대로 - 표 알약 모달: 참조.행·열에 색(행을 못 받으면 열만) - 재료 고르기 줄(요소가 객체): 모달이 객체를 받게 — 잡힌 품목 단가 + 후보 목록 - 돈 모양 한 벌(Logic_Money.ts): 상세·모달·텍스트 수식 소계와 계·시험 계산 결과·조합 미리 보기 정수 · 수량·비율은 그대로 - 서버 파일은 안 만짐 검증 — typecheck 통과 · GF000160 ORCA: 알약 7 서로 다름 · 계 141,882 · 재료 줄 모달 · GF000219·GF000001 회귀 없음. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EY2vdBQkHjdih9GgCsp9Fd
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchAuto,
|
||||
runCalc,
|
||||
runText,
|
||||
searchElements,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
type LogicRow,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
|
||||
/** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */
|
||||
@@ -55,10 +57,19 @@ export interface CalcContext {
|
||||
|
||||
const SUMS = ["노무비", "재료비", "경비", "계"];
|
||||
|
||||
/** 로직마다 자동값을 한 번만 받음(다시 그려도 안 부름) · 견본으로 채운 입력 이름 — 사용자가 고치면 빠짐 */
|
||||
const autoAsked = new WeakSet<Record<string, string>>();
|
||||
const sampled = new WeakMap<Record<string, string>, Set<string>>();
|
||||
|
||||
/** 로직을 열 때마다 새로 부르는 미리보기 — 늦게 온 응답이 새 로직을 덮지 않게 순번으로 거름 */
|
||||
let previewSeq = 0;
|
||||
|
||||
export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const marks = sampled.get(ctx.values) ?? new Set<string>();
|
||||
const edited = (name: string, tag?: HTMLElement): void => {
|
||||
marks.delete(name);
|
||||
tag?.remove();
|
||||
};
|
||||
// 그릇 폭 280px 고정 칸은 옛 테스트 컨테이너(UI_Test*, 걷힘)의 죽은 CSS 규칙 —
|
||||
// 지금 쓰는 LogicLab 화면은 그 칸에 안 걸려 폭이 이미 넓음(실측 730px) · 따로 안 풂
|
||||
const optionText = (o: string | number): string =>
|
||||
@@ -67,12 +78,18 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const name = spec.이름;
|
||||
let control: HTMLElement;
|
||||
const picks = spec.고르기;
|
||||
const tag = marks.has(name)
|
||||
? el("span", { className: "m01-logic__tag", text: tx("Calc_Sample") })
|
||||
: undefined;
|
||||
if (picks?.length) {
|
||||
const pick = createSelectField({
|
||||
options: ["", ...picks.map(String)].map((o) => ({ value: o, text: optionText(o) })),
|
||||
value: ctx.values[name] ?? "",
|
||||
compact: true,
|
||||
onChange: (v) => (ctx.values[name] = v),
|
||||
onChange: (v) => {
|
||||
ctx.values[name] = v;
|
||||
edited(name, tag);
|
||||
},
|
||||
});
|
||||
control = pick.root;
|
||||
const keys = picks.filter((o): o is string => typeof o === "string" && KEY_RE.test(o));
|
||||
@@ -91,7 +108,10 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
});
|
||||
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
|
||||
box.value = ctx.values[name] ?? "";
|
||||
box.addEventListener("input", () => (ctx.values[name] = box.value));
|
||||
box.addEventListener("input", () => {
|
||||
ctx.values[name] = box.value;
|
||||
edited(name, tag);
|
||||
});
|
||||
control = box;
|
||||
}
|
||||
const label = spec.단위 ? `${name} (${spec.단위})` : name;
|
||||
@@ -99,10 +119,11 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const wide = label.length > 10;
|
||||
return el("label", {
|
||||
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
|
||||
children: [el("span", { text: label }), control],
|
||||
children: [el("span", { text: label }), ...(tag ? [tag] : []), control],
|
||||
});
|
||||
});
|
||||
const out = el("div", { className: "m01-logic__calc-out" });
|
||||
const mySeqAtOpen = ctx.onText ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
|
||||
let manualRun = false; // [계산] 이 이미 답했으면 뒤늦게 오는 미리보기로 안 덮음
|
||||
if (ctx.onText) {
|
||||
const mySeq = ++previewSeq;
|
||||
@@ -118,6 +139,19 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
ctx.onText?.(answer);
|
||||
});
|
||||
}
|
||||
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]);
|
||||
sampled.set(ctx.values, new Set([...marks, ...names]));
|
||||
buildCalc(host, ctx); // 채운 값·견본 딱지를 새로 그리고 바로 계산(결과 · 텍스트 수식까지)
|
||||
host.querySelector("button")?.click();
|
||||
});
|
||||
}
|
||||
const run = async (): Promise<void> => {
|
||||
manualRun = true;
|
||||
const inputs: Record<string, unknown> = {};
|
||||
@@ -139,7 +173,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const answer = await runCalc(body);
|
||||
if (ctx.onText) ctx.onText(await runText(body));
|
||||
ctx.onLines(answer.ok ? (answer.lines ?? null) : null);
|
||||
out.replaceChildren(...answerView(answer, draft));
|
||||
out.replaceChildren(...answerView(answer, draft, (ctx.row.결과단위 ?? "").startsWith("원")));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
@@ -154,7 +188,8 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
/** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */
|
||||
function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] {
|
||||
const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : [];
|
||||
if (!answer.ok) {
|
||||
return [
|
||||
@@ -172,7 +207,7 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
children: [
|
||||
el("td", { text: line.출처 ? `${line.이름} (${line.출처})` : line.이름 }),
|
||||
el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }),
|
||||
el("td", { className: "m01-logic__money", text: formatNumber(line.금액) }),
|
||||
el("td", { className: "m01-logic__money", text: formatMoney(line.금액) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -200,7 +235,7 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
className: "m01-logic__sums",
|
||||
children: SUMS.flatMap((k) => [
|
||||
el("dt", { text: k === "계" ? tx("Calc_Sum") : k }),
|
||||
el("dd", { className: "m01-logic__money", text: formatNumber(answer.sums?.[k] ?? 0) }),
|
||||
el("dd", { className: "m01-logic__money", text: formatMoney(answer.sums?.[k] ?? 0) }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
@@ -211,7 +246,7 @@ function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] {
|
||||
className: "m01-logic__sums",
|
||||
children: [
|
||||
el("dt", { text: tx("Calc_Result") }),
|
||||
el("dd", { text: formatNumber(answer.result) }),
|
||||
el("dd", { text: money ? formatMoney(answer.result) : formatNumber(answer.result) }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user