feat(750-2): 단가산출 보기 넷 = SK 셈 답 하나 — 시험 계산 · 텍스트 수식도 SK 답(글 · 중간) · 바탕 M01 로직 따로 셈 걷음 · 옵션 바꿈 서버 셈 한 번
- Sangeun_Detail — 텍스트 수식 = 답 `글` · 시험 계산 = 답 `줄` · `기준` · `중간`(`buildCalc` 덧칸 `answer` · `shown` · `held`) · `/logic-lib/calc` · `/text` · 미리보기 부름 · `baseCovers` · `dropText` · `applyCovers` 걷음 · 바탕 원값 셈 = `보기: false` - 재료 고름(줄 모달 · 시험 계산 재료 칸) = SK 문서 `고름`(cid 열쇠 · 둘째 기초 세부 줄은 보기만) - LogicLab_Ho — 읽는 식 줄을 `cid` 로 이음(`HoContext.cids` · 이름 차례 추측은 M01 만) · LogicLab_Formula — 단위 환산 `나눔` 머리 한 줄 - 낡은 시험 고침 — test_717_2b(`baseCovers` · `dropText` 단정 뺌 · 함수 지움) · test_725_1(`void recalc()` 새 꼴) · 새 지킴 test_750_2 - M01 단가산출 기초 = 덧칸 안 넘겨 옛 길 그대로 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,5 +228,9 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
|
||||
const sums = Object.fromEntries(
|
||||
FOOT_KEYS.map((k) => [k, text.groups.find((g) => FOOT_OF[g.비목] === k)?.소계 ?? 0]),
|
||||
) as Record<FootKey, number>;
|
||||
host.replaceChildren(...boxes, buildFoot(sums, calculated));
|
||||
const divide =
|
||||
text.나눔 && text.나눔 !== 1
|
||||
? [muted(`${tl("Formula_Divide")} ÷ ${text.나눔}`)] // M02 750-1 단위 환산 — 식은 바탕 단위 그대로
|
||||
: [];
|
||||
host.replaceChildren(...divide, ...boxes, buildFoot(sums, calculated));
|
||||
}
|
||||
|
||||
@@ -145,6 +145,10 @@ export interface HoContext {
|
||||
calcHost?: HTMLElement;
|
||||
/** 그 자리 고침 고리(없으면 모달은 보기만) */
|
||||
edit?: HoEdit;
|
||||
/** (M02 750-2) Ho 줄 차례 → SK 줄 cid — 읽는 식 줄을 `cid` 로 이음(이름 차례 추측 안 함) */
|
||||
cids?: string[];
|
||||
/** (M02 750-2) 재료 고름 = SK 문서 `고름`(줄 차례 → 고르는 길 · 없으면 모달은 보기만) — 없으면 시험 입력(M01) */
|
||||
pickMat?: (idx: number) => ((it: { ref: string; label: string }) => void) | undefined;
|
||||
}
|
||||
|
||||
type Group = "labor" | "material" | "cost" | "other";
|
||||
@@ -272,9 +276,11 @@ function modalOf(e: Entry, ctx: HoContext, extra: { cond?: PickCond; item?: HoLi
|
||||
// 재료 줄 = 시험 계산 자재 칸과 같은 모달 — 고르면 시험 입력으로 다시 셈(정본 안 바뀜)
|
||||
kind: item?.종류,
|
||||
usedSlot: line?.품목?.값칸,
|
||||
...(item?.종류 === "재료" && ctx.calcHost
|
||||
? { onPick: (it) => pickMaterial(ctx.calcHost!, ctx.values, e.idx, it) }
|
||||
: {}),
|
||||
...(item?.종류 === "재료" && ctx.pickMat
|
||||
? { onPick: ctx.pickMat(e.idx) }
|
||||
: item?.종류 === "재료" && ctx.calcHost
|
||||
? { onPick: (it) => pickMaterial(ctx.calcHost!, ctx.values, e.idx, it) }
|
||||
: {}),
|
||||
// 덧줄(%) = M02 덮개만 율 고침(M01 시험 값은 호표 줄만)
|
||||
...(edit && (e.extra ? edit.scope === "m02" : item)
|
||||
? {
|
||||
@@ -378,9 +384,15 @@ function attachPieces(all: Entry[], ctx: HoContext): void {
|
||||
}
|
||||
const names = [...(ctx.row.호표 ?? []).map(nameOf), ...(ctx.row.덧줄 ?? []).map((x) => x.이름)];
|
||||
const found = new Map<string, TextLine>();
|
||||
const byCid = new Map((ctx.cids ?? []).map((c, i) => [c, i])); // M02 750-2 — cid 로 이음
|
||||
for (const g of source.groups) {
|
||||
let from = 0;
|
||||
for (const line of g.줄) {
|
||||
if (ctx.cids && line.cid) {
|
||||
const at = byCid.get(line.cid);
|
||||
if (at !== undefined) found.set(`${g.비목}|${at}`, line);
|
||||
continue;
|
||||
}
|
||||
const at = names.findIndex((n, i) => i >= from && n === line.이름);
|
||||
if (at < 0) continue;
|
||||
found.set(`${g.비목}|${at}`, line);
|
||||
|
||||
@@ -27,6 +27,10 @@ const TEXT = {
|
||||
Ho_Title: ["단가산출 상세", "Unit-price detail"],
|
||||
Formula_Title: ["텍스트 수식", "Text formula"],
|
||||
Formula_Slot: ["시험 계산을 누르면 식이 글로 나옴", "Run the trial calc to see the formula"],
|
||||
Formula_Divide: [
|
||||
"식은 바탕 단위 기준 · 금액 · 소계 = 식 값",
|
||||
"Formulas use the base unit · amounts and subtotals = formula value",
|
||||
],
|
||||
Calc_Title: ["시험 계산", "Trial calculation"],
|
||||
View_Est: ["인력·재료·경비", "Labor·material·expense"],
|
||||
View_Label: ["보기", "View"],
|
||||
|
||||
@@ -158,6 +158,8 @@ export interface TextLine {
|
||||
장비?: boolean | null;
|
||||
기준단위?: string | null;
|
||||
환산근거?: string;
|
||||
/** (M02 750-1) SK 셈 답 줄 cid — 상세 줄과 이음 */
|
||||
cid?: string;
|
||||
}
|
||||
/** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */
|
||||
export type TextAnswer =
|
||||
@@ -165,6 +167,8 @@ export type TextAnswer =
|
||||
ok: true;
|
||||
groups: { 비목: string; 줄: TextLine[]; 소계: number; 끝수: string }[];
|
||||
계: number;
|
||||
/** (M02 750-1) 단위 환산 배 ≠ 1 — 식은 바탕 단위 · 금액 · 소계 · 계는 ÷배 */
|
||||
나눔?: number;
|
||||
}
|
||||
| { ok: false; reason: string };
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ 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;
|
||||
@@ -63,9 +69,15 @@ export interface CalcContext {
|
||||
dirty: () => boolean;
|
||||
/** 로직마다 넣은 값 — 다시 그려도 남음 */
|
||||
values: Record<string, string>;
|
||||
onLines: (lines: CalcLine[] | null) => void;
|
||||
onLines?: (lines: CalcLine[] | null) => 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 =>
|
||||
@@ -133,11 +145,11 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
children: [el("span", { text: label }), control],
|
||||
});
|
||||
});
|
||||
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again);
|
||||
const materials = materialRows(ctx.row, ctx.prices ?? {}, ctx.values, again, ctx.held);
|
||||
const out = el("div", { className: "m01-logic__calc-out" });
|
||||
const mySeqAtOpen = ctx.onText ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
|
||||
const mySeqAtOpen = ctx.onText && !ctx.answer ? previewSeq + 1 : previewSeq; // 이 그리기가 받을 순번
|
||||
let manualRun = false; // [계산] 이 이미 답했으면 뒤늦게 오는 미리보기로 안 덮음
|
||||
if (ctx.onText) {
|
||||
if (ctx.onText && !ctx.answer) {
|
||||
const mySeq = ++previewSeq;
|
||||
const draft = ctx.dirty() || ctx.savedKey === null;
|
||||
const body = {
|
||||
@@ -168,8 +180,22 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
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();
|
||||
@@ -189,9 +215,9 @@ 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);
|
||||
ctx.onLines?.(answer.ok ? (answer.lines ?? null) : null);
|
||||
if (answer.ok && answer.lines) showItems(host, ctx.values, answer.lines);
|
||||
out.replaceChildren(...answerView(answer, draft, (ctx.row.결과단위 ?? "").startsWith("원")));
|
||||
out.replaceChildren(...answerView(answer, draft, money));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
@@ -211,6 +237,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
runButton,
|
||||
out,
|
||||
);
|
||||
if (ctx.shown) paint(ctx.shown);
|
||||
}
|
||||
|
||||
/** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */
|
||||
|
||||
@@ -76,6 +76,8 @@ export function materialRows(
|
||||
prices: Record<string, ElementBrief | null>,
|
||||
values: Record<string, string>,
|
||||
again: () => void,
|
||||
/** (M02 750-2) 고르면 SK 문서 `고름`에 — 줄 차례 → 고른 재료 · 없으면 시험 입력(M01) */
|
||||
sink?: (i: number, it: Chosen) => void,
|
||||
): HTMLElement[] {
|
||||
const mine = picksOf(values);
|
||||
return (row.호표 ?? [])
|
||||
@@ -117,6 +119,7 @@ export function materialRows(
|
||||
onPick: (it) => {
|
||||
mine.set(i, it);
|
||||
current.textContent = it.label;
|
||||
sink?.(i, it);
|
||||
again();
|
||||
},
|
||||
});
|
||||
@@ -128,7 +131,7 @@ export function materialRows(
|
||||
export function showItems(
|
||||
host: HTMLElement,
|
||||
values: Record<string, string>,
|
||||
lines: CalcLine[],
|
||||
lines: (CalcLine | undefined)[],
|
||||
): void {
|
||||
const mine = picksOf(values);
|
||||
host.querySelectorAll<HTMLElement>("[data-mat-idx]").forEach((span) => {
|
||||
|
||||
Reference in New Issue
Block a user