diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts index ae4bf7c60..0c524a5a8 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -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; - 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)); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts index 9e2c32258..3e7f83396 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts @@ -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(); + 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); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 500a02934..cbfee37de 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -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"], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index edaa85033..0176da2d4 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -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 }; diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index 6bfa49452..9fc8c3cdb 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -52,6 +52,12 @@ async function keyName(key: string): Promise { 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; - onLines: (lines: CalcLine[] | null) => void; + onLines?: (lines: CalcLine[] | null) => void; /** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */ onText?: (answer: TextAnswer) => void; + /** (M02 750-2) 있으면 [계산] = 이 셈(SK 셈 답 하나) · `/calc` · `/text` · 미리보기 안 부름 */ + answer?: () => Promise; + /** (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 => { 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 = {}; 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` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */ diff --git a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts index c7388dfa2..e110a3705 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts @@ -76,6 +76,8 @@ export function materialRows( prices: Record, values: Record, 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, - lines: CalcLine[], + lines: (CalcLine | undefined)[], ): void { const mine = picksOf(values); host.querySelectorAll("[data-mat-idx]").forEach((span) => { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Cover.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Cover.ts index 7ce45416f..e30746d87 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Cover.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Cover.ts @@ -28,11 +28,12 @@ export function calcSangeun( scope: SkScope, doc: Record, given: Record = {}, + seen = true, ): Promise { const 입력값 = Object.fromEntries(Object.entries(given).filter(([, v]) => String(v).trim())); return call(`/layers/${scope.layer}/sangeun/calc${q(scope)}`, { method: "POST", - body: JSON.stringify({ 문서: doc, 입력값 }), + body: JSON.stringify({ 문서: doc, 입력값, 보기: seen }), // 보기 false = 글 · 중간 덧칸 안 지음(바탕 원값 셈) }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index 1af48839b..c9cb1e618 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -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(); @@ -204,21 +202,26 @@ export function mountSangeunDetail( else drawHead(); }; + const inputKey = (): string => JSON.stringify([doc, values]); + async function recalc(): Promise { 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: 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("[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("[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 { const key = baseKey(); @@ -504,8 +520,7 @@ export function mountSangeunDetail( touch(false); } draw(); - await recalc(); // 바탕 정체(bare) 먼저 — 시험 계산 · 텍스트 수식 = 덮개 사본 - if (alive) drawCalc(); + await recalc(); // 바탕 정체(bare) 먼저 — 시험 계산 칸 항목(재료 잡힌 품목)이 쓰임 } void loadBase(); diff --git a/M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts b/M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts index 11a0c5887..1f8130c45 100644 --- a/M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts +++ b/M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts @@ -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 }; + /** 재료 줄이 잡은 품목(시험 계산 재료 칸 · 줄 모달) */ + 품목?: CalcLine["품목"]; } export interface SkMat { @@ -114,6 +117,9 @@ export interface SkAnswer { /** 바탕 기준단위 → SK 단위 배(728-2) · 화면 줄을 나눈 배(없으면 1 · 문서 품 = 화면 품 × 화면배) */ 배?: number; 화면배?: number; + /** 보기 덧칸(750-1) — 텍스트 수식 글(줄마다 cid · 금액 = SK 줄) · 시험 계산 중간값 · 몸 `보기: false` 면 없음 */ + 글?: TextAnswer; + 중간?: Record; } export interface CoverRow { @@ -545,47 +551,6 @@ export function hoView( }; } -/** 바탕 로직 호표 차례 고침(덮개 qty · 비목 — 배율 뺌) — 시험 계산 · 텍스트 수식 줄 사본(`applyCovers`)용 */ -export function baseCovers( - bare: SkAnswer | null, - count: number, - doc: SkDoc, -): Record { - const out: Record = {}; - (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( - 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; diff --git a/resources/tester/test_717_2b_sangeun_detail.py b/resources/tester/test_717_2b_sangeun_detail.py index 23cb21baf..dacadac94 100644 --- a/resources/tester/test_717_2b_sangeun_detail.py +++ b/resources/tester/test_717_2b_sangeun_detail.py @@ -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_상세_부품_길_부품상자(): diff --git a/resources/tester/test_725_1_sangeun_add_inputs.py b/resources/tester/test_725_1_sangeun_add_inputs.py index f442d2767..4625f0ece 100644 --- a/resources/tester/test_725_1_sangeun_add_inputs.py +++ b/resources/tester/test_725_1_sangeun_add_inputs.py @@ -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 diff --git a/resources/tester/test_750_2_views_one_answer.py b/resources/tester/test_750_2_views_one_answer.py new file mode 100644 index 000000000..ca3836567 --- /dev/null +++ b/resources/tester/test_750_2_views_one_answer.py @@ -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 옛 길 그대로