feat(m01-lb): 로직 만들기 판 단계 안내 Wizard 완성 — 표 칸 역할 · 문장 틀 · 항목 연결 · 값 종류 · 시험
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
@@ -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, string | number> = {}): string =>
|
||||
|
||||
const fmt = (n: number): string => Number(n.toPrecision(6)).toLocaleString("ko-KR");
|
||||
|
||||
async function get<T>(path: string, params: Record<string, string>): Promise<T> {
|
||||
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<string, string>;
|
||||
값칸: Record<string, string>;
|
||||
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<LbLogic | null> {
|
||||
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<LbLogic | null> {
|
||||
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<LbLogic | null> {
|
||||
);
|
||||
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<LbLogic | null> {
|
||||
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<LbLogic | null> {
|
||||
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<LbLogic | null> {
|
||||
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<string, Role>): 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 })),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<string, string> = { 노무: "노무비", 재료: "재료비" };
|
||||
|
||||
/** 사람 줄 노임 합 글 — 「(수량 * 노임 키 + …)」 · 사람 줄이 없으면 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 };
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
} 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", {
|
||||
|
||||
@@ -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<T>(path: string, params: Record<string, string>): Promise<T> {
|
||||
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<string, string>;
|
||||
값칸: Record<string, string>;
|
||||
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<string, Role>,
|
||||
): 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 })),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -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, "돌": "깬돌"}
|
||||
Reference in New Issue
Block a user