From d2ab8ee8ccd40144fbf83ff675e249b0bcec3af1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 2 Oct 2026 00:56:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(m02-df):=20=EB=B3=80=ED=99=98=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=ED=99=94=EB=A9=B4=20=ED=9D=A0=20=EB=84=B7=20?= =?UTF-8?q?=E2=80=94=20=EB=B3=80=EC=88=98=20=EC=98=88=EC=8B=9C=EA=B0=92=20?= =?UTF-8?q?=C2=B7=20=ED=91=9C=20=EC=B0=BE=EA=B8=B0=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EB=9C=BB=20=C2=B7=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=84=9C=EB=B2=84=20=EA=B0=92=20=C2=B7=20?= =?UTF-8?q?=ED=95=A9=EA=B3=84=20=EB=81=9D=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변환: 설계 값 변수 예시값 = 줄 시험입력 · 없으면 check_master.시험입력 기본(글 수 → 수) · totalRound = null(옛 계는 끝수 없음) - Model CfLogic.totalRound(없음 = 기본 계금 1원 미만 버림 · null = 끝수 없음) · Round totalRoundFor · roundBy/roundText 가 null 받음 · Summary · M01 흐름 보기 합계가 그것을 씀 - Compute: 함수 결과 오류는 오류 값(「#N/A」 등)으로 넘김(화면 Eval 과 같음 · 받는 칸으로 번짐 · 옛 엔진은 그 줄을 비움) — 서버가 빈 칸 0 으로 계속 세던 차이 없앰 - 화면 값 물음(Entry_Detail queriesOf): 인력 · 기계 · 유가 조건 찾기도 자리 물음 · 재료 검색어 틀은 채운 글 · 다른 로직 입력은 서버 /logic-value 로 받은 설계 값째 셈(그 안 조건 찾기 · 단가까지) · Eval 은 서버 출력이 있으면 그것 - Values.one_value: 인력 · 기계 · 유가 자리 물음 = group_find - 전수 대조 시험입력 1,414/1,472(96.1%) · 기본 입력 1,429(97.1%) — 남은 49 는 sub4 범위규칙 칸 · 검색어 찾기 · 재료 값 차이 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk --- .../M01_MasterData_UI_LogicLab_FlowRows.ts | 6 +- .../M01_MasterData_UI_LogicLab_FlowView.ts | 4 +- M02_MasterTemplete/DataFlow/M02_DF_Api.ts | 2 + M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 4 +- M02_MasterTemplete/DataFlow/M02_DF_Convert.py | 9 ++- .../DataFlow/M02_DF_Entry_Detail.ts | 67 +++++++++++++++++-- M02_MasterTemplete/DataFlow/M02_DF_Eval.ts | 5 ++ M02_MasterTemplete/DataFlow/M02_DF_Model.ts | 2 + M02_MasterTemplete/DataFlow/M02_DF_Round.ts | 10 ++- M02_MasterTemplete/DataFlow/M02_DF_Summary.ts | 7 +- M02_MasterTemplete/DataFlow/M02_DF_Values.py | 6 +- resources/tester/test_m02_df_convert.py | 16 ++++- resources/tester/test_m02_df_model.py | 40 ++++++++++- resources/tester/test_m02_df_router.py | 3 +- 14 files changed, 156 insertions(+), 25 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowRows.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowRows.ts index a9652b42b..fa792b2fa 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowRows.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowRows.ts @@ -70,7 +70,7 @@ const plain = (v: unknown): string => export function flowRows( lg: CfLogic, ev: CfEvalResult, - round: { item: CfRound; total: CfRound }, + round: { item: CfRound; total: CfRound | null }, ): FlowRows { const outs: CfBox[] = lg.boxes.filter((b) => b.kind === "output"); const groups = GROUP_ORDER.map((key): FlowGroup => { @@ -92,7 +92,7 @@ export function flowRows( key, lines, sub, - subText: sub === null ? "" : showRounded(sub, round.total), + subText: sub === null ? "" : showRounded(sub, round.total ?? undefined), }; }).filter((g) => g.lines.length); const total = roundBy(round.total, ev.cost.계); @@ -115,7 +115,7 @@ export function flowRows( return { groups, total, - totalText: showRounded(total, round.total), + totalText: showRounded(total, round.total ?? undefined), funcs, errors: ev.errors, }; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts index cdf1e7456..8cdae1f50 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts @@ -21,7 +21,7 @@ import { type FormulaLine, } from "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView"; import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; -import { itemRoundNow, totalRoundNow } from "../M02_MasterTemplete/DataFlow/M02_DF_Round"; +import { itemRoundNow, totalRoundFor } from "../M02_MasterTemplete/DataFlow/M02_DF_Round"; import { flowRows, type FlowFunc, @@ -158,7 +158,7 @@ export function buildFlowView( ); return; } - const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRoundNow() }); + const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRoundFor(lg) }); const detail = section(tf("Detail"), [ ...(rows.groups.length ? rows.groups.map(groupTable) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts index f1002f961..1d341911d 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts @@ -34,6 +34,8 @@ export interface CfQuery { }; value?: unknown; pick?: string; + /** 다른 로직 입력 상자 — 서버가 그 로직을 받은 설계 값으로 셈(/logic-value · 화면 queriesOf 가 만듦 · /values 로는 안 감) */ + logic?: { key: string; designs: Record }; } /** 입력 값 답 — 표는 `rows` · 그 밖은 `value` · 못 구하면 ok false + 쉬운 말 */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index 87b9ceeb9..01ab6dcfd 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -639,7 +639,9 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N at = _at(out.get("cell") or "A1")[2] hit = (calc.get(sheet.get("id")) or {}).get(at) value = ( - _plain(hit) + hit["오류"] # 오류 값을 넘김(Eval fromCalc 와 같음 · 받는 칸은 그 글/NA()) + if isinstance(hit, dict) and "오류" in hit + else _plain(hit) if hit is not None else ((sheet.get("칸") or {}).get(at) or {}).get("값") ) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Convert.py b/M02_MasterTemplete/DataFlow/M02_DF_Convert.py index 07742adaf..37153d371 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Convert.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Convert.py @@ -87,11 +87,17 @@ class _Flow: self.links.append(one | ({"rule": rule} if rule else {})) def design(self) -> None: + """설계 값 상자 — 예시값 = 줄의 시험입력 · 없으면 check_master.시험입력 기본.""" + given = store.cm.시험입력(self.row) + try: + given = mf._inputs(self.row, given) # 글로 적힌 수 → 수(옛 엔진과 같음) + except mf.FormulaError: + pass variables = [] for s in self.row.get("입력") or []: pick = s.get("고르기") one = {"name": s["이름"], "type": "choice" if pick else "number"} - one |= {"unit": str(s.get("단위") or "-"), "example": plain(pick[0]) if pick else None} + one |= {"unit": str(s.get("단위") or "-"), "example": plain(given.get(s["이름"]))} if pick: one["choices"] = plain(pick) if "범위" in s: @@ -329,6 +335,7 @@ def convert(row: dict, ctx: dict | None = None) -> dict: flow = {"key": row.get("키"), "name": row.get("이름") or ""} flow |= {"resultUnit": row.get("결과단위") or "", "boxes": list(f.boxes.values())} flow |= {"links": f.links, "userElements": []} + flow["totalRound"] = None # 옛 계는 끝수 없음(줄.끝수 묶음은 출력 상자 끝수로 실음) return {"flow": flow, "notes": list(dict.fromkeys(f.notes))} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts index 4cd321278..531146558 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts @@ -169,8 +169,24 @@ export async function unitChoices(): Promise { return units.length ? units : ["원/㎡"]; } +/** 재료 검색어 틀(「레미콘 {a}-{b}」)의 자리에 알약 값 — 빈 자리가 있으면 null(서버 Compute `_fill_template` 과 같음) */ +function fillTemplate( + rule: NonNullable, + pill: (p: CfPort) => unknown, +): unknown { + const got = new Map((rule.slots ?? []).map((x) => [x.name, pill(x.from)])); + if ([...got.values()].some((v) => v === null || v === undefined)) return null; + return (rule.template ?? "").replace(/\{([^{}]+)\}/g, (m, k: string) => + got.has(k) ? String(got.get(k)) : m, + ); +} + +/** 조건 찾기를 쓰는 그룹 — 재료 밖은 인력 · 기계 · 유가전력(field 키 · 서버 Values.GROUP_RULE) */ +const RULE_GROUPS = ["인력", "기계", "유가전력"]; + /** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 재료 자리(알약 = 자리 이름 · 조건 찾기는 이어진 알약 값을 - * pill 로 읽어 넘김) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ + * pill 로 읽어 넘김 · 인력 · 기계 · 유가 조건 찾기도) · 표(알약 「표」) · 다른 로직(서버가 받은 설계 값으로 셈 · port 「*」) + * · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ export function queriesOf( lg: CfLogic, pill: (p: CfPort) => unknown = () => null, @@ -178,10 +194,25 @@ export function queriesOf( ): { q: CfQuery; box: string; port: string }[] { return lg.boxes.flatMap((b) => { const inp = b.input; + if (b.kind === "input" && inp?.mode === "logic" && inp.logic?.key) { + const ref = inp.logic; + const designs: Record = {}; + for (const d of ref.designs) { + const l = lg.links.find((x) => x.to.box === b.id && x.to.name === d.name); + const v = l ? pill(l.from) : ref.examples?.[d.name]; + if (v !== null && v !== undefined) designs[d.name] = v; + } + const q: CfQuery = { id: `${b.id}*`, group: "기계", logic: { key: ref.key, designs } }; + return [{ q, box: b.id, port: "*" }]; + } if (b.kind !== "input" || !inp || inp.mode !== "group" || !inp.group) return []; const group = inp.group; const slots = inp.elements - .filter((e) => group === "재료" && e.mode && e.mode !== "fixed") + .filter( + (e) => + (group === "재료" && e.mode && e.mode !== "fixed") || + (RULE_GROUPS.includes(group) && e.mode === "rule"), + ) .map((e) => { const port = elementPort(e); const q: CfQuery = { @@ -190,7 +221,8 @@ export function queriesOf( unit: e.unit, slot: { ...e, field: e.rule?.field, op: e.rule?.op }, }; - if (e.mode === "rule" && e.rule) q.value = pill(e.rule.from); + if (e.mode === "rule" && e.rule) + q.value = e.rule.field === "검색어" ? fillTemplate(e.rule, pill) : pill(e.rule.from); return { q, box: b.id, port }; }); const els = inp.elements @@ -216,14 +248,35 @@ export async function fetchValues( pill?: (p: CfPort) => unknown, elementPort?: (e: CfElementRef) => string, ): Promise<{ values: CfValues; errors: string[] }> { - const asks = queriesOf(lg, pill, elementPort); - if (!asks.length) return { values: {}, errors: [] }; + const all = queriesOf(lg, pill, elementPort); + const asks = all.filter((a) => !a.q.logic); + const values: CfValues = {}; + const errors: string[] = []; + // 다른 로직 — 서버가 받은 설계 값으로 셈(그 안 조건 찾기 · 단가까지) → 출력 이름마다 값 + const logicAsks = all.filter((a) => a.q.logic); + const logicDone = logicAsks.length + ? import("./M02_DF_Api").then(({ cfLogicValue }) => + Promise.all( + logicAsks.map((a) => + cfLogicValue(a.q.logic!.key, a.q.logic!.designs).then( + (got) => { + values[a.box] = { ...(values[a.box] ?? {}), ...(got.outputs as CfValues[string]) }; + }, + (e: unknown) => errors.push(`${a.q.logic!.key} — ${String(e)}`), + ), + ), + ), + ) + : Promise.resolve(); + if (!asks.length) { + await logicDone; + return { values, errors }; + } const answers = await ask( asks.map((a) => a.q), lg.userElements as never, ); - const values: CfValues = {}; - const errors: string[] = []; + await logicDone; for (const a of asks) { const got = answers.find((x) => x.id === a.q.id); if (!got?.ok) errors.push(`${a.port}${got?.reason ? ` — ${got.reason}` : ""}`); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts index f3549a10c..c1cadd47a 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts @@ -413,6 +413,11 @@ function otherLogic( ): Record { const ref = box.input?.logic; const out: Record = {}; + // 서버가 받은 설계 값으로 셈한 출력(Entry_Detail 다른 로직 물음)이 있으면 그것 — 그 안 단가 · 조건 찾기까지 정본 + if (ref?.outputs.some((p) => given[p.name] !== undefined)) { + for (const p of ref.outputs) out[p.name] = given[p.name] ?? null; + return out; + } const other = ref ? opts.logics?.[ref.key] : undefined; if (!other || (opts.depth ?? 0) > 8) { if (ref && opts.logics && !other) errors.push(tl("NoLogic", { box: box.name })); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts index 8a012ef26..6939e9823 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts @@ -208,6 +208,8 @@ export interface CfLogic { boxes: CfBox[]; links: CfLink[]; userElements: CfUserElement[]; + /** 원가 합계 끝수 — 없으면 기본(일위대가표 계금 1원 미만 버림) · null = 끝수 없음(값 그대로 · 옛 로직 변환) */ + totalRound?: CfRound | null; } /** 알약 하나의 모습 */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts index 5101ad946..fb22d56b4 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts @@ -70,7 +70,8 @@ export async function defaultTotalRound(): Promise { } /** 끝수 적용 — 소수 자리 · 방법(부동소수 오차는 아주 작은 값으로 눌러 줌) */ -export function roundBy(r: CfRound, v: number): number { +export function roundBy(r: CfRound | null | undefined, v: number): number { + if (!r) return v; const k = 10 ** r.places; const x = v * k; const eps = Math.abs(x) * 1e-12 + 1e-9; @@ -84,7 +85,8 @@ export function roundBy(r: CfRound, v: number): number { } /** 「0.1원 미만 버림」 꼴 쉬운 말 */ -export function roundText(r: CfRound): string { +export function roundText(r: CfRound | null): string { + if (!r) return "끝수 없음(값 그대로)"; const step = r.places > 0 ? Number((10 ** -r.places).toFixed(r.places)) : 1; return `${step}원 미만 ${r.method}`; } @@ -107,3 +109,7 @@ export function showRounded(v: number, r?: CfRound): string { let totalNow: CfRound = { places: 0, method: "버림" }; void defaultTotalRound().then((r) => (totalNow = r)); export const totalRoundNow = (): CfRound => totalNow; + +/** 그 흐름의 합계 끝수 — 흐름에 정한 것(null = 끝수 없음) · 없으면 기본 */ +export const totalRoundFor = (lg: { totalRound?: CfRound | null }): CfRound | null => + lg.totalRound === undefined ? totalNow : lg.totalRound; diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts index a4686af39..e3ae369f3 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts @@ -69,18 +69,19 @@ export function renderSummary( ? ts("Total") : ts("Per", { per: /^\d/.test(perUnit) ? perUnit : `1${perUnit}` }); const { cost } = result; + const tr = lg.totalRound === undefined ? totalRound : lg.totalRound; // 흐름 합계 끝수(null = 없음) const big = el("p", { className: "m02-dfsum__big", children: [ el("span", { text: money ? perText : ts("Total") }), - el("strong", { text: `${formatMoney(roundBy(totalRound, cost.계))} ${ts("Won")}` }), + el("strong", { text: `${formatMoney(roundBy(tr, cost.계))} ${ts("Won")}` }), ], }); const costs = el("dl", { className: "m02-dfsum__costs", children: COSTS.flatMap((c) => [ el("dt", { text: ts(COST_KEY[c]) }), - el("dd", { text: formatMoney(roundBy(totalRound, cost[c])) }), + el("dd", { text: formatMoney(roundBy(tr, cost[c])) }), ]), }); @@ -161,7 +162,7 @@ export function renderSummary( : []), el("p", { className: "m02-dfsum__muted", - text: `${ts("RoundTotal")}: ${roundText(totalRound)}`, + text: `${ts("RoundTotal")}: ${roundText(tr)}`, }), verifyHost, ); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Values.py b/M02_MasterTemplete/DataFlow/M02_DF_Values.py index 0cc31950f..6c469cc23 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Values.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Values.py @@ -297,7 +297,11 @@ def one_value(item: dict, local: list[dict] | None = None) -> dict: if group == "기계" and isinstance(ref, str) and MACHINE_SEP in ref: ref, _, field = ref.partition(MACHINE_SEP) try: - if isinstance(item.get("slot"), dict): # 재료 자리(고정 · 선택 · 조건 찾기) + if ( + isinstance(item.get("slot"), dict) and group in GROUP_RULE + ): # 인력 · 기계 · 유가 조건 찾기 + got = group_find(str(group), item["slot"], item.get("value")) + elif isinstance(item.get("slot"), dict): # 재료 자리(고정 · 선택 · 조건 찾기) got = slot_price(item["slot"], item.get("value"), item.get("pick")) elif isinstance(ref, str) and _UKEY.match(ref): got = _user(ref, local or []) diff --git a/resources/tester/test_m02_df_convert.py b/resources/tester/test_m02_df_convert.py index 44b6c985f..35486a4af 100644 --- a/resources/tester/test_m02_df_convert.py +++ b/resources/tester/test_m02_df_convert.py @@ -125,7 +125,8 @@ def _keys() -> list[str]: REASONS = [ # (알림에 든 말, 까닭) — 앞이 먼저 ("표에 여럿 맞음", "표 범위 규칙 다름 — 여럿 맞음(이상·이하 · 이상·미만 경계)"), ("표에 없는 조건", "표 범위 규칙 다름 — 없는 조건(이상·이하 · 이상·미만 경계)"), - ("같음", "재료 검색어 자리 — 서버 조건 찾기(키 같음) ≠ 옛 검색어 찾기"), + ("검색어", "재료 검색어 틀 — 서버 검색어 찾기 대기(sub4)"), + ("같음", "조건 찾기 못 찾음(키 같음)"), ("맞는 재료 없음", "재료 빈 검색어 — 서버 재료 값 찾기 다름"), ("비어 있음", "단가 빈 품목 — 서버 처리 다름"), ("값 없음", "단가 빈 품목 — 서버 처리 다름"), @@ -270,3 +271,16 @@ def test_전수_대조_옛_셈_같음() -> None: r for r in rows if r["시험입력"]["state"] == "흐름 셈 멈춤" ][:3] assert report["시험입력"]["맞는 비율"] >= 0.5, report["시험입력"] + + +def test_변수_예시값_합계_끝수() -> None: + _copy() + got = _shape("GF000007", "GF000226") + for key, one in got.items(): + flow = one["flow"] + design = next(b for b in flow["boxes"] if b["id"] == "in-design") + assert all(v["example"] is not None for v in design["input"]["variables"]), key + assert flow["totalRound"] is None # 옛 계는 끝수 없음 + box = next(b for b in got["GF000226"]["flow"]["boxes"] if b["id"] == "in-design") + vars226 = {v["name"]: v["example"] for v in box["input"]["variables"]} + assert vars226["직경"] == 60 # 줄의 시험입력(글 「60.0」 → 수) diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index a3047d2dc..b3dbefa41 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -854,9 +854,8 @@ def test_표_찾기_빈_조건칸_무엇이든_빈_값줄_건너뜀_실패는_ monkeypatch.setattr(cp.dv, "one_value", lambda item, local=None: {"ok": True, "rows": BLOCK}) for v, (screen_out, screen_breaks) in zip(vals, got): srv = cp.run(lg, {"v": v["v"]}, lambda key: {}, []) - out = srv["outputs"]["o"] - want = None if isinstance(screen_out, str) and screen_out.startswith("#") else screen_out - assert (None if out is None else float(out)) == (None if want is None else float(want)) + out = srv["outputs"]["o"] # 오류 값은 화면과 같은 글(「#N/A」) + assert out == screen_out if isinstance(screen_out, str) else float(out) == float(screen_out) assert [b["kind"] for b in srv["breaks"]] == screen_breaks @@ -915,3 +914,38 @@ def test_표_찾기_범위_경계_규칙_칸() -> None: ) assert got == [[1, None], [2, None], [3, None], [3, None], [None, "표에 없는 조건"]] assert [list(cp._lookup(rule, block, {"x": x})) for x in xs] == got + + +def test_화면_값_물음_조건_찾기_인력기계유가_검색어_틀_다른_로직() -> None: + lg = { + "key": None, "name": "t", "resultUnit": "", "userElements": [], + "boxes": [ + {"id": "v", "kind": "input", "name": "설계", "input": {"mode": "variable", "elements": [], "variables": []}}, + {"id": "g", "kind": "input", "name": "기계", "input": {"mode": "group", "group": "기계", "variables": [], "elements": [ + {"ref": "EQ:{기계}#가격", "name": "가격 자리", "slot": "가격 자리", "spec": "", "unit": "-", "mode": "rule", + "rule": {"field": "키", "op": "같음", "from": {"box": "v", "name": "기계"}}}]}}, + {"id": "m", "kind": "input", "name": "재료", "input": {"mode": "group", "group": "재료", "variables": [], "elements": [ + {"ref": "{}", "name": "레미콘 자리", "slot": "레미콘 자리", "spec": "", "unit": "㎥", "mode": "rule", "scope": {"구분": "레미콘"}, + "rule": {"field": "검색어", "op": "같음", "from": {"box": "v", "name": "a"}, "template": "레미콘 {a}-{b}", + "slots": [{"name": "a", "from": {"box": "v", "name": "a"}}, {"name": "b", "from": {"box": "v", "name": "b"}}]}}]}}, + {"id": "o", "kind": "input", "name": "로직", "input": {"mode": "logic", "elements": [], "variables": [], "logic": { + "key": "GC000270", "outputs": [{"name": "노무비", "unit": "원"}], "designs": [{"name": "기계", "unit": "-"}, {"name": "지역", "unit": "-"}], + "examples": {"지역": "전국평균"}}}}, + ], + "links": [{"id": "l1", "from": {"box": "v", "name": "기계"}, "to": {"box": "o", "name": "기계"}}], + } # fmt: skip + pills = {"기계": "0201-0020", "a": 25, "b": "18"} + got = _run( + "cache.set(path.resolve(path.join(ROOT, 'M01_MasterData', 'M01_MasterData_UI_Logic_Api.ts')), { exports: { fetchLogics: async () => [] } });\n" + "const D = loadTs(path.join(ROOT, 'M02_MasterTemplete', 'DataFlow', 'M02_DF_Entry_Detail.ts'));\n" + f"const lg = {json.dumps(lg, ensure_ascii=False)};\nconst pills = {json.dumps(pills, ensure_ascii=False)};\n" + "const qs = D.queriesOf(lg, (p) => pills[p.name] ?? null, M.elementPort)" + ".map(a => [a.port, a.q.group, a.q.value ?? null, a.q.logic ?? null, !!a.q.slot]);\n" + "out([qs, E.evalLogic(lg, { o: { 노무비: 123 } }, { logics: {} }).ports.o]);" + ) + qs, server = got + assert ["가격 자리", "기계", "0201-0020", None, True] in qs + assert ["레미콘 자리", "재료", "레미콘 25-18", None, True] in qs + designs = {"기계": "0201-0020", "지역": "전국평균"} + assert ["*", "기계", None, {"key": "GC000270", "designs": designs}, False] in qs + assert server == {"노무비": 123} # 서버가 셈한 다른 로직 출력이 있으면 그것 diff --git a/resources/tester/test_m02_df_router.py b/resources/tester/test_m02_df_router.py index 96b8a613e..ea833470d 100644 --- a/resources/tester/test_m02_df_router.py +++ b/resources/tester/test_m02_df_router.py @@ -573,7 +573,8 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien assert any("맞는 재료 없음" in e for e in bad["errors"]) and any( "표에 없는 조건" in e for e in bad["errors"] ) - assert bad["ports"]["f"]["합"] is None and _d(bad["cost"]["재료비"]) == 0 + # 함수 결과 오류 = 오류 값(화면 Eval 과 같음 · 받는 칸으로 번짐) + assert bad["ports"]["f"]["합"] == "#N/A" and _d(bad["cost"]["재료비"]) == 0 def test_표준_표_읽기_쓰기_처음값(client: TestClient, folder: Path) -> None: