From 2e99091673df1888054171eeeccea79979d92d14 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:27:04 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(m02-df):=2036-0b=20=EC=84=A0=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=97=AC=EB=9F=AC=20=EC=A4=84(=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20=C2=B7=20=EB=98=90=EB=8A=94)=20=C2=B7=20?= =?UTF-8?q?=EA=B0=92=20=EB=AA=A9=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CfRule gate conds[{from · op · value}] · join 그리고/또는 · 옛 한 줄 꼴(from · op · value)도 읽음 — 옛 칸은 첫 조건과 같게 둠(makeGate) - gateConds · makeGate · valueChoices(변수 고르기 · 재료 규격 · 표 칸 값 · 수 · 함수 결과는 null) - 규칙 → 받는 칸 엑셀 식 IF(AND/OR(...),A1,NA()) · 선 이름표 「지반 = 토사 그리고 높이 ≥ 3 일 때만」 · 겹침 · 안 맞음은 조건 알약 묶음끼리 - 조건 값 열쇠 「상자|알약」 먼저(이름 겹침 막음) · M02_DF_Compute.py gate 같은 뜻(서버 = 화면 시험) - 시험 test_m02_df_model 30개 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk --- M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 41 +++++-- M02_MasterTemplete/DataFlow/M02_DF_Eval.ts | 22 +++- M02_MasterTemplete/DataFlow/M02_DF_Model.ts | 77 ++++++++++++- M02_MasterTemplete/DataFlow/M02_DF_Rule.ts | 31 ++++-- resources/tester/test_m02_df_model.py | 104 ++++++++++++++++++ 5 files changed, 249 insertions(+), 26 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index 3abcdc73b..331a2a151 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -323,7 +323,7 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]: return None, "오류" tests = [] for c in rule.get("conds") or []: - col, want = str(c.get("col")), conds.get((c.get("from") or {}).get("name")) + col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {}) if col in head: i = head.index(col) tests.append(lambda row, i=i, want=want: _same(row[i], want)) @@ -362,12 +362,13 @@ def apply_rule(rule: dict, value: Any, conds: dict) -> tuple[Any, str | None]: if kind == "lookup": return _lookup(rule, value, conds) if kind == "gate": - src = (rule.get("from") or {}).get("name") - return ( - (value, None) - if _compare(conds.get(src), rule.get("op"), rule.get("value")) - else (None, "통과 안 됨") - ) + rows, join = _gate_conds(rule) + hits = [ + _compare(_cond_of(conds, c.get("from") or {}), c.get("op"), c.get("value")) + for c in rows + ] + ok = any(hits) if join == "또는" else all(hits) + return (value, None) if ok else (None, "통과 안 됨") n = _num_of(value) if n is None: return (None, None) if value is None else (None, "오류") @@ -387,7 +388,23 @@ def _sources(rule: dict | None) -> list[dict]: return [] if rule.get("kind") == "lookup": return [c.get("from") or {} for c in rule.get("conds") or []] - return [rule.get("from") or {}] if rule.get("kind") == "gate" else [] + if rule.get("kind") == "gate": + return [c.get("from") or {} for c in _gate_conds(rule)[0]] + return [] + + +def _gate_conds(rule: dict) -> tuple[list[dict], str]: + """조건 통과 줄 · 잇는 말 — conds · join · 옛 한 줄 꼴은 줄 하나(Model gateConds).""" + rows = rule.get("conds") or [ + {"from": rule.get("from") or {}, "op": rule.get("op"), "value": rule.get("value")} + ] + return rows, rule.get("join") or "그리고" + + +def _cond_of(conds: dict, port: dict) -> Any: + """조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름(Rule condOf).""" + key = f"{port.get('box')}|{port.get('name')}" + return conds[key] if key in conds else conds.get(port.get("name")) # ── 계산(`M02_DF_Eval.evalLogic` 와 같은 차례) ───────────────────────────── @@ -462,9 +479,11 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N return v if v is None and rule.get("kind") in ("scale", "round"): return None - conds = { - p.get("name"): ports.get(p.get("box"), {}).get(p.get("name")) for p in _sources(rule) - } + conds = {} + for p in _sources(rule): + got_v = ports.get(p.get("box"), {}).get(p.get("name")) + conds[p.get("name")] = got_v + conds[f"{p.get('box')}|{p.get('name')}"] = got_v got, why = apply_rule(rule, v, conds) if why: blocked.add(ln["to"].get("box")) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts index a35d22cf9..038c1e9a7 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts @@ -16,10 +16,12 @@ import { depends, designInputs, elementPort, + gateConds, portsIn, portsOut, ruleSources, type CfBox, + type CfGateRule, type CfLink, type CfLogic, } from "./M02_DF_Model"; @@ -266,7 +268,10 @@ export function evalLogic( if (!l.rule) return v; if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null; const conds = Object.fromEntries( - ruleSources(l.rule).map((p) => [p.name, valueOf(p.box, p.name)]), + ruleSources(l.rule).flatMap((p) => [ + [p.name, valueOf(p.box, p.name)], + [`${p.box}|${p.name}`, valueOf(p.box, p.name)], + ]), ); const res = applyRule(l.rule, v, conds); if (l.rule.kind === "gate") passed.set(l.id, res.why === null); @@ -411,21 +416,26 @@ function gates( const groups = new Map(); for (const l of lg.links) if (l.rule?.kind === "gate") { - const k = `${l.rule.from.box}|${l.rule.from.name}|${l.from.box}|${l.from.name}`; + // 같은 알약에서 나가 같은 조건 알약들로 가르는 선끼리 — 여러 조건이면 조건 알약 묶음이 같을 때 + const srcs = ruleSources(l.rule) + .map((p) => `${p.box}|${p.name}`) + .sort() + .join("+"); + const k = `${srcs}|${l.from.box}|${l.from.name}`; groups.set(k, [...(groups.get(k) ?? []), l]); } for (const list of groups.values()) { if (list.length < 2) continue; const n = list.filter((l) => passed.get(l.id)).length; if (n === 1) continue; - const g = list[0].rule as Extract; - const fill = { box: name(g.from.box), pill: g.from.name }; + const g = gateConds(list[0].rule as CfGateRule).conds[0].from; + const fill = { box: name(g.box), pill: g.name }; const text = tl(n > 1 ? "Overlap" : "NoneMatch", fill); found.push({ kind: n > 1 ? "조건 겹침" : "조건 안 맞음", level: "문제", - box: g.from.box, - port: g.from.name, + box: g.box, + port: g.name, text, }); errors.push(text); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts index f8ee64e95..bee7c9010 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts @@ -126,9 +126,27 @@ export type CfGateOp = "=" | "≠" | "≥" | "≤" | ">" | "<"; /** 선 규칙(선 하나에 하나 · 없으면 그대로) — 받는 칸의 엑셀 수식으로 바뀌어 엔진 한 벌로 셈 * lookup = 보낸 표에서 조건 칸마다 맞출 알약 · 꺼낼 칸(범위 칸은 「이름·아래」 「이름·위」) * gate = 알약 값이 조건을 통과할 때만 보냄 · scale = × ÷ +% -% · round = 끝수 */ +/** 조건 통과의 조건 한 줄 */ +export interface CfGateCond { + from: CfPort; + op: CfGateOp; + value: number | string; +} + +/** 조건 통과 — conds 여러 줄을 join(그리고 · 또는)으로 · 옛 한 줄 꼴(from · op · value)도 읽음 + * from · op · value 는 첫 조건과 같게 둠(옛 화면 코드가 그대로 돌게 · makeGate 가 맞춰 줌) — conds 가 있으면 conds 가 이김 */ +export type CfGateRule = { + kind: "gate"; + conds?: CfGateCond[]; + join?: "그리고" | "또는"; + from: CfPort; + op: CfGateOp; + value: number | string; +}; + export type CfRule = | { kind: "lookup"; conds: { col: string; from: CfPort }[]; col: string } - | { kind: "gate"; from: CfPort; op: CfGateOp; value: number | string } + | CfGateRule | { kind: "scale"; op: "×" | "÷" | "+%" | "-%"; value: number } | ({ kind: "round" } & CfRound); @@ -357,10 +375,65 @@ export function portDir(box: CfBox, name: string): "in" | "out" { export function ruleSources(rule: CfRule | undefined): CfPort[] { if (!rule) return []; if (rule.kind === "lookup") return rule.conds.map((c) => c.from); - if (rule.kind === "gate") return [rule.from]; + if (rule.kind === "gate") return gateConds(rule).conds.map((c) => c.from); return []; } +/** 조건 통과의 조건 줄 · 잇는 말 — 옛 한 줄 꼴은 조건 하나 */ +export function gateConds(rule: CfGateRule): { conds: CfGateCond[]; join: "그리고" | "또는" } { + const conds = rule.conds?.length + ? rule.conds + : [{ from: rule.from, op: rule.op, value: rule.value }]; + return { conds, join: rule.join ?? "그리고" }; +} + +/** 조건 통과 만들기 — 옛 칸(from · op · value)은 첫 조건과 같게 */ +export function makeGate(conds: CfGateCond[], join: "그리고" | "또는" = "그리고"): CfGateRule { + const first = conds[0] ?? { from: { box: "", name: "" }, op: "=" as const, value: "" }; + return { + kind: "gate", + conds: clone(conds), + join, + from: { ...first.from }, + op: first.op, + value: first.value, + }; +} + +/** 알약이 가질 수 있는 값 목록 — 조건 값 고르기 · null = 숫자 칸(범위 검사) + * 입력 변수 고르기 = choices · 재료 알약 = 그 요소들의 규격 · 표 알약 = opts.col 칸 값들(opts.values 의 표 덩어리) + * · 함수 결과 · 다른 로직 출력 · 수 변수 = null */ +export function valueChoices( + lg: CfLogic, + port: CfPort, + opts: { col?: string; values?: Record> } = {}, +): (string | number)[] | null { + const box = boxOf(lg, port.box); + const inp = box?.input; + if (!box || box.kind !== "input" || !inp || inp.mode === "logic") return null; + const v = inp.variables.find((x) => x.name === port.name); + if (v) return v.type === "choice" && v.choices?.length ? [...v.choices] : null; + if (port.name === TABLE_PORT && inp.table) { + const block = opts.values?.[box.id]?.[TABLE_PORT]; + if (!opts.col || !Array.isArray(block) || !Array.isArray(block[0])) return null; + const head = (block[0] as unknown[]).map(String); + const at = head.indexOf(opts.col); + if (at < 0) return null; + const seen: (string | number)[] = []; + for (const row of block.slice(1) as unknown[][]) { + const x = row[at]; + if ((typeof x === "number" || typeof x === "string") && x !== "" && !seen.includes(x)) + seen.push(x); + } + return seen.length ? seen : null; + } + const specs = inp.elements + .filter((e) => elementPort(e) === port.name && e.spec.trim()) + .map((e) => e.spec.trim()); + const uniq = [...new Set(specs)]; + return uniq.length ? uniq : null; +} + /** 상자 사이 기댐 — 선 + 선 규칙의 조건 알약 + 재료 조건 찾기의 알약(→ 그 상자) */ export function depends(lg: CfLogic): { from: string; to: string }[] { const out: { from: string; to: string }[] = []; diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts b/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts index adf25c2fa..3e2233023 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts @@ -12,7 +12,7 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { colName } from "../../A00_Common/spreadsheet/spreadsheet_address"; import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc"; import type { CalcValue, Cell } from "../../A00_Common/spreadsheet/spreadsheet_types"; -import type { CfGateOp, CfRound, CfRule } from "./M02_DF_Model"; +import { gateConds, type CfGateOp, type CfRound, type CfRule } from "./M02_DF_Model"; type Scalar = number | string | boolean | null; @@ -27,6 +27,8 @@ export interface CfRuleResult { const TEXT = { Lookup: ["표에서 찾기", "Look up in table"], Gate: ["일 때만", "only when"], + And: ["그리고", "and"], + Or: ["또는", "or"], Places: ["소수 {n}자리", "{n} decimals"], Round: ["반올림", "round"], Up: ["올림", "round up"], @@ -57,8 +59,11 @@ export function ruleLabel(rule: CfRule): string { const conds = rule.conds.map((c) => `${c.col} = ${c.from.name}`).join(" · "); return `${tl("Lookup")}${conds ? ` (${conds})` : ""} → ${rule.col}`; } - case "gate": - return `${rule.from.name} ${rule.op} ${rule.value} ${tl("Gate")}`; + case "gate": { + const g = gateConds(rule); + const join = g.join === "또는" ? tl("Or") : tl("And"); + return `${g.conds.map((c) => `${c.from.name} ${c.op} ${c.value}`).join(` ${join} `)} ${tl("Gate")}`; + } case "scale": return rule.op === "+%" || rule.op === "-%" ? `${rule.op[0]}${rule.value}%` @@ -109,6 +114,10 @@ const lit = (v: number | string): string => /* ── 규칙 ───────────────────────────────────────────────────────────────── */ +/** 조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름 */ +const condOf = (conds: Record, p: { box: string; name: string }): unknown => + `${p.box}|${p.name}` in conds ? conds[`${p.box}|${p.name}`] : conds[p.name]; + /** 규칙 적용 — value = 보낸 알약 값(표면 머리 줄 포함 칸 덩어리) · conds = 조건 알약 이름 → 값(lookup · gate) */ export function applyRule( rule: CfRule, @@ -121,9 +130,17 @@ export function applyRule( if (a1) 칸.A1 = a1; let formula: string; if (rule.kind === "gate") { - const c1 = cellOf(conds[rule.from.name]); - if (c1) 칸.C1 = c1; - formula = `IF(C1${OP[rule.op]}${lit(rule.value)},A1,NA())`; + // 조건 값 = C1 · D1 · E1 … · 여러 줄은 AND / OR + const g = gateConds(rule); + const tests = g.conds.map((c, i) => { + const at = `${colName(2 + i)}1`; + const cv = cellOf(condOf(conds, c.from)); + if (cv) 칸[at] = cv; + return `${at}${OP[c.op]}${lit(c.value)}`; + }); + const test = + tests.length > 1 ? `${g.join === "또는" ? "OR" : "AND"}(${tests.join(",")})` : tests[0]; + formula = `IF(${test},A1,NA())`; } else if (rule.kind === "scale") { const v = rule.value; formula = @@ -169,7 +186,7 @@ function lookup( let bad = !out || rows < 1; rule.conds.forEach((c, i) => { const ref = `$${colName(W + 1 + i)}$1`; - const cv = cellOf(conds[c.from.name]); + const cv = cellOf(condOf(conds, c.from)); if (cv) 칸[`${colName(W + 1 + i)}1`] = cv; const same = at(c.col); const low = at(`${c.col}·아래`); diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index 4cfbee4d8..3d39c8e64 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -680,3 +680,107 @@ def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음 k: float(v) for k, v in screen_out.items() } assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks + + +# ── 36-0b 선 조건 여러 줄(그리고 · 또는) · 값 목록 ───────────────────────────── +def test_조건_여러_줄_그리고_또는_글_식_서버같음() -> None: + lg = _ground() + two = { + "kind": "gate", + "conds": [ + {"from": {"box": "in-ground", "name": "지반"}, "op": "=", "value": "토사"}, + {"from": {"box": "in-ground", "name": "굴착량"}, "op": "≥", "value": 50}, + ], + "join": "그리고", + "from": {"box": "in-ground", "name": "지반"}, + "op": "=", + "value": "토사", + } + either = {**two, "join": "또는"} + cases = [ + (two, {"in-ground": {"지반": "토사", "굴착량": 100}}), + (two, {"in-ground": {"지반": "토사", "굴착량": 10}}), + (either, {"in-ground": {"지반": "암", "굴착량": 100}}), + (either, {"in-ground": {"지반": "암", "굴착량": 10}}), + ] + got = _run( + RULE + + f"const base = {json.dumps(lg, ensure_ascii=False)};\n" + + f"const cases = {json.dumps(cases, ensure_ascii=False)};\n" + "out(cases.map(([rule, vals]) => {\n" + " const lg = { ...base, links: base.links.map(l => l.id === 'l1' ? { ...l, rule } : l) };\n" + " const ev = E.evalLogic(lg, vals);\n" + " return [R.ruleLabel(rule), R.applyRule(rule, 1, { 지반: vals['in-ground'].지반, 굴착량: vals['in-ground'].굴착량 }).formula," + " ev.ports['fn-exc'], ev.breaks.filter(b => b.link === 'l1').map(b => b.kind)];\n" + "}));" + ) + labels = [g[0] for g in got] + assert labels[0] == "지반 = 토사 그리고 굴착량 ≥ 50 일 때만" + assert labels[2] == "지반 = 토사 또는 굴착량 ≥ 50 일 때만" + assert got[0][1] == 'IF(AND(C1="토사",D1>=50),A1,NA())' + assert got[2][1] == 'IF(OR(C1="토사",D1>=50),A1,NA())' + passed = [g[3] == [] for g in got] + assert passed == [True, False, True, False] + # 서버(Compute) = 화면(Eval) + for (rule, values), (_, _, ports, breaks) in zip(cases, got): + srv_lg = { + **lg, + "links": [{**ln, "rule": rule} if ln["id"] == "l1" else ln for ln in lg["links"]], + } + srv = _server(srv_lg, values) + exc = srv["ports"].get("fn-exc") or {} + assert {k: (None if v is None else float(v)) for k, v in exc.items()} == { + k: (None if v is None else float(v)) for k, v in ports.items() + } + assert [b["kind"] for b in srv["breaks"] if b.get("link") == "l1"] == breaks + + +def test_옛_한_줄_꼴과_makeGate() -> None: + got = _run( + RULE + + "const old = { kind: 'gate', from: { box: 'b1', name: '지반' }, op: '=', value: '토사' };\n" + "const made = M.makeGate([{ from: { box: 'b1', name: '지반' }, op: '≠', value: '암' }, { from: { box: 'b1', name: '높이' }, op: '<', value: 3 }], '또는');\n" + "out([M.gateConds(old), R.ruleLabel(old), made.from, made.op, made.value, M.ruleSources(made).map(p => p.name)," + " R.applyRule(made, 7, { 지반: '암', 높이: 2 }).value, R.applyRule(made, 7, { 지반: '암', 높이: 5 }).why]);" + ) + conds, label, first_from, first_op, first_value, sources, ok, blocked = got + assert conds == { + "conds": [{"from": {"box": "b1", "name": "지반"}, "op": "=", "value": "토사"}], + "join": "그리고", + } + assert label == "지반 = 토사 일 때만" + assert (first_from, first_op, first_value) == ({"box": "b1", "name": "지반"}, "≠", "암") + assert sources == ["지반", "높이"] + assert (ok, blocked) == (7, "통과 안 됨") + + +def test_값_목록() -> None: + got = _run( + PIPE + + "lg = M.updateBox(lg, dia.id, { input: { ...lg.boxes[0].input, variables: [...lg.boxes[0].input.variables," + " { name: '지반', type: 'choice', unit: '-', choices: ['토사', '암'], example: '토사' }] } });\n" + "lg = M.updateBox(lg, mat.id, { input: { ...lg.boxes[1].input, elements: lg.boxes[1].input.elements.map((e, i) => i === 1 ? { ...e, spec: '12mm' } : e) } });\n" + "r = add(lg, 'input'); lg = r.lg; const tb = r.box;\n" + "lg = M.updateBox(lg, tb.id, { input: { mode: 'group', group: '소요량', elements: [], variables: [], table: { key: 'QF1' } } });\n" + "const vals = { [tb.id]: { 표: [['돌', '석공'], ['깬돌', 0.2], ['견치돌', 0.3], ['깬돌', 0.25]] } };\n" + "const v = (box, name, opts) => M.valueChoices(lg, { box, name }, opts);\n" + "out([v(dia.id, '지반'), v(dia.id, '관경'), v(mat.id, '거푸집 자리'), v(mat.id, '흄관 단가')," + " v(tb.id, '표', { col: '돌', values: vals }), v(tb.id, '표', { col: '없음', values: vals }), v(tb.id, '표')," + " v(f.id, '재료값'), v('없는 상자', '지반')]);" + ) + assert got == [["토사", "암"], None, ["12mm"], None, ["깬돌", "견치돌"], None, None, None, None] + + +def test_조건_여러_줄_겹침_안맞음() -> None: + got = _run( + SPLIT + + "const both = (op1, v1) => M.makeGate([{ from: { box: v.id, name: '지반' }, op: op1, value: v1 }, { from: { box: v.id, name: '높이' }, op: '≥', value: 3 }]);\n" + "let g = M.setRule(lg, lg.links[0].id, both('=', '토사')).lg;\n" + "g = M.setRule(g, lg.links[1].id, both('≠', '암')).lg;\n" + "const over = E.breaks(g, V('토사')).filter(b => b.level === '문제').map(b => b.kind);\n" + "const none = E.breaks(g, V('암', 2)).filter(b => b.level === '문제').map(b => b.kind);\n" + "out([over, none]);" + ) + over, none = got + assert "조건 겹침" in over + assert "조건 안 맞음" in none From 672dbbd1b7256d74c9d34217ba314999891fc576 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:29:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore(M01):=20=ED=9D=90=EB=A6=84=EB=8F=84?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20=EC=A1=B0=EC=82=AC=20=E2=80=94=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EC=82=AC=EB=B3=B8=20=EB=8B=B4?= =?UTF-8?q?=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YAxqrz1iadqdygC6bE3pic --- docs/raw/M01_쉬운단가/흐름도_적용_조사.md | 121 ++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/raw/M01_쉬운단가/흐름도_적용_조사.md diff --git a/docs/raw/M01_쉬운단가/흐름도_적용_조사.md b/docs/raw/M01_쉬운단가/흐름도_적용_조사.md new file mode 100644 index 000000000..c2672300d --- /dev/null +++ b/docs/raw/M01_쉬운단가/흐름도_적용_조사.md @@ -0,0 +1,121 @@ +# 흐름도 본 프로그램 적용 — 36-1 옛 식 조사 (sub6 · 읽기만) + +근거 = 마스터 로직 파일 57 개 · 로직 줄 **1,472**(자체 `로직_자체.json` 포함) · 식을 지금 서버 파서(`master_formula.parse`)로 전부 읽음 — **못 읽는 식 0** · 파일 안 씀. +수 칸 = 「나온 수(식 마디 수) / 그 꼴이 든 로직 수」 · 예 = 키 + 식 앞부분. + +## ① 옛 식을 푸는 서버 코드 + +| 무엇 | 자리 | +| --- | --- | +| 식 문법 · 파서(`_Parser` — 또는 → 그리고 → 비교 → + - → * / → 부호 · `.칸` → 기본) | `resources/master_data/scripts/master_formula.py:56`(`parse` :225 · 함수 목록 `FUNCS` :23 = 만약 · 올림 · 버림 · 반올림 · 최소 · 최대 · 비교 `_OPS` :27) | +| 기본 마디 — `( )` · `'글'` · 수 · `찾기(표, 조건=값…)` · `로직(키, 넘길=값…)` · 함수 · 요소 키(`LB000002`) · `ID:원문번호{입력}`(`EQ:{기계}`) · 이름 | 같은 파일 `primary` :185 · 요소 키 판정 `mk.KEY`(`master_keys.py`) | +| 셈(나무 풀기) — 이름 · 요소 값 · 찾기 · 로직 부름 · 함수 · 사칙 · 비교 · 그리고 / 또는(지름길) | `master_formula.py:385` `evaluate` | +| 표 찾기 — 빈 조건 칸 = 무엇이든 · 범위규칙(초과·이하 · 이상·이하 · 이상·미만 · 이하) · 여러 줄이면 위 끝 최소 · 찾는 칸 있는 줄 | `master_formula.py:331` `_match` · `:362` `find` | +| 로직 한 줄 셈 — 입력 검사 → 중간 차례 → 결과 식 또는 호표 줄(수량 × 단가 · 로직 줄은 비목 나눔) → 덧줄(앞 줄까지 비목 합) → 끝수 | `master_formula.py:538` `run` · 입력 `_inputs` :489 · 빈 가격 입력 `fill_empty` :479 · 끝수 `cut` :468 · `{입력}` 채우기 `fill` :314 | +| 요소 값 · 재료 단가 | `master_material.py:238` `element` · `master_pick.py:130` `priced` · `:123` `pick`(검색어 묶음 → 품목) | +| 정적 검사(저장 때) | `master_formula.py:744` `check_logic` · `M01_MasterData_Store_Cache.py:69` `_check` | +| M01 셈 진입 | `M01_MasterData/M01_MasterData_Store.py:543` `calc`(사본 읽기 → 줄 바꿔 끼움 `_swap` :524 → `cm.calc`) · `:572` `text`(읽는 식 줄 `master_text.py:493 lines`) · `:563` `auto`(견본 입력) | +| 일괄 | `check_master.py:174` `calc` · `:511` `calc_all`(시험입력으로 전 로직) | + +## ② 1,472 로직 식 말 전수 + +| 꼴 | 나온 수 | 로직 수 | 예(3) | +| --- | --- | --- | --- | +| 따옴표 글 | 12,536 | 1,085 | GC000994 `로직(GC000276, 기계='5401-0017', 지역=지역)` · GC000996 `로직(GC000273, 기계='6105-0190')` · GC000997 `로직(GC000273, 기계='6516-0060')` | +| 이름 = 입력 | 10,815 | 1,178 | GC000994 `찾기(QC001863, 지반구분=지반구분).시공량` · GC000996 `만약(작업위치 == '경사면(고소작업차)', 찾기(QC001865).'고소작업차(5ton)' * 8 / 시공량, 0)` · GC000997 `로직(GC000268, 기계=크레인, 지역=지역)` | +| 찾기(…).칸 | 8,156 | 1,401 | GC000994 `찾기(QC001863, 지반구분=지반구분).시공량` · GC000995 `찾기(QC001864).시공량` · GC000996 `찾기(QC001865).시공량` | +| 찾기(표 · 조건) | 8,156 | 1,401 | GC000994 `찾기(QC001863, 지반구분=지반구분).시공량` · GC000995 `찾기(QC001864).시공량` · GC000996 `찾기(QC001865).시공량` | +| 사칙 * | 7,697 | 1,037 | GC000994 `찾기(QC000237).크롤러드릴_공기식 * 8 / 시공량` · GC000996 `찾기(QC001865).'그라우팅 믹서(190×2ℓ)' * 8 / 시공량` · GC000997 `찾기(QC001866).크레인 * 8 / 시공량` | +| 이름 = 중간값 | 7,367 | 1,184 | GC000994 `찾기(QC000237).보링공 / 시공량` · GC000995 `찾기(QC001864).철근공 / 시공량` · GC000996 `찾기(QC001865).보링공 / 시공량` | +| 사칙 / | 3,824 | 1,019 | GC000994 `찾기(QC000237).보링공 / 시공량` · GC000995 `찾기(QC001864).철근공 / 시공량` · GC000996 `찾기(QC001865).보링공 / 시공량` | +| 비교 == | 3,736 | 451 | GC000996 `만약(작업위치 == '경사면(고소작업차)', 찾기(QC001865).'고소작업차(5ton)' * 8 / 시공량, 0)` · GC000999 `만약(시공구분 == '기계시공', 찾기(QC001868, 시공구분=시공구분, 수종=수종, 흉고직경=흉고직경).고소작업차 * 8` · GC001000 `만약(시공구분 == '기계시공', 찾기(QC001869, 시공구분=시공구분, 수종=수종, 흉고직경=흉고직경).고소작업차 * 8` | +| 함수 만약 | 3,541 | 477 | GC000996 `만약(작업위치 == '경사면(고소작업차)', 찾기(QC001865).'고소작업차(5ton)' * 8 / 시공량, 0)` · GC000999 `만약(시공구분 == '기계시공', 찾기(QC001868, 시공구분=시공구분, 수종=수종, 흉고직경=흉고직경).고소작업차 * 8` · GC001000 `만약(시공구분 == '기계시공', 찾기(QC001869, 시공구분=시공구분, 수종=수종, 흉고직경=흉고직경).고소작업차 * 8` | +| 호표 요소 = 키(인력) | 3,451 | 1,369 | GC000994 `LB000014` · GC000995 `LB000008` · GC000996 `LB000014` | +| 사칙 + | 3,094 | 668 | GC000996 `(보링공 * LB000014 + 기계설비공 * LB000051 + 특별인부 * LB000003) * 11 / 100` · GC000997 `(중급기술자 * LB000286 + 보링공 * LB000014 + 특별인부 * LB000003 + 보통인부 * LB000002` · GC000998 `(철공 * LB000009 + 보통인부 * LB000002) * 3 / 100` | +| 로직(다른 로직 부르기) | 1,116 | 506 | GC000994 `로직(GC000276, 기계='5401-0017', 지역=지역)` · GC000996 `로직(GC000273, 기계='6105-0190')` · GC000997 `로직(GC000268, 기계=크레인, 지역=지역)` | +| 로직 넘길 값 있음 | 1,089 | 506 | GC000994 `로직(GC000276, 기계='5401-0017', 지역=지역)` · GC000996 `로직(GC000273, 기계='6105-0190')` · GC000997 `로직(GC000268, 기계=크레인, 지역=지역)` | +| 호표 요소 = 로직 부르기(장비 · 일위대가) | 1,075 | 506 | GC000994 `로직(GC000276, 기계='5401-0017', 지역=지역)` · GC000996 `로직(GC000273, 기계='6105-0190')` · GC000997 `로직(GC000268, 기계=크레인, 지역=지역)` | +| 호표 비목 없음(로직 줄 → 비목 나눔) | 1,075 | 506 | GC000994 `로직(GC000276, 기계='5401-0017', 지역=지역)` · GC000996 `로직(GC000273, 기계='6105-0190')` · GC000997 `로직(GC000268, 기계=크레인, 지역=지역)` | +| 찾기 조건 없음(표 한 줄) | 1,022 | 286 | GC000994 `찾기(QC000237).보링공 / 시공량` · GC000995 `찾기(QC001864).시공량` · GC000996 `찾기(QC001865).시공량` | +| 요소 키 부르기(인력) | 924 | 393 | GC000996 `(보링공 * LB000014 + 기계설비공 * LB000051 + 특별인부 * LB000003) * 11 / 100` · GC000997 `(중급기술자 * LB000286 + 보링공 * LB000014 + 특별인부 * LB000003 + 보통인부 * LB000002` · GC000998 `(철공 * LB000009 + 보통인부 * LB000002) * 3 / 100` | +| 호표 요소 = 재료 고르기 묶음(구분 · 상세구분 · 검색어) | 673 | 231 | GC000319 `{"구분": "도료·방수·접착", "상세구분": "도료", "검색어": "융착식 프라이머"}` · GC000337 `{"구분": "철근·강재", "상세구분": "철망·와이어메쉬", "검색어": "철망 Φ3.2."}` · GC000723 `{"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""}` | +| 논리 또는 | 297 | 35 | GC000346 `(만약(공법군 == '발파굴착A군' 또는 공법군 == '기계굴착A군', 착암공인원, 0)) * 배율` · GC000111 `만약(토질_암분류 == '호박돌 섞인 고결토・경질토' 또는 토질_암분류 == '풍화암', 로직(GC000267, 기계='020` · GC001136 `찾기(QC002058, '규격(㎜)범위'=만약(규격 == 'ø15' 또는 규격 == '20' 또는 규격 == '25' 또는 규` | +| 사칙 - | 273 | 156 | GC000998 `찾기(QC001867).시공량 * (1 - (수직고초과단계 * 9 - 경사가산) / 100)` · GC001008 `찾기(QC001880, 기계구분=기계구분).시공량 * (1 - (-만약(연간시공횟수 == '연3회 이상', 0, 찾기(QC00` · GC001009 `찾기(QC001883).시공량 * (1 - (-만약(연간시공횟수 == '연3회 이상', 0, 찾기(QC001884, 연간시공횟` | +| 논리 그리고 | 199 | 23 | GC001120 `만약(부위 == '뿜칠재' 그리고 뿜칠면 == '콘크리트면', 1.2, 1)` · GC000753 `찾기(QC001431, '배관규격(mm)'=배관규격, 보온재=보온재, 보온두께=보온두께).시공량 * (1 + (0 - 만약(설` · GC000769 `만약((규격 >= 15 그리고 규격 <= 25) 또는 (규격 >= 32 그리고 규격 <= 50), 0, 찾기(QC001447,` | +| 시험입력 있음 | 195 | 0 | | +| 요소키.칸(요소 칸 값) | 160 | 49 | GC001043 `EQ000532.주연료2 * 찾기(QC001933, 맨홀구분=맨홀구분).'모르타르 믹서(0.3㎥)' * 8 / 시공량` · GC000137 `찾기(QC000264).IC진동롤러_자주식 * 8 * EQ000098.주연료 * 찾기(QC000270, 구분='주연료 할증')` · GC000138 `EQ:{장비}.주연료종류` | +| 이름 = 비목 합(노무비 · 재료비 · 경비) | 153 | 146 | GC000722 `노무비 * 기계경비율 / 100` · GC000723 `노무비 * 기계경비율 / 100` · GC000724 `노무비 * 기계경비율 / 100` | +| 비교 <= | 123 | 36 | GC000570 `만약(층수 <= 10, 0, 만약(층수 <= 15, 4, 4 + 올림((층수 - 15) / 5)))` · GC000346 `만약(갱구거리 <= 500, 0, 찾기(CC000077, 갱구에서뚫기점까지의거리=갱구거리).할증률)` · GC000185 `만약(타설깊이 <= 25, 찾기(QC000348, L='25이하').t_1, 만약(타설깊이 <= 30, 찾기(QC000348,` | +| 함수 최대 | 122 | 43 | GC000750 `올림(최대(댐퍼면적 - 0.1, 0) / 0.1)` · GC000373 `최대(찾기(CC000125, 형식=만약(형식 == '박스거더' 또는 형식 == '강바닥판 박스거더', '박스거더', '박스거더` · GC000203 `최대(만약(용도 == '신구-콘크리트 접착제바르기', 찾기(QC000412, 용도=용도, 재료명='Epoxy신구-콘크리트접착제` | +| 비교 > | 99 | 44 | GC000575 `찾기(QC001210, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` · GC000576 `찾기(QC001212, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` · GC000580 `찾기(QC001218, '시공높이(m)'=최소(시공높이m, 7.2), '블록규격(mm)'=블록규격mm, 마감=마감).시공량 *` | +| 요소 부르기 ID:원문번호(+{입력}) | 84 | 14 | GC000138 `EQ:{장비}.주연료종류` · GC000139 `EQ:{장비}.주연료종류` · GC000267 `버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) * (1 + 가산비율 / ` | +| 요소 키 부르기(기계) | 78 | 37 | GC001043 `EQ000532.주연료2 * 찾기(QC001933, 맨홀구분=맨홀구분).'모르타르 믹서(0.3㎥)' * 8 / 시공량` · GC000137 `찾기(QC000264).IC진동롤러_자주식 * 8 * EQ000098.주연료 * 찾기(QC000270, 구분='주연료 할증')` · GC000140 `EQ000004.주연료종류` | +| 찾기 조건 값이 식 | 70 | 33 | GC001008 `(특별인부 * LB000003 + 보통인부 * LB000002 + 그물막인부 * LB000002) * 찾기(QC001882, ` · GC000575 `찾기(QC001210, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` · GC000576 `찾기(QC001212, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` | +| 함수 올림 | 67 | 53 | GC000570 `만약(층수 <= 10, 0, 만약(층수 <= 15, 4, 4 + 올림((층수 - 15) / 5)))` · GC000750 `올림(최대(댐퍼면적 - 0.1, 0) / 0.1)` · GC000575 `찾기(QC001210, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` | +| 호표 요소 = 자리 표시 {입력}(LB:{직종} 등) | 66 | 57 | GC000559 `LB:{공수직종}` · GC001043 `MO:휘발유:{지역}` · GC000137 `MO:경유:{지역}` | +| 함수 최소 | 51 | 18 | GC000575 `찾기(QC001210, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` · GC000576 `찾기(QC001212, '시공높이(m)'=최소(시공높이m, 7.2), 벽두께=벽두께).시공량 * (1 - (만약(시공높이m >` · GC000580 `찾기(QC001218, '시공높이(m)'=최소(시공높이m, 7.2), '블록규격(mm)'=블록규격mm, 마감=마감).시공량 *` | +| 빼기 부호(-x) | 47 | 28 | GC001008 `찾기(QC001880, 기계구분=기계구분).시공량 * (1 - (-만약(연간시공횟수 == '연3회 이상', 0, 찾기(QC00` · GC001009 `찾기(QC001883).시공량 * (1 - (-만약(연간시공횟수 == '연3회 이상', 0, 찾기(QC001884, 연간시공횟` · GC001012 `찾기(QC001889).시공량 * (1 - (-살포량증감) / 100)` | +| 비교 < | 46 | 26 | GC000561 `만약(층수층 < 6, 찾기(CC000157, '강재사용량(㎏/㎡)'=강재사용량).'보정치(a)' * 찾기(CC000159, '` · GC000564 `만약(강재총사용량 < 300, 1, 찾기(CC000165, '강재총사용량(t)'=강재총사용량, '1t당 볼트본수(본)'=볼트본` · GC000101 `만약(수심증감 == '적용 안 함', 0, 만약(수심 < 2.5, 0 - 찾기(QC000204, 수심=수심).감, 만약(수심 ` | +| 함수 반올림 | 42 | 41 | GF000003 `반올림(3600 * q * K * f * E / Cm, 3)` · GF000042 `반올림(3600 * q * K * f * E / Cm, 3)` · GF000088 `반올림(3600 * q * K * f * E / Cm, 3)` | +| 재료 묶음에 {입력} | 32 | 19 | GC000264 `{"구분": "레미콘", "상세구분": "레미콘", "검색어": "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"}` · GC000431 `{"구분": "기계·설비", "상세구분": "기계류", "검색어": "싱글코아바렐 {규격}"}` · GC000432 `{"구분": "기계·설비", "상세구분": "기계류", "검색어": "더블코아바렐 {규격}"}` | +| 함수 버림 | 32 | 17 | GC000267 `버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) * (1 + 가산비율 / ` · GC000268 `버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) + EQ:{기계}.관리비계` · GC000269 `버림(EQ:{기계}.가격 * 1000 * ((EQ:{기계}.상각비계수 + EQ:{기계}.정비비계수) * (1 + 가산비율 / ` | +| 결과 식 로직(돈 아님) | 27 | 27 | GC000422 `a * E` · GF000003 `반올림(3600 * q * K * f * E / Cm, 3)` · GF000042 `반올림(3600 * q * K * f * E / Cm, 3)` | +| 요소 키 부르기(재료) | 23 | 8 | GC000360 `((찾기(QC000648, 품명='아세틸렌', 레일규격=레일규격).수량) * MT003163 + (찾기(QC000648, 품명` · GC000361 `((찾기(QC000650, 품명='테르밋용재', 레일규격=레일규격, 규격='점화용').수량) * MT030628 + (찾기(Q` · GC000374 `(찾기(QC000664, 품명='용접봉').수량 * MT003191 + 찾기(QC000664, 품명='산소').수량 * 100` | +| 비교 >= | 21 | 11 | GC000101 `만약(수심증감 == '적용 안 함', 0, 만약(수심 < 2.5, 0 - 찾기(QC000204, 수심=수심).감, 만약(수심 ` · GC000769 `만약((규격 >= 15 그리고 규격 <= 25) 또는 (규격 >= 32 그리고 규격 <= 50), 0, 찾기(QC001447,` · GC000774 `만약((규격 >= 15 그리고 규격 <= 25) 또는 (규격 >= 32 그리고 규격 <= 50), 0, 찾기(QC001452,` | +| 로직(…).칸(비목 · 계) | 14 | 11 | GC000087 `로직(GC000271, 기계='0201-0100', 브레이커='0230-0010', 지역=지역, 보정작업=보정작업).계 * (` · GC000090 `로직(GC000271, 기계='0201-0100', 브레이커='0230-0010', 지역=지역, 보정작업=보정작업).계 * (` · GC000091 `로직(GC000268, 기계='5405-0110', 지역=지역).계 * (찾기(QC000189).유압식크롤러드릴)` | +| 비교 != | 12 | 8 | GC000068 `만약(작업 != '목재 붙이기', 찾기(QC000143, 구분='보통인부', 작업=작업).수량, 0)` · GC000176 `만약(비트 != '오거비트', 해머압축기대수 * T / 60, 0)` · GC000183 `만약(재질 != '토사', T1 / T * T / 60, 0)` | +| 줄.'호표 이름'(호표 줄 금액) | 7 | 3 | GC000211 `(줄.'합판(내수 12㎜)' + 줄.'각재') * 찾기(QC000428, 사용횟수=사용횟수).소모자재 / 100` · GF000185 `(노무비 + 재료비 + 경비 - 줄.'구체콘크리트(철근) 레미콘') * 찾기(QF000387, 구분='구체콘크리트(철근) - ` · GF000210 `(줄.'패널' + 줄.'내부패널') * 찾기(QF000418, 구분=유형).요율 / 100` | +| 로직 넘길 값이 식 | 5 | 5 | GC000332 `로직(GC000267, 기계=만약(형식 == 'A-Type', '0201-0040', '0201-0020'), 지역=지역, 보` · GF000294 `로직(GC000268, 기계=만약(근원직경 < 20, '2105-0010', 만약(찾기(QF000063, '근원직경(cm)'=` · GF000297 `로직(GC000268, 기계=만약(흉고직경 < 18, '2105-0010', 만약(찾기(QF000081, '흉고직경(cm)'=` | +| (글자) 이름( 꼴(함수 아닌 이름 뒤 괄호) | 1,177 | — | GC000996 `찾기(QC001865).'그라우팅 믹서(190×2ℓ)' * 8 / 시공량` · GC000996 `찾기(QC001865).'그라우팅 펌프(30∼60ℓ/min)' * 8 / 시공량` · GC000996 `만약(작업위치 == '경사면(고소작업차)', 찾기(QC001865).'고소작업차(5ton)' * 8 / 시공량, 0)` | +| (글자) ∼ · ~ 물결 | 63 | — | GC000996 `찾기(QC001865).'그라우팅 펌프(30∼60ℓ/min)' * 8 / 시공량` · GC001026 `찾기(QC001905, 절단폭=절단폭, 차로차단=차로차단).'굴착기+브레이커(0.2㎥~0.6㎥)' * 8 / 시공량` · GC000573 `찾기(QC001207, 구분='그라우팅펌프', 규격='40∼125(ℓ/min)').수량` | +| (글자) % 글자 | 62 | — | GC000298 `찾기(QC000519, 포장=포장).시공량 * (1 - 만약(포장 == '공항포장', 만약(지장물 == '연속 포설 불가', ` · GC000299 `찾기(QC000520, 포장=포장).시공량 * (1 - 만약(포장 == '공항포장', 만약(지장물 == '연속 포설 불가', ` · GC000303 `찾기(QC000524, 형식=형식).시공량 * (1 - 만약(유도블록 == '아님', 0, 찾기(QC000583, 구분='유도` | +| (글자) {} 자리 표시 | 50 | — | GC000138 `EQ:{장비}.주연료종류` · GC000138 `1 * 8 / 시공량 * EQ:{장비}.주연료 * 찾기(QC000266, 구분='주연료 할증').값 / 100` · GC000139 `EQ:{장비}.주연료종류` | +| (글자) & 글자 | 53 | — | GC000931 `(찾기(QC001700, 공정별='Draft tube설치 가설된 Concrete tube에 이어서 Leveling & Cent` · GC000931 `(찾기(QC001700, 공정별='소운반').비계공 + 찾기(QC001700, 공정별='Draft tube설치 가설된 Conc` · GC000931 `(찾기(QC001700, 공정별='Draft tube설치 가설된 Concrete tube에 이어서 Leveling & Cent` | +- 「요소 부르기 꼴 `보통인부(…)`」 — **문법에 없음**(이름 뒤 괄호는 함수 · 찾기 · 로직만). 글자로 1,177 번 보이는 「이름(」 은 모두 따옴표 칸 이름 안(`찾기(…).'고소작업차(5ton)'`). 요소는 키로만 부름(`LB000006` · `EQ000532.주연료2` · `EQ:{기계}.가격`). +- `&` · `∼`/`~` · `%` · `{ }` 글자 — `&` · `∼` · `%` 는 모두 따옴표 글(조건 값 · 칸 이름) 안 · `{ }` 는 `ID:원문번호{입력}` 와 재료 검색어 · 자리 표시 안(아래 ③). +- 끝수 — 1,472 중 묶음(`{대상, 자리, 방법}`) **0** · 설명 글 37 · 없음 1,435 → 엔진이 끝수를 실제로 붙이는 로직은 지금 없음(덧줄 · 함수 안 `반올림(…)` 으로 씀). +- 표 — 로직이 찾는 표 1,865 · 범위 조건이 있는 표 106(범위규칙 글 40 꼴 — 첫 낱말은 넷뿐) · 값 칸에 「1/1.30」 같은 식 글이 든 표 104(엔진이 식으로 풂). + +## ③ 흐름도로 옮기기 어려운 꼴 + +| 꼴 | 수(마디 / 로직) | 왜 어려운가 | 이렇게 옮기면 됨 | +| --- | --- | --- | --- | +| 요소 칸 값 `EQ000532.주연료2` · `EQ000098.주연료` | 160 / 49 | 요소의 값이 아니라 **요소의 다른 칸**(기계 주연료 · 가격 · 계수) | 입력 상자 「기계」 요소 알약을 칸별로(바깥 값 「기계 key + field」 — Api 에 이미 있음 `CfQuery 기계 field`) | +| `ID:원문번호{입력}` — `EQ:{기계}.가격` · `EQ:{장비}.주연료` | 84 / 14 | 부를 요소가 **입력값에 따라 바뀜**(기계경비 로직 GC000267 등 핵심 로직) | 「재료 자리」 처럼 「기계 자리」(선택 자리 · 입력 알약 값으로 키 고름) — 지금 흐름도에 없음 → **새 입력 방식 필요(⬜ 브레인)** · 그 전엔 「다른 로직 입력」 으로 기계경비 로직을 통째로 부름 | +| 호표 요소 자리 표시 `LB:{공수직종}` · `MO:경유:{지역}` | 66 / 57 | 노임 직종 · 유가 지역이 입력값에 따라 바뀜 | 같은 「선택 자리」(인력 · 유가전력) — 재료 방식 셋을 다른 그룹에도 | +| 재료 묶음 검색어에 `{입력}` — `레미콘 {굵은골재최대치수}-{강도}-{슬럼프}` | 32 / 19 | 검색어가 입력값으로 만들어짐 | 재료 「조건 찾기」(이름 / 규격에 알약 값) — 알약 여럿을 이어 붙인 글이 필요 → 함수 상자로 글을 만들어 조건 알약으로 | +| 호표 로직 줄(비목 없음) `로직(GC000267, 기계='0201-0020', 지역=지역)` | 1,075 / 506 | 한 줄 금액이 **노무비 · 재료비 · 경비 셋으로 나뉨** | 다른 로직 입력 상자의 출력 알약 셋(노무비 · 재료비 · 경비) × 수량 함수 셋 → 출력 셋 | +| 로직 넘길 값이 식 `기계=만약(형식 == 'A-Type', '0201-0040', …)` | 5 / 5 | 넘길 값이 조건으로 바뀜 | 함수 상자(IF)로 값을 만들어 다른 로직의 받는 알약에 선 | +| 찾기 조건 값이 식 `'시공높이(m)'=최소(시공높이m, 7.2)` | 70 / 33 | 조건 값이 계산값 | 함수 상자로 먼저 셈 → 「표에서 찾기」 조건 알약에 그 출력 | +| 덧줄의 비목 합 `노무비 * 기계경비율 / 100` | 153 / 146 | 바탕이 **앞 줄까지의 비목 합**(덧줄이 덧줄을 바탕으로 쓸 수 있음) | 출력 상자 비목별 모음 함수(앞 줄 차례대로) → 비율 함수 · 앞 덧줄도 받는 알약으로 | +| 호표 줄 금액 `줄.'합판(내수 12㎜)'` | 7 / 3 | 특정 호표 줄 금액을 다시 씀 | 그 줄 금액 함수 출력에서 선 | +| 로직 결과 칸 `로직(…).계` | 14 / 11 | 다른 로직 돈 결과의 한 칸 | 다른 로직 입력의 그 비목 · 계 알약 | +| 표 한 줄(조건 없는 찾기) `찾기(QC001864).시공량` | 1,022 / 286 | 조건이 없어 「줄 하나」 표 | 「표에서 찾기」 조건 없이 칸만(표 첫 줄) — 줄이 둘 이상이면 옛 엔진도 오류 | +| 범위 조건 표(범위규칙 넷 · 위 끝 최소) | 표 106 | 엑셀 한 식으로 쓰기 어려움(빈 칸 = 무엇이든 · 규칙별 경계) | 선 규칙 「표에서 찾기」 가 엔진 `find` 와 같은 뜻으로(줄마다 맞음 칸 + INDEX/MATCH — 옛 가져오기에서 쓴 꼴) | +| 표 값의 식 글 「1/1.30」 | 표 104 | 엔진은 글을 식으로 풂 · 엑셀은 글 | 서버가 표 블록을 줄 때 수로 바꿔 줌 | +| 결과 식 로직(돈 아님) | 27 / 27 | 출력이 비용 아님 | 출력 종류 「값」 | +| 빈 입력 · 표에 없는 조건 | — | 엔진은 **그 줄만 비우고** 계속(합에서 뺌) | 흐름도 「끊긴 곳」 + 그 출력 0 으로 셈(같은 합) | + +## ④ 계산 길(로직 셈)을 부르는 곳 + +| 어디 | 무엇 | 자리 | +| --- | --- | --- | +| 서버 `POST /api/m01/calc` | 시험 계산(화면 · 저장 전 줄 포함) | `M01_MasterData_Router.py:222` → `Store.calc` :543 | +| 서버 `POST /api/m01/text` | 읽는 식 줄(텍스트 수식) | `M01_MasterData_Router.py:228` → `Store.text` :572 | +| 서버 `GET /api/m01/logic/auto` | 견본 입력값 | `M01_MasterData_Router.py:192` → `Store.auto` :563 → `master_auto.values` :80 | +| 화면 M01 단가산출 시험 계산 | `/calc` · `/text` · `/logic/auto` | `M01_MasterData_UI_Logic_Api.ts:224 · :229 · :245` · 부름 `M01_MasterData_UI_Logic_Calc.ts:142 · :155 · :184` | +| 서버 일위대가 조합 미리보기 | 조합에 담긴 로직마다 셈 | `M01_MasterData_Store_Combo.py:259 combo_preview` → `master_combo.py:106 run`(:128 `mf.run`) · M02 `POST /api/m02/layers/{layer}/combos/preview` `M02_MasterTemplete_Router_Combos.py:195` → `M02_MasterTemplete_Combo_Store.py:193` | +| 서버 프로젝트 사본 계산 | 프로젝트 항목 단가(복사본 표 · 노임) | `POST /api/m02/projects/{id}/items/{folder}/calc` `M02_Project_Items_Router.py:178` → `M02_Item_Copy.py:326` → `M02_Item_Prices.py:211`(:230 `mf.run`) · 화면 `ui_template/ui_template_item_picker_project.ts:218` | +| 흐름도 서버 | 단가 · 다른 로직 값(호표 한 줄 견본 로직으로 옛 셈 부름) | `M02_DF_Values.py:68 · :105 · :229 · :262`(`store.calc`) · `M02_DF_Router.py:216 logic_value`(흐름도 셈 `M02_DF_Compute`) | +| 옛 시험판 서버(CostFlow · 남김) | 같은 꼴 | `M02_CF_Router.py:230 · :267 · :390 · :421` | +| 일괄 · 대조 스크립트 | 전 로직 시험 계산 · 견적 대조 | `check_master.py:511 calc_all` · `compare_estx.py:258 · :456` · `master_text.py:504` | + +갈래(정함 4)를 넣을 곳 = **`Store.calc` · `Store.text` 둘**(M01 화면 · 흐름도 값이 모두 여기를 지남) + 조합 미리보기 `master_combo.run` · 프로젝트 `M02_Item_Prices.calc`(이 둘은 `mf.run` 을 바로 부름 — 흐름이 있는 로직을 거르려면 여기도). + +## ⑤ 시험입력 · 기본 입력 + +- 줄에 `시험입력` 이 있는 로직 **195**(1,472 중) — 일부 입력만 적힘(예 GF000219 `{"높이": 4.0, "돌": "깬잡석"}`). +- 기본 입력 만드는 법이 **둘**이고 범위에서 갈림: + - 일괄 시험(`check_master.py:496 시험입력`) — 고르기 첫 값 · 범위 **위 끝** · 그 밖 1 · 줄의 `시험입력` 이 이김. + - 화면 견본(`master_auto.py:80 values` · `/logic/auto`) — 고르기 첫 값 · 범위 **아래 끝** · 그 밖 = 그 입력으로 찾는 표의 가장 낮은 값(없으면 나누는 수면 1 · 아니면 0) · 단위에 「원」 이면 비움. +- 전수 대조(정함 3)는 **`check_master.시험입력`**(일괄 시험과 같은 값) 을 쓰는 것을 권함 — 흐름도 변환의 변수 예시값도 같은 규칙으로(⬜ 브레인 정함 필요: 화면 견본과 범위 끝이 다름).