From c16d7fdda21d2ab79f6007b6282f7a8f3c5de06c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 11:57:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(m01-lb):=20=EB=A1=9C=EC=A7=81=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=ED=8C=90=20=EB=8B=A8=EA=B3=84=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20Wizard=20=EC=99=84=EC=84=B1=20=E2=80=94=20=ED=91=9C?= =?UTF-8?q?=20=EC=B9=B8=20=EC=97=AD=ED=95=A0=20=C2=B7=20=EB=AC=B8=EC=9E=A5?= =?UTF-8?q?=20=ED=8B=80=20=C2=B7=20=ED=95=AD=EB=AA=A9=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=C2=B7=20=EA=B0=92=20=EC=A2=85=EB=A5=98=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU --- M01_MasterData/LogicBuilder/M01_LB_Wizard.ts | 112 ++-------- .../LogicBuilder/M01_LB_Wizard_Build.ts | 21 +- .../LogicBuilder/M01_LB_Wizard_Calc.ts | 24 ++- .../LogicBuilder/M01_LB_Wizard_Parts.ts | 94 +++++++++ resources/tester/test_m01_lb_wizard_build.py | 193 ++++++++++++++++++ 5 files changed, 345 insertions(+), 99 deletions(-) create mode 100644 M01_MasterData/LogicBuilder/M01_LB_Wizard_Parts.ts create mode 100644 resources/tester/test_m01_lb_wizard_build.py diff --git a/M01_MasterData/LogicBuilder/M01_LB_Wizard.ts b/M01_MasterData/LogicBuilder/M01_LB_Wizard.ts index 0cfceeb6f..9f172557f 100644 --- a/M01_MasterData/LogicBuilder/M01_LB_Wizard.ts +++ b/M01_MasterData/LogicBuilder/M01_LB_Wizard.ts @@ -6,7 +6,6 @@ * 글 [한, 영] 짝은 아래 TEXT · 용어는 정함 9 · 이 폴더째 지워도 됨 * ========================================================================== */ -import { API_BASE_URL } from "@config/config_frontend"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, @@ -16,7 +15,7 @@ import { showToast, } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; -import { ApiError, type CalcAnswer } from "../M01_MasterData_UI_Logic_Api"; +import { type CalcAnswer } from "../M01_MasterData_UI_Logic_Api"; import { calcLater, plainError } from "../EasyCalc/M01_EasyCalc_Api"; import { readLogic, type LbLogic } from "./M01_LB_Blocks"; import { autoLinks, stepCalc, stepLink } from "./M01_LB_Wizard_Calc"; @@ -30,6 +29,13 @@ import { type WCtx, type WTable, } from "./M01_LB_Wizard_Build"; +import { + get, + tablePreview, + toTable, + type SectionItem, + type TableHead, +} from "./M01_LB_Wizard_Parts"; import "./M01_LB_Wizard.css"; const TEXT = { @@ -67,6 +73,7 @@ const TEXT = { Unused: ["쓰지 않음", "Unused"], Choices: ["선택 목록", "Choices"], Free: ["숫자 입력", "Number input"], + PickTable: ["표를 하나 골라 주세요", "Pick a table"], NeedResult: ["결과 칸이 하나는 있어야 함", "At least one result column"], FixedVal: ["고정값", "Fixed value"], BaseUnit: ["표 기준 「{b}」", "Table base “{b}”"], @@ -95,32 +102,7 @@ const t = (key: TKey, v: Record = {}): string => const fmt = (n: number): string => Number(n.toPrecision(6)).toLocaleString("ko-KR"); -async function get(path: string, params: Record): Promise { - const res = await fetch(`${API_BASE_URL}/m01${path}?${new URLSearchParams(params)}`, { - credentials: "include", - }); - const data = (await res.json().catch(() => ({}))) as { detail?: unknown } & T; - if (!res.ok) throw new ApiError(res.status, data.detail ?? res.statusText); - return data; -} - -interface SectionItem { - section: string; - title: string; - chapter: string; - tables: number; - logics: number; -} -interface TableHead { - file: string; - 키: string; - 이름: string; - 기준: string; - 조건: Record; - 값칸: Record; - count: number; -} - +const LBL = { preview: t("Preview"), notes: t("Notes") }; const BOOKS = ["산림품셈", "건설품셈"]; const UNITS = ["㎡", "㎥", "m", "km", "ha", "개소", "개", "ton", "본", "100㎡", "100m", "10m"]; @@ -233,7 +215,7 @@ export function openWizard(): Promise { const per = a.resultUnit.replace(/^원\//, ""); const v = money ? (ans.sums?.["계"] ?? Number(ans.result ?? 0)) : Number(ans.result); result.textContent = money - ? t("AmountPer", { p: per, m: fmt(v) }) + ? t("AmountPer", { p: per, m: Math.round(v).toLocaleString("ko-KR") }) : t("ValueIs", { c: col, v: fmt(v), u: a.table.값칸[col] ?? "" }); }); }; @@ -369,7 +351,7 @@ export function openWizard(): Promise { tables .querySelectorAll(".m01-lbw__row") .forEach((n, i) => n.classList.toggle("is-on", heads[i]?.키 === h.키)); - preview.replaceChildren(tablePreview(a.table)); + preview.replaceChildren(tablePreview(a.table, LBL)); renderNav(); }; let timer = 0; @@ -379,8 +361,8 @@ export function openWizard(): Promise { ); main.replaceChildren(bookBox.root, q.root, list, tables, preview); void load(); - if (a.table.key) preview.replaceChildren(tablePreview(a.table)); - return () => (a.table.key ? null : t("Tables")); + if (a.table.key) preview.replaceChildren(tablePreview(a.table, LBL)); + return () => (a.table.key ? null : t("PickTable")); }; /* ── 2단계 칸 역할 ── */ @@ -471,7 +453,7 @@ export function openWizard(): Promise { box.replaceChildren( el("p", { className: "m01-lbw__muted", text: t("RoleHint") }), ...chips, - tablePreview(a.table, a.roles), + tablePreview(a.table, LBL, a.roles), ); renderNav(); }; @@ -580,9 +562,12 @@ export function openWizard(): Promise { stepCalc(ctx, main); valid = () => null; } else if (step === 5) { - stepLink(ctx, main); - if (!linked) - ((linked = true), void autoLinks(ctx).then(() => step === 5 && stepLink(ctx, main))); + if (linked) stepLink(ctx, main); + else { + linked = true; + main.replaceChildren(el("p", { className: "m01-lbw__muted", text: t("Loading") })); + void autoLinks(ctx).then(() => step === 5 && stepLink(ctx, main)); + } valid = () => null; } else valid = stepName(); side.hidden = step === 1; @@ -641,58 +626,3 @@ export function openWizard(): Promise { draw(); }); } - -/** 서버 표 답 → WTable */ -function toTable(raw: WTable & { 키: string }, file: string): WTable { - return { - key: raw.키, - file, - 이름: raw.이름, - 기준: raw.기준 ?? "", - 출처: raw.출처 ?? "", - 조건: raw.조건, - 값칸: raw.값칸, - 범위규칙: raw.범위규칙 ?? null, - 줄: raw.줄, - 주: raw.주 ?? [], - }; -} - -const cellText = (v: unknown): string => - Array.isArray(v) - ? v.map((x) => x ?? "").join("~") - : v === null || v === undefined - ? "" - : String(v); - -/** 원문 표 앞 몇 줄 + 주 — 칸 머리 색 = 역할 */ -function tablePreview(tb: WTable, roles?: Record): HTMLElement { - const cols = [...Object.keys(tb.조건), ...Object.keys(tb.값칸)]; - const head = el("tr", { - children: cols.map((c) => - el("th", { className: `is-${roles?.[c] ?? (c in tb.조건 ? "조건" : "결과")}`, text: c }), - ), - }); - const rows = tb.줄 - .slice(0, 6) - .map((r) => el("tr", { children: cols.map((c) => el("td", { text: cellText(r[c]) })) })); - return el("div", { - className: "m01-lbw__table", - children: [ - el("h5", { text: `${t("Preview")} — ${tb.이름} (${tb.기준 || "—"})` }), - el("div", { - className: "m01-lbw__scroll", - children: [el("table", { children: [head, ...rows] })], - }), - ...(tb.줄.length > 6 - ? [el("p", { className: "m01-lbw__muted", text: `… ${tb.줄.length} 줄` })] - : []), - ...(tb.주.length - ? [ - el("h5", { text: t("Notes") }), - ...tb.주.map((n) => el("p", { className: "m01-lbw__note-line", text: n })), - ] - : []), - ], - }); -} diff --git a/M01_MasterData/LogicBuilder/M01_LB_Wizard_Build.ts b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Build.ts index 47c8c6364..e9abc8a54 100644 --- a/M01_MasterData/LogicBuilder/M01_LB_Wizard_Build.ts +++ b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Build.ts @@ -184,12 +184,25 @@ function inputsOf(a: WAnswers): LogicInput[] { if (m.k === "할증" && m.rate.t === "cond") add({ 이름: m.rate.input }); if (m.k === "나누기" && m.by.t === "input") add({ 이름: m.by.name }); } - for (const l of a.links) for (const n of l.needInputs ?? []) add(n); + if (a.kind === "금액") + for (const l of a.links) + if (l.kind !== "없음" && a.roles[l.col] === "결과") + for (const n of l.needInputs ?? []) add(n); return out; } const COST: Record = { 노무: "노무비", 재료: "재료비" }; +/** 사람 줄 노임 합 글 — 「(수량 * 노임 키 + …)」 · 사람 줄이 없으면 0 */ +const personSum = (ho: HoLine[]): string => { + const terms = ho + .filter((h) => h.종류 === "인력") + .map( + (h) => `${/^[\p{L}\p{N}_.]+$/u.test(h.수량) ? h.수량 : `(${h.수량})`} * ${String(h.요소)}`, + ); + return `(${terms.length ? terms.join(" + ") : "0"})`; +}; + /** 답 → 로직 줄 */ export function buildRow(a: WAnswers): LogicRow { const mids: { 이름: string; 식: string; 출처?: string }[] = []; @@ -222,7 +235,11 @@ export function buildRow(a: WAnswers): LogicRow { })); const 덧줄 = a.extras.map((e) => e.k === "비율" - ? { 이름: e.name, 식: `${e.base} * ${num(e.rate)} / 100`, 비목: e.cost } + ? { + 이름: e.name, + 식: `${e.base === "사람줄합" ? personSum(호표) : e.base} * ${num(e.rate)} / 100`, + 비목: e.cost, + } : { 이름: e.name, 식: num(e.amount), 비목: e.cost }, ); return { ...base, 입력: inputsOf(a), 중간: mids, 호표, 덧줄, 끝수: null }; diff --git a/M01_MasterData/LogicBuilder/M01_LB_Wizard_Calc.ts b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Calc.ts index 45cd91dc0..4410cad55 100644 --- a/M01_MasterData/LogicBuilder/M01_LB_Wizard_Calc.ts +++ b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Calc.ts @@ -5,7 +5,13 @@ * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; -import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; import { fetchPick } from "../M01_MasterData_Api_Fetch"; import { openEasyPick } from "../EasyCalc/M01_EasyCalc_Pick"; import { plainError } from "../EasyCalc/M01_EasyCalc_Api"; @@ -50,9 +56,11 @@ const TEXT = { DSurchargeNum: ["{p} % 를 더함", "add {p} %"], DSurchargeIn: ["「{n}」 % 를 더함(입력값)", "add “{n}” % (input)"], DSurchargeCond: [ - "「{i}」 가 {l} {o} 이면 0 · 넘으면 「{t}」 의 {c} % 를 더함", - "if “{i}” {o} {l} add 0, else “{t}” {c} %", + "「{i}」 가 {l} {o}이면 0 · 아니면 「{t}」 의 {c} % 를 더함", + "if “{i}” is {l} {o} add 0, else “{t}” {c} %", ], + OpLe: ["이하", "or less"], + OpLt: ["미만", "under"], DDivNum: ["{n} 로 나눔", "divide by {n}"], DDivIn: ["「{n}」 로 나눔(입력값)", "divide by “{n}” (input)"], DMin: ["{n} 보다 작은 값", "smaller of value and {n}"], @@ -127,7 +135,7 @@ const describe = (m: Mod): string => { : t("DSurchargeCond", { i: r.input, l: r.limit, - o: r.op === "<=" ? "≤" : "<", + o: t(r.op === "<=" ? "OpLe" : "OpLt"), t: r.table.이름, c: r.col, }); @@ -371,7 +379,7 @@ export function stepCalc(ctx: WCtx, host: HTMLElement): void { export async function autoLinks(ctx: WCtx): Promise { const { a } = ctx; for (const col of colsOf(a, "결과")) { - if (a.links.some((l) => l.col === col)) continue; + if (a.links.some((l) => l.col === col && l.kind !== "없음")) continue; const unit = a.table.값칸[col] ?? ""; const link: Link = { col, kind: "없음", name: col, spec: "", unit, element: null }; try { @@ -393,6 +401,7 @@ export async function autoLinks(ctx: WCtx): Promise { } catch { /* 못 찾으면 연결 안 함으로 둠 */ } + a.links = a.links.filter((l) => l.col !== col); a.links.push(link); } ctx.changed(); @@ -441,7 +450,10 @@ export function stepLink(ctx: WCtx, host: HTMLElement): void { attrs: { type: "button" }, text: t(label), }); - b.addEventListener("click", () => void pick(kind, pk).catch((e) => alert(plainError(e)))); + b.addEventListener( + "click", + () => void pick(kind, pk).catch((e) => showToast(plainError(e), "error")), + ); return b; }); const none = el("button", { diff --git a/M01_MasterData/LogicBuilder/M01_LB_Wizard_Parts.ts b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Parts.ts new file mode 100644 index 000000000..3f3773190 --- /dev/null +++ b/M01_MasterData/LogicBuilder/M01_LB_Wizard_Parts.ts @@ -0,0 +1,94 @@ +/* ============================================================================= + * M01_LB_Wizard_Parts.ts + * 단계 안내 곁부품 — 서버 읽기(절 · 표) · 표 → WTable · 원문 표 미리 보기 · `M01_LB_Wizard.ts` 가 부름 + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { el } from "@ui/ui_template_elements"; +import { ApiError } from "../M01_MasterData_UI_Logic_Api"; +import type { Role, WTable } from "./M01_LB_Wizard_Build"; + +export async function get(path: string, params: Record): Promise { + const res = await fetch(`${API_BASE_URL}/m01${path}?${new URLSearchParams(params)}`, { + credentials: "include", + }); + const data = (await res.json().catch(() => ({}))) as { detail?: unknown } & T; + if (!res.ok) throw new ApiError(res.status, data.detail ?? res.statusText); + return data; +} + +export interface SectionItem { + section: string; + title: string; + chapter: string; + tables: number; + logics: number; +} +export interface TableHead { + file: string; + 키: string; + 이름: string; + 기준: string; + 조건: Record; + 값칸: Record; + count: number; +} + +/** 서버 표 답 → WTable */ +export function toTable(raw: WTable & { 키: string }, file: string): WTable { + return { + key: raw.키, + file, + 이름: raw.이름, + 기준: raw.기준 ?? "", + 출처: raw.출처 ?? "", + 조건: raw.조건, + 값칸: raw.값칸, + 범위규칙: raw.범위규칙 ?? null, + 줄: raw.줄, + 주: raw.주 ?? [], + }; +} + +const cellText = (v: unknown): string => + Array.isArray(v) + ? v.map((x) => x ?? "").join("~") + : v === null || v === undefined + ? "" + : String(v); + +/** 원문 표 앞 몇 줄 + 주 — 칸 머리 색 = 역할 */ +export function tablePreview( + tb: WTable, + labels: { preview: string; notes: string }, + roles?: Record, +): HTMLElement { + const cols = [...Object.keys(tb.조건), ...Object.keys(tb.값칸)]; + const head = el("tr", { + children: cols.map((c) => + el("th", { className: `is-${roles?.[c] ?? (c in tb.조건 ? "조건" : "결과")}`, text: c }), + ), + }); + const rows = tb.줄 + .slice(0, 6) + .map((r) => el("tr", { children: cols.map((c) => el("td", { text: cellText(r[c]) })) })); + return el("div", { + className: "m01-lbw__table", + children: [ + el("h5", { text: `${labels.preview} — ${tb.이름} (${tb.기준 || "—"})` }), + el("div", { + className: "m01-lbw__scroll", + children: [el("table", { children: [head, ...rows] })], + }), + ...(tb.줄.length > 6 + ? [el("p", { className: "m01-lbw__muted", text: `… ${tb.줄.length} 줄` })] + : []), + ...(tb.주.length + ? [ + el("h5", { text: labels.notes }), + ...tb.주.map((n) => el("p", { className: "m01-lbw__note-line", text: n })), + ] + : []), + ], + }); +} diff --git a/resources/tester/test_m01_lb_wizard_build.py b/resources/tester/test_m01_lb_wizard_build.py new file mode 100644 index 000000000..210a11172 --- /dev/null +++ b/resources/tester/test_m01_lb_wizard_build.py @@ -0,0 +1,193 @@ +"""로직 만들기 판 단계 안내 — 답 → 로직 줄 짜기(`M01_LB_Wizard_Build.ts`). + +화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 순수 함수만 부름. +견본 표 · 답은 이 시험 안에 둠(진짜 마스터 내용에 기대지 않음). +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "M01_MasterData" / "LogicBuilder" / "M01_LB_Wizard_Build.ts" + +RUNNER = r""" +const fs = require("fs"); +const path = require("path"); +const [root, file] = process.argv.slice(1); +const ts = require(path.join(root, "config", "node_modules", "typescript")); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, () => ({})); +const m = box.exports; +const jobs = JSON.parse(fs.readFileSync(0, "utf8")); +const out = jobs.map((job) => { + if (job.op === "unit") return m.unitOfBase(job.base); + if (job.op === "row") return m.buildRow(job.a); + if (job.op === "trial") return m.trialOf(job.a, m.buildRow(job.a)); + return null; +}); +process.stdout.write(JSON.stringify(out)); +""" + +TABLE = { + "key": "QF900001", + "이름": "견본 쌓기", + "기준": "1㎡", + "출처": "산림품셈 9-9-9", + "조건": {"뒷길이": "수", "돌": "고르기"}, + "값칸": {"석공": "인", "보통인부": "인"}, + "줄": [ + {"뒷길이": 25, "돌": "깬돌", "석공": 0.15, "보통인부": 0.12}, + {"뒷길이": 30, "돌": "깬돌", "석공": 0.26, "보통인부": 0.21}, + ], + "주": ["③ 높이 3m 초과 시 할증"], +} +RATE_TABLE = { + "key": "QF900002", + "이름": "견본 증가율표", + "기준": "", + "출처": "산림품셈 9-9-9 주③", + "조건": {"높이": "범위"}, + "값칸": {"증가율": "%"}, + "줄": [{"높이": [3, 4], "증가율": 30}], + "주": [], +} + + +def answers(**more) -> dict: + base = { + "book": "산림품셈", + "section": "9-9-9", + "table": TABLE, + "roles": {"뒷길이": "조건", "돌": "조건", "석공": "결과", "보통인부": "결과"}, + "fixed": {}, + "options": {"뒷길이": [25, 30], "돌": ["깬돌"]}, + "resultUnit": "원/㎡", + "mods": [], + "links": [ + { + "col": "석공", + "kind": "노무", + "name": "석공", + "spec": "", + "unit": "인", + "element": "LB000033", + }, + { + "col": "보통인부", + "kind": "노무", + "name": "보통인부", + "spec": "", + "unit": "인", + "element": "LB000002", + }, + ], + "extras": [], + "name": "견본", + "kind": "금액", + "valueCol": "", + } + base.update(more) + return base + + +def run(jobs: list[dict]) -> list: + if shutil.which("node") is None: + pytest.skip("node 가 없음") + done = subprocess.run( + ["node", "-e", RUNNER, str(ROOT), str(SOURCE)], + input=json.dumps(jobs, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + return json.loads(done.stdout) + + +def test_결과단위는_표_기준에서(): + out = run([{"op": "unit", "base": b} for b in ("1㎡", "100㎡ 당", "㎡당", "m당", "")]) + assert out == ["원/㎡", "원/100㎡", "원/㎡", "원/m", "원/㎡"] + + +def test_기본_두_줄(): + row = run([{"op": "row", "a": answers()}])[0] + assert [i["이름"] for i in row["입력"]] == ["뒷길이", "돌"] + assert row["입력"][0]["고르기"] == [25, 30] + assert row["중간"] == [] + assert [h["수량"] for h in row["호표"]] == [ + "찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공", + "찾기(QF900001, 뒷길이=뒷길이, 돌=돌).보통인부", + ] + assert row["호표"][0]["종류"] == "인력" and row["호표"][0]["비목"] == "노무비" + assert row["결과단위"] == "원/㎡" and row["출처"] == "산림품셈 9-9-9" + + +def test_고정칸은_값으로(): + a = answers( + roles={"뒷길이": "고정", "돌": "조건", "석공": "결과", "보통인부": "쓰지 않음"}, + fixed={"뒷길이": 25}, + ) + row = run([{"op": "row", "a": a}])[0] + assert [i["이름"] for i in row["입력"]] == ["돌"] + assert [h["수량"] for h in row["호표"]] == ["찾기(QF900001, 돌=돌, 뒷길이=25).석공"] + + +def test_문장틀_차례대로_씌움(): + cond = { + "t": "cond", + "input": "높이", + "op": "<=", + "limit": 3, + "table": RATE_TABLE, + "col": "증가율", + } + a = answers( + mods=[ + {"k": "할증", "rate": cond, "src": "산림품셈 9-9-9 주③"}, + {"k": "나누기", "by": {"t": "input", "name": "시공량"}}, + {"k": "올림", "fn": "반올림", "places": 3}, + ] + ) + row = run([{"op": "row", "a": a}])[0] + assert row["중간"] == [ + { + "이름": "증가율", + "식": "만약(높이 <= 3, 0, 찾기(QF900002, 높이=높이).증가율)", + "출처": "산림품셈 9-9-9 주③", + } + ] + assert row["호표"][0]["수량"] == ( + "반올림((찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공 * (1 + 증가율 / 100)) / 시공량, 3)" + ) + assert [i["이름"] for i in row["입력"]] == ["뒷길이", "돌", "높이", "시공량"] + + +def test_비율_덧줄은_사람줄합(): + extras = [{"k": "비율", "name": "공구손료", "base": "사람줄합", "rate": 3, "cost": "재료비"}] + row = run([{"op": "row", "a": answers(extras=extras)}])[0] + assert row["덧줄"] == [ + { + "이름": "공구손료", + "식": "((찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공) * LB000033 + (찾기(QF900001, 뒷길이=뒷길이, 돌=돌).보통인부) * LB000002) * 3 / 100", + "비목": "재료비", + } + ] + + +def test_값_종류는_결과_식_하나(): + a = answers(kind="값", valueCol="석공", resultUnit="인/㎡") + row = run([{"op": "row", "a": a}])[0] + assert row["결과"] == {"식": "찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공"} + assert "호표" not in row and row["결과단위"] == "인/㎡" + + +def test_예시값은_첫_줄에_걸림(): + out = run([{"op": "trial", "a": answers()}])[0] + assert out == {"뒷길이": 25, "돌": "깬돌"}