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:
@@ -28,11 +28,12 @@ export function calcSangeun<T = SkAnswer>(
|
||||
scope: SkScope,
|
||||
doc: Record<string, unknown>,
|
||||
given: Record<string, string> = {},
|
||||
seen = true,
|
||||
): Promise<T> {
|
||||
const 입력값 = Object.fromEntries(Object.entries(given).filter(([, v]) => String(v).trim()));
|
||||
return call<T>(`/layers/${scope.layer}/sangeun/calc${q(scope)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ 문서: doc, 입력값 }),
|
||||
body: JSON.stringify({ 문서: doc, 입력값, 보기: seen }), // 보기 false = 글 · 중간 덧칸 안 지음(바탕 원값 셈)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { calcSheetRows } from "../A00_Common/calc_sheet";
|
||||
import { swapUnit } from "../A00_Common/master_unit_swap";
|
||||
import { buildCalc } from "../M01_MasterData/M01_MasterData_UI_Logic_Calc";
|
||||
import { buildCalc, type CalcShown } from "../M01_MasterData/M01_MasterData_UI_Logic_Calc";
|
||||
import {
|
||||
fetchLogic,
|
||||
setLogicApiPaths,
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
type LogicRow,
|
||||
type TextAnswer,
|
||||
} from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { applyCovers } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Cover";
|
||||
import { buildLabDetail } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Detail";
|
||||
import { buildHo, lineOpener, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho";
|
||||
import { createViewPicker } from "../M01_MasterData/M01_MasterData_UI_LogicLab_View";
|
||||
@@ -41,10 +40,8 @@ import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { baseUnitOf, buildUnitSwap, type SwapHow } from "./M02_MasterTemplete_Sangeun_Unit";
|
||||
import {
|
||||
baseCovers,
|
||||
dropBase,
|
||||
dropLine,
|
||||
dropText,
|
||||
goneRows,
|
||||
hoView,
|
||||
removedSubs,
|
||||
@@ -89,6 +86,7 @@ export function mountSangeunDetail(
|
||||
let lastText: TextAnswer | undefined;
|
||||
let ans: SkAnswer | null = null;
|
||||
let bare: SkAnswer | null = null;
|
||||
let okKey = ""; // 마지막으로 잘 받은 셈의 입력(문서 + 시험 값) — 같으면 [계산] 이 다시 안 셈
|
||||
let fail = "";
|
||||
let extra: LogicInput[] = []; // 더한줄 로직 입력 중 바탕에 없는 것 = 옵션 칸(725-6 · 727-3)
|
||||
const inputsOf = new Map<string, LogicInput[]>();
|
||||
@@ -204,21 +202,26 @@ export function mountSangeunDetail(
|
||||
else drawHead();
|
||||
};
|
||||
|
||||
const inputKey = (): string => JSON.stringify([doc, values]);
|
||||
|
||||
async function recalc(): Promise<void> {
|
||||
const mine = ++seq;
|
||||
await syncExtra();
|
||||
if (mine !== seq || !alive) return;
|
||||
const plain = { ...doc, 덮개: {}, 뺀줄: [], 더한줄: [] };
|
||||
const key = inputKey();
|
||||
try {
|
||||
const [got, was] = await Promise.all([
|
||||
calcSangeun(scope, doc, values),
|
||||
doc.바탕 ? calcSangeun(scope, plain, values) : Promise.resolve(null),
|
||||
doc.바탕 ? calcSangeun(scope, plain, values, false) : Promise.resolve(null), // 바탕 원값 = 보기 덧칸 없이
|
||||
]);
|
||||
if (mine !== seq || !alive) return; // 늦게 온 답 — 버림
|
||||
[ans, bare, fail] = [got, was, ""];
|
||||
[ans, bare, fail, okKey] = [got, was, "", key];
|
||||
text = got.글 ?? null; // 750-2 텍스트 수식 · 시험 계산 = 이 답 하나(바탕 로직 따로 셈 없음)
|
||||
if (text?.ok) lastText = text;
|
||||
} catch (error) {
|
||||
if (mine !== seq || !alive) return;
|
||||
[ans, fail] = [null, error instanceof Error ? error.message : String(error)];
|
||||
[ans, text, fail] = [null, null, error instanceof Error ? error.message : String(error)];
|
||||
}
|
||||
draw();
|
||||
}
|
||||
@@ -313,21 +316,18 @@ export function mountSangeunDetail(
|
||||
];
|
||||
const row = base ? { ...base.logic, 입력: [...(base.logic.입력 ?? []), ...extra] } : standIn();
|
||||
const view = ans ? hoView(ans, bare, base?.logic ?? null, doc) : null;
|
||||
const baseNames = [...(row.호표 ?? []), ...(row.덧줄 ?? [])].map((x) =>
|
||||
String(x.이름 ?? ("요소" in x ? x.요소 : "")),
|
||||
);
|
||||
const cut = <T extends TextAnswer | undefined>(t: T): T =>
|
||||
(t ? dropText(t, baseNames, bare, doc) : t) as T;
|
||||
const hoCtx = {
|
||||
row: view ? { ...row, 호표: view.호표, 덧줄: view.덧줄 } : row,
|
||||
prices: base?.prices ?? {},
|
||||
lines: (view?.lines ?? null) as CalcLine[] | null,
|
||||
text: cut(text ?? undefined) ?? null,
|
||||
lastText: cut(lastText),
|
||||
text,
|
||||
lastText,
|
||||
values,
|
||||
onInput,
|
||||
calcHost,
|
||||
edit: view && edit ? hoEdit(view, rows) : undefined,
|
||||
cids: view?.cids,
|
||||
pickMat: (idx: number) => pickMat(view?.cids[idx]),
|
||||
};
|
||||
const ho = el("div", {
|
||||
attrs: { "data-sk-cover": "" },
|
||||
@@ -381,6 +381,16 @@ export function mountSangeunDetail(
|
||||
onInput,
|
||||
cover: { ho, est, open: lineOpener(hoCtx) }, // 748-2 텍스트 수식 변수 · 값 누름 = 같은 줄 모달
|
||||
});
|
||||
drawCalc();
|
||||
}
|
||||
|
||||
/** 재료 고름(줄 모달 · 시험 계산 재료 칸) = SK 문서 `고름`(cid → MT) — 둘째 기초 세부 줄은 보기만 */
|
||||
function pickMat(cid: string | undefined): ((it: { ref: string }) => void) | undefined {
|
||||
if (!cid || cid.startsWith("add:#")) return undefined;
|
||||
return (it) => {
|
||||
doc.고름 = { ...(doc.고름 ?? {}), [cid]: it.ref };
|
||||
touch();
|
||||
};
|
||||
}
|
||||
|
||||
/** 공용 상세 고침 고리 — 열쇠 = Ho 줄 차례 → cid · 고침 = SK 덮개 · 더한줄(M01 안 바뀜 · 내역 17-2) ·
|
||||
@@ -396,7 +406,6 @@ export function mountSangeunDetail(
|
||||
if (!r) return;
|
||||
setLineCover(doc, r, c, opened());
|
||||
touch();
|
||||
if (base) rerunCalc(); // 시험 계산 · 텍스트 수식 = 덮개 사본으로
|
||||
},
|
||||
more: (idx, close) => {
|
||||
const r = at(idx);
|
||||
@@ -442,11 +451,6 @@ export function mountSangeunDetail(
|
||||
};
|
||||
}
|
||||
|
||||
function rerunCalc(): void {
|
||||
drawCalc();
|
||||
calcHost.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
}
|
||||
|
||||
/** 옵션 칸 · 텍스트 수식 · 계산서 입력 단추 — 입력 고침 → SK `입력값`(727-3 · 열린입력 = 시험 값만) ·
|
||||
* 시험 계산 다시 그림 · 셈 */
|
||||
function onInput(name: string, v: string): void {
|
||||
@@ -458,9 +462,7 @@ export function mountSangeunDetail(
|
||||
doc.입력값 = kept;
|
||||
touch(false);
|
||||
}
|
||||
drawCalc();
|
||||
calcHost.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
if (!base) void recalc(); // 바탕 없음 — 시험 계산 단추 없음 · SK 셈을 바로
|
||||
void recalc(); // 750-2 옵션 바꿈 = 서버 셈 한 번 — 네 보기가 그 답 하나로 다시 그려짐
|
||||
}
|
||||
|
||||
function drawCalc(): void {
|
||||
@@ -468,24 +470,38 @@ export function mountSangeunDetail(
|
||||
calcHost.replaceChildren(createText(L("M02_SangeunNoInputs")));
|
||||
return;
|
||||
}
|
||||
const covers = baseCovers(bare, base.logic.호표?.length ?? 0, doc);
|
||||
const covered = Object.keys(covers).length > 0;
|
||||
const count = base.logic.호표?.length ?? 0;
|
||||
buildCalc(calcHost, {
|
||||
savedKey: base.logic.키,
|
||||
file: base.file,
|
||||
row: covered ? applyCovers(base.logic, covers) : base.logic,
|
||||
row: base.logic,
|
||||
prices: base.prices,
|
||||
dirty: () => covered, // 덮개 사본 = 저장 안 한 줄로 셈(메모리만)
|
||||
dirty: () => false,
|
||||
values,
|
||||
onText: (t) => {
|
||||
text = t;
|
||||
if (t.ok) lastText = t;
|
||||
draw();
|
||||
answer: async () => {
|
||||
if (okKey !== inputKey()) await recalc(); // 칸을 고치고 [계산] — SK 셈 한 번
|
||||
return shownOf(count);
|
||||
},
|
||||
onLines: () => void recalc(), // 시험 입력이 바뀜 — SK 도 같은 입력으로
|
||||
shown: shownOf(count),
|
||||
held: (i, it) => pickMat(bare?.정체[i])?.(it),
|
||||
});
|
||||
}
|
||||
|
||||
/** 시험 계산 칸에 그릴 것 = SK 셈 답(줄 · 기준 · 중간) */
|
||||
function shownOf(count: number): CalcShown | null {
|
||||
if (!ans) return fail ? { answer: { ok: false, reason: fail } } : null;
|
||||
const by = new Map(ans.줄.map((l) => [l.cid, l as unknown as CalcLine]));
|
||||
return {
|
||||
answer: {
|
||||
ok: true,
|
||||
lines: ans.줄 as unknown as CalcLine[], // 못 셈 줄 = 금액 「—」(계에 안 듦)
|
||||
sums: ans.기준 ?? undefined,
|
||||
middle: ans.중간 ?? {},
|
||||
},
|
||||
items: (bare?.정체 ?? []).slice(0, count).map((cid) => by.get(cid)),
|
||||
};
|
||||
}
|
||||
|
||||
/** `fresh` = 막 넣은 바탕 — 단위가 비거나 「식」이면 기초 기준단위(「100본」 · 바꾸면 서버가 환산 · 728-2) */
|
||||
async function loadBase(fresh = false): Promise<void> {
|
||||
const key = baseKey();
|
||||
@@ -504,8 +520,7 @@ export function mountSangeunDetail(
|
||||
touch(false);
|
||||
}
|
||||
draw();
|
||||
await recalc(); // 바탕 정체(bare) 먼저 — 시험 계산 · 텍스트 수식 = 덮개 사본
|
||||
if (alive) drawCalc();
|
||||
await recalc(); // 바탕 정체(bare) 먼저 — 시험 계산 칸 항목(재료 잡힌 품목)이 쓰임
|
||||
}
|
||||
|
||||
void loadBase();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CalcLine, TextAnswer } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { bornOfCid } from "./M02_Born_Tag";
|
||||
import type { SheetGroup } from "../../A00_Common/calc_sheet";
|
||||
import { forgetAdd, goneName, reviveAdd, setQty, resetQty } from "./M02_Sangeun_Undo";
|
||||
@@ -87,6 +88,8 @@ export interface SkLine {
|
||||
바탕값?: { qty?: number; 단가?: number } | null;
|
||||
/** 둘째 기초 세부 줄(744-1) — 그 기초 1 기준단위 몫(M01 원문 · 머리 수량 안 곱함) · 화면 품 칸 */
|
||||
원문?: { 수량: number | null; 금액: number | null; 비목?: Record<string, number | null> };
|
||||
/** 재료 줄이 잡은 품목(시험 계산 재료 칸 · 줄 모달) */
|
||||
품목?: CalcLine["품목"];
|
||||
}
|
||||
|
||||
export interface SkMat {
|
||||
@@ -114,6 +117,9 @@ export interface SkAnswer {
|
||||
/** 바탕 기준단위 → SK 단위 배(728-2) · 화면 줄을 나눈 배(없으면 1 · 문서 품 = 화면 품 × 화면배) */
|
||||
배?: number;
|
||||
화면배?: number;
|
||||
/** 보기 덧칸(750-1) — 텍스트 수식 글(줄마다 cid · 금액 = SK 줄) · 시험 계산 중간값 · 몸 `보기: false` 면 없음 */
|
||||
글?: TextAnswer;
|
||||
중간?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CoverRow {
|
||||
@@ -545,47 +551,6 @@ export function hoView(
|
||||
};
|
||||
}
|
||||
|
||||
/** 바탕 로직 호표 차례 고침(덮개 qty · 비목 — 배율 뺌) — 시험 계산 · 텍스트 수식 줄 사본(`applyCovers`)용 */
|
||||
export function baseCovers(
|
||||
bare: SkAnswer | null,
|
||||
count: number,
|
||||
doc: SkDoc,
|
||||
): Record<number, LineCover> {
|
||||
const out: Record<number, LineCover> = {};
|
||||
(bare?.정체 ?? []).slice(0, count).forEach((cid, i) => {
|
||||
const c = doc.덮개?.[cid];
|
||||
const one: LineCover = {};
|
||||
if (c?.qty != null) one.qty = c.qty;
|
||||
for (const k of PARTS) if (c?.[k] != null) one[k] = c[k];
|
||||
if (Object.keys(one).length) out[i] = one;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 텍스트 수식 답에서 뺀 바탕 줄 글 거름 — 공용 상세는 줄 글을 이름 차례로 붙임(같은 이름 더한줄이
|
||||
* 뺀 줄 글을 받지 않게) · 차례 = 바탕 이름(호표 → 덧줄)을 따라감(`Ho.attachPieces` 와 같은 길) */
|
||||
export function dropText<T extends { ok: boolean; groups?: { 줄: { 이름: string }[] }[] }>(
|
||||
text: T | null,
|
||||
names: string[],
|
||||
bare: SkAnswer | null,
|
||||
doc: SkDoc,
|
||||
): T | null {
|
||||
const drop = new Set(doc.뺀줄 ?? []);
|
||||
const gone = new Set((bare?.정체 ?? []).flatMap((cid, i) => (drop.has(cid) ? [i] : [])));
|
||||
if (!text?.ok || !text.groups || !gone.size) return text;
|
||||
const groups = text.groups.map((g) => {
|
||||
let from = 0;
|
||||
const 줄 = g.줄.filter((line) => {
|
||||
const at = names.findIndex((n, i) => i >= from && n === line.이름);
|
||||
if (at < 0) return true;
|
||||
from = at + 1;
|
||||
return !gone.has(at);
|
||||
});
|
||||
return { ...g, 줄 };
|
||||
});
|
||||
return { ...text, groups };
|
||||
}
|
||||
|
||||
/* ── 일위대가 줄 → SK 문서(717-3 · 723-3 줄 모드) ── */
|
||||
export interface LinePatch {
|
||||
덮개?: Record<string, SkCover>;
|
||||
|
||||
Reference in New Issue
Block a user