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:
2026-11-02 23:01:57 +09:00
co-authored by Claude Sonnet 5.5
parent d87c7b14fd
commit db590e6fa8
12 changed files with 155 additions and 93 deletions
@@ -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 };
+33 -6
View File
@@ -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 옛 길 그대로