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) => {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -117,9 +117,6 @@ const v = hoView(ans2, bare, base, doc2);
|
||||
out.view = [v.호표.map((x) => [x.종류, x.요소, x.수량, x.비목 ?? null]), v.덧줄, v.cids,
|
||||
v.lines.map((l) => l?.cid ?? null), v.covers];
|
||||
out.viewRef = hoView(ans2, bare, null, doc2).호표[0];
|
||||
out.baseCovers = baseCovers(bare, 3, doc2);
|
||||
const text = { ok: true, groups: [{ 비목: "노무비", 줄: [{ 이름: "석공" }, { 이름: "굴착기" }] }] };
|
||||
out.cut = dropText(text, ["석공", "굴착기", "모르타르"], bare, doc2).groups[0].줄.map((x) => x.이름);
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
)
|
||||
@@ -164,8 +161,6 @@ console.log(JSON.stringify(out));
|
||||
assert got["viewRef"] == { # 참조 바탕 = 셈 줄로 지음
|
||||
"종류": "인력", "요소": "", "이름": "석공", "단위": "인", "수량": "0.3", "비목": "노무비",
|
||||
} # fmt: skip
|
||||
assert got["baseCovers"] == {"0": {"qty": 0.3}}
|
||||
assert got["cut"] == ["석공"] # 뺀 바탕 줄 글 거름(같은 이름 더한줄이 안 받음)
|
||||
|
||||
|
||||
def test_상세_부품_길_부품상자():
|
||||
|
||||
@@ -110,6 +110,6 @@ def test_no_base_common_option_seoul(world): # noqa: F811
|
||||
def test_screen_no_base_inputs_and_gap_dash():
|
||||
"""725-6 화면 — 바탕 없음 = 더한줄 로직 입력 모음 칸 · 못 셈 줄 금액 「—」 + 까닭 풍선."""
|
||||
ts = (ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Sangeun_Detail.ts").read_text("utf-8")
|
||||
assert "입력: extra" in ts and "syncExtra" in ts and "if (!base) void recalc()" in ts
|
||||
assert "입력: extra" in ts and "syncExtra" in ts and "void recalc(); // 750-2" in ts
|
||||
ho = (ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Ho.ts").read_text("utf-8")
|
||||
assert "e.line?.까닭 ? gapCell" in ho and "title: why" in ho
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""750-2 화면 — 네 보기가 SK 셈 답 하나로(바탕 M01 로직 따로 셈 길 걷음) · 재료 고름 = 문서 `고름` ·
|
||||
읽는 식 줄 = cid 로 이음 · 옵션 바꿈 = SK 셈 한 번(원문 대조는 서버 test_750_1)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _ts(rel: str) -> str:
|
||||
return (ROOT / rel).read_text("utf-8")
|
||||
|
||||
|
||||
def test_detail_uses_only_sk_answer():
|
||||
ts = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
assert "text = got.글" in ts and "answer: async" in ts and "shown: shownOf" in ts
|
||||
assert "onText" not in ts and "onLines" not in ts # 바탕 `/calc` · `/text` 부름 걷음
|
||||
assert "dropText" not in ts and "baseCovers" not in ts and "applyCovers" not in ts
|
||||
assert "calcSangeun(scope, plain, values, false)" in ts # 바탕 원값 셈 = 보기 덧칸 없이
|
||||
assert "doc.고름 = " in ts and "pickMat" in ts # 재료 고름 = SK 문서 고름
|
||||
|
||||
|
||||
def test_text_lines_joined_by_cid():
|
||||
ho = _ts("M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts")
|
||||
assert "ctx.cids && line.cid" in ho and "pickMat" in ho
|
||||
api = _ts("M01_MasterData/M01_MasterData_UI_Logic_Api.ts")
|
||||
assert "cid?: string" in api and "나눔?: number" in api
|
||||
|
||||
|
||||
def test_calc_view_answer_hook_skips_logic_lib():
|
||||
ts = _ts("M01_MasterData/M01_MasterData_UI_Logic_Calc.ts")
|
||||
assert "ctx.onText && !ctx.answer" in ts # 미리보기 안 부름
|
||||
assert "if (ctx.answer)" in ts and "runCalc" in ts # M01 옛 길 그대로
|
||||
Reference in New Issue
Block a user