diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index c9dff1c54..f487a6d78 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -187,11 +187,7 @@ def get_logics( @router.get("/logic") def get_logic(key: str) -> dict: - """흐름 로직은 「입력」 칸 없음 — 보기용 「설계 때 정할 것」 을 입력 꼴로(파일 그대로).""" - got = _call(store.logic, key) - if isinstance(got["logic"].get("흐름"), dict): - got["logic"] = {**got["logic"], "입력": _num(store.cm.mf.inputs_of(got["logic"]))} - return got + return _call(store.logic, key) @router.get("/logic/auto") diff --git a/M01_MasterData/M01_MasterData_Store_Flow.py b/M01_MasterData/M01_MasterData_Store_Flow.py index 9aadb06bd..c2d83eaf0 100644 --- a/M01_MasterData/M01_MasterData_Store_Flow.py +++ b/M01_MasterData/M01_MasterData_Store_Flow.py @@ -185,7 +185,11 @@ def run(flow: dict, key: str, inputs: dict, strict: bool = False) -> dict: given = {k: _value(v, words) for k, v in (inputs or {}).items()} flow = {**flow, "key": key} values = dc._design_values(flow, given) - blank = [d for d in dc.design_inputs(flow) if strict and given.get(d["port"]) is None] + designs = dc.design_inputs(flow) if strict else [] + lack = [d["port"] for d in designs if d["port"] not in given] + if lack: # 안 준 입력 — 엔진 `_inputs` 와 같이 멈춤(빈 값으로 준 입력은 아래 「빈 입력」) + raise store.cm.mf.FormulaError(f"「{key}」 입력 「{lack[0]}」 없음") + blank = [d for d in designs if given.get(d["port"]) is None] for d in blank: values.setdefault(d["box"], {})[d["port"]] = None got = dc.run(flow, values, load, flow.get("userElements") or [], [key]) @@ -200,14 +204,22 @@ def _engine(master, ref: str, row: dict, given: dict) -> dict: 단가 · 값 · 표 = 부른 쪽이 넘긴 master(프로젝트 사본이면 그 단가 · `M02_DF_Values.SOURCE`). """ + picks = ( + store.cm.mf.mp.CHOSEN.get() or {} + ) # 고른 자재 — 흐름은 재료 자리 이름으로(호표 차례 아님) + slots = {d["port"] for d in dc.design_inputs(row[FLOW]) if d.get("kind") == "재료 자리"} + given = {**given, **{k: v for k, v in picks.items() if k in slots}} token = dv.SOURCE.set(master) try: got = run(row[FLOW], ref, given, strict=True) finally: dv.SOURCE.reset(token) - if got["errors"]: - raise store.cm.mf.FormulaError(f"「{ref}」 흐름 셈 — {' · '.join(got['errors'])}") - cost, notes = got["cost"], got.get("notes") or [] + # 빈 값(빈 입력 · 단가 없는 품목 · 표에 없는 조건 → #N/A)을 쓴 칸은 엔진처럼 그 값만 비움 — 알림 + empty = [e for e in got["errors"] if "#N/A" in e or "단가 없는 품목" in e] + stop = [e for e in got["errors"] if e not in empty] + if stop: + raise store.cm.mf.FormulaError(f"「{ref}」 흐름 셈 — {' · '.join(stop)}") + cost, notes = got["cost"], [*(got.get("notes") or []), *empty] outs = [b for b in row[FLOW].get("boxes") or [] if b.get("kind") == "output"] extra = {"중간": {}, "값들": cost["값들"], **({"알림": notes} if notes else {})} if outs and all((b.get("output") or {}).get("cost") in (None, "값") for b in outs): @@ -216,6 +228,7 @@ def _engine(master, ref: str, row: dict, given: dict) -> dict: lines = [] # 옛 「줄」 꼴 — 돈 출력 상자마다 한 줄(수량 1 · 단가 = 금액) for b in outs: kind, value = (b.get("output") or {}).get("cost"), got["outputs"].get(b["id"]) + value = None if value == dc.NA_VALUE else value if kind in dc.COSTS: unit = (b.get("output") or {}).get("unit") or "" lines.append({"이름": b.get("name") or "", "단위": unit, @@ -346,10 +359,20 @@ def _redo(row: dict, key: str) -> list[str]: def _bare(row: dict) -> dict: - """흐름 로직 줄 — 옛 식 칸(입력 · 중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본) + """흐름 로직 줄 — 계산 칸(중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본) · 「입력」 은 흐름의 + 「설계 때 정할 것」 으로 같은 꼴로 다시 씀(로직 입력 약속 하나 · 설명은 이름이 같으면 살림) · 끝수 · 시험입력 · 메타 칸은 둠 · 출처 없으면 「자체」.""" - kept = {k: v for k, v in row.items() if k not in store.cm.mf.OLD_FIELDS} - return {"끝수": None, **kept, "출처": row.get("출처") or "자체"} + kept = {k: v for k, v in row.items() if k not in store.cm.mf.CALC_FIELDS} + said = {s.get("이름"): s.get("설명") for s in row.get("입력") or [] if s.get("설명")} + inputs = [] + for spec in store.cm.mf.inputs_of(row): + one = { + "이름": spec["이름"], + **({"설명": said[spec["이름"]]} if spec["이름"] in said else {}), + } + one |= {k: spec[k] for k in ("단위", "고르기", "범위") if spec.get(k) is not None} + inputs.append(one) + return {"끝수": None, **kept, "입력": inputs, "출처": row.get("출처") or "자체"} def save(batch: list[dict]) -> dict: @@ -360,6 +383,7 @@ def save(batch: list[dict]) -> dict: row = ch.get("row") if not isinstance(row, dict) or FLOW not in row: continue + check(row[FLOW]) # 꼴부터 — 입력 약속을 흐름에서 다시 쓰기 전에 new = ch.get("op") == "add" or (ch.get("op") == "edit" and not ch.get("key")) if new: if part.get("file") != make.OWN: diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts index 37eb783c7..a91dc98b7 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts @@ -1,8 +1,7 @@ /* ============================================================================= * M02_DF_Api.ts - * 단가산출 흐름도 Api — 목록(내 것 + 견본) · 열기 · 저장(서버가 입력 값을 마스터에서 다시 받아 화면과 - * 같은 차례 · 같은 규칙으로 셈하고 받는 칸에 값만 되돌려 넣음) · 지우기 · 입력 값 한 번에 받기 · 사용자 요소 · - * 자재 찾기 · 다른 로직 값 · 설계 때 정할 것 · 단위 · 금액 표준 표. 모양 = sub7 Model · 아래는 Api 몫만. + * 단가산출 흐름도 Api — 입력 값 한 번에 받기 · 자재 찾기 · 다른 로직 값 · 설계 때 정할 것 · + * 단위 · 금액 표준 표(흐름 저장은 로직 줄 「흐름」 칸 · M01 `/save`). 모양 = sub7 Model · 아래는 Api 몫만. * 서버 = `M02_DF_Router.py`(`/api/m02/df/*`) · 정함 `docs/raw/M01_쉬운단가/흐름도_정함.md` * ========================================================================== */ @@ -90,22 +89,6 @@ export interface CfComputed { errors: string[]; } -/** 목록 한 줄 — `sample` = `_sample` 견본(읽기만 · 저장하면 새 번호) */ -export interface CfListItem { - key: string; - name: string; - resultUnit: string; - saved: string; - sample: boolean; -} - -/** 저장 답 — `logic` 은 받는 칸에 서버 값이 들어간 다이어그램 */ -export interface CfSaved { - key: string; - logic: CfLogic; - values: CfComputed; -} - /** 서버가 준 몸과 상태 번호 — 0 = 서버에 못 닿음 */ export class CfError extends Error { constructor( @@ -133,25 +116,6 @@ async function send(path: string, body?: unknown, method?: string): Promise { - return (await send<{ logics: CfListItem[] }>("/list")).logics; -} - -export async function cfGet(key: string): Promise { - return (await send<{ logic: CfLogic }>(`/get?${new URLSearchParams({ key }).toString()}`)).logic; -} - -/** 저장 — 서버가 입력 값을 마스터에서 다시 넣고 같은 엔진으로 다시 셈(브라우저 계산값은 버림) · - * 견본을 저장하면 새 번호 */ -export async function cfSave(logic: CfLogic): Promise { - return send("/save", { logic }); -} - -export async function cfDelete(key: string): Promise { - await send<{ key: string }>("/delete", { key }); -} - /** 입력 값 한 번에 — 열 때 · 예시값을 바꿀 때 화면이 받아 시트에 넣음 · * userElements = 이 다이어그램 안 사용자 요소(ref 가 `UE-…` 일 때 먼저 봄) */ export async function cfValues( @@ -161,20 +125,6 @@ export async function cfValues( return (await send<{ values: CfAnswer[] }>("/values", { items, userElements })).values; } -/** 사용자 요소 목록 */ -export async function cfUserElements(): Promise { - return (await send<{ elements: CfUserElement[] }>("/user-elements")).elements; -} - -/** 사용자 요소 만들기(공용 목록 `_data`) — id(`UE-0001`)는 서버가 줌 · 마스터에 안 씀 */ -export async function cfUserElementAdd(element: Omit): Promise { - return (await send<{ element: CfUserElement }>("/user-elements", { op: "add", element })).element; -} - -export async function cfUserElementDelete(id: string): Promise { - await send<{ id: string }>("/user-elements", { op: "delete", id }); -} - const PLAIN: Record = { 0: "서버와 이어지지 않음 — 잠시 뒤 다시 해 주세요", 400: "넣은 값의 모양이 맞지 않음", diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Router.py b/M02_MasterTemplete/DataFlow/M02_DF_Router.py index 3e48df45c..980b1526d 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Router.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Router.py @@ -1,34 +1,21 @@ -"""단가산출 다이어그램 시험 길(PLAN 35-7) — 목록 · 열기 · 저장 · 지우기 · 입력 값 · 사용자 요소. +"""단가산출 흐름도 서버 길 — 입력 값 · 다른 로직 값 · 설계 때 정할 것 · 자재 찾기 · 표준 표. - GET /api/m02/df/list → {logics: [{key, name, resultUnit, saved, sample}]} - GET /api/m02/df/get?key=DF-0001 → {logic, sample} - POST /api/m02/df/save {logic} → {key, logic, values: {ports, outputs, cost, errors}} - POST /api/m02/df/delete {key} - POST /api/m02/df/values {items, userElements?} → {values: CfAnswer[]} - GET /api/m02/df/user-elements → {elements} - POST /api/m02/df/user-elements {op: add | delete, element? | id?} + POST /api/m02/df/values {items, userElements?} → {values: CfAnswer[]} + POST /api/m02/df/logic-value {key, designs} → {key, outputs, cost, errors} + GET /api/m02/df/design-inputs?key= → {key, designs: CfDesign[]} + GET /api/m02/df/materials?q=&sub=&detail=&page=&size= → {total, page, size, items} + GET · PUT · DELETE /api/m02/df/standards → {unit, money, saved} -모양 = sub7 Model `M02_DF_Model.ts` -(CfLogic = key · name · resultUnit · boxes · links · userElements). -알약 이름(브레인 정함): 입력 상자가 내보내는 알약 = 요소 name · 표 「표」 · 변수 name / -함수 상자 받는 알약 = inputs[].port · 내보내는 알약 = output.name / 출력 상자 받는 알약 = 「값」. -저장 = 입력 값(노임 · 재료 · 유가전력 · 기계 · 환율 · 요율 · 소요량 / 계수 표 · 사용자 요소)을 -마스터(사용자 요소는 다이어그램 안 · `_data`)에서 다시 받아 → 함수 상자 시트들을 한 통합문서로 -묶어(받는 칸에 값 · 함수 → 함수는 시트 사이 참조) 우리 스프레드시트 엔진(Node · -`common_util_spreadsheet`)으로 셈 → 받는 칸에 값만 되돌려 넣어 `_data` 에 씀 · 브라우저 값은 -안 받아 적음. 단가는 기존 `/calc` 와 같은 셈. -자리 = 이 폴더 `_data/`(git 밖) · 견본 `_sample/`(git 안 · 읽기만 · 저장하면 새 번호) · -키 `DF-0001` · 사용자 요소 `UE-0001` · 마스터 파일은 안 씀. 권한 = 시스템 관리자(`main.py` 한 줄). -옛 시험판(SheetLogic) 서버를 옮겨 고쳐 씀 — 그 폴더를 import 하지 않음. -정함 = `docs/raw/M01_쉬운단가/원가다이어그램_정함.md`. +모양 = sub7 Model `M02_DF_Model.ts` CfLogic. +흐름 저장 = 로직 줄 「흐름」 칸(M01 `/save` · `M01_MasterData_Store_Flow`) — 이 길은 쓰지 않음. +다른 로직 = 마스터 로직 키(흐름이면 흐름 셈 · 아니면 지금 계산 · `M01_MasterData_Store_Flow.load`). +사용자 요소 = 흐름 안 `userElements` 만. 권한 = 시스템 관리자(`main.py` 한 줄). """ from __future__ import annotations import json import os -import re -from datetime import datetime from pathlib import Path from typing import Any @@ -41,30 +28,10 @@ from M01_MasterData.M01_MasterData_Router import _num from M02_MasterTemplete.DataFlow import M02_DF_Compute as dc from M02_MasterTemplete.DataFlow import M02_DF_Values as dv -cm = store.cm -router = APIRouter(prefix="/api/m02/df", tags=["M02 DataFlow (시험)"]) +router = APIRouter(prefix="/api/m02/df", tags=["M02 DataFlow"]) HERE = Path(__file__).resolve().parent -DATA = HERE / "_data" # 시험은 임시 폴더로 바꿈 -SAMPLE = HERE / "_sample" -USERS = "_user_elements.json" # 사용자 요소 공용 목록 — `_data` 안 -_KEY = re.compile(r"^DF-(\d{4,})$") -_UKEY = re.compile(r"^UE-(\d{4,})$") -_SAMPLE_KEY = re.compile(r"^[\w가-힣·() -]+$") -_CELL = re.compile(r"^([A-Z]+)(\d+)$") -RANGE_LOW, RANGE_HIGH = "·아래", "·위" -FIELDS = (*cm.mf.COST_ITEMS, "계") -GROUPS = ("인력", "재료", "유가전력", "기계", "소요량", "계수", "환율", "요율") -TABLE_PORT, OUTPUT_PORT = "표", "값" -COSTS = ("노무비", "재료비", "경비") - - -class LogicBody(BaseModel): - logic: dict[str, Any] - - -class KeyBody(BaseModel): - key: str +STANDARD = HERE / "_standard" # 단위 · 금액 표준 표 — 고치면 이 파일에 바로(git 이 따라감) class ValuesBody(BaseModel): @@ -72,158 +39,30 @@ class ValuesBody(BaseModel): userElements: list[dict[str, Any]] = [] -class UserBody(BaseModel): - op: str - element: dict[str, Any] | None = None - id: str | None = None +class LogicValueBody(BaseModel): + key: str + designs: dict[str, Any] = {} -# ── 자리 ────────────────────────────────────────────────────────────── -def _write(path: Path, data: Any) -> None: - DATA.mkdir(parents=True, exist_ok=True) - tmp = path.with_name(f".{path.name}.tmp") - tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8", newline="\n") - os.replace(tmp, path) +class StandardsBody(BaseModel): + unit: dict[str, Any] + money: dict[str, Any] -def _mine(key: str) -> Path: - if not _KEY.match(key or ""): - raise store.StoreError(400, f"다이어그램 번호 모양이 아님 「{key}」") - return DATA / f"{key}.json" - - -def _sample(key: str) -> Path | None: - if not key or _KEY.match(key) or not _SAMPLE_KEY.match(key): - return None - path = SAMPLE / f"{key}.json" - return path if path.is_file() else None - - -def _read(key: str) -> tuple[dict, bool]: - """(다이어그램, 견본인가) — 내 것은 `_data` · 견본은 `_sample`.""" - path = _sample(key) - if path is not None: - return json.loads(path.read_text(encoding="utf-8")), True - path = _mine(key) - if not path.is_file(): - raise store.StoreError(404, f"없는 다이어그램 「{key}」") - return json.loads(path.read_text(encoding="utf-8")), False - - -def listing() -> dict: - out = [] - for path, sample in [ - *((p, False) for p in sorted(DATA.glob("DF-*.json"))), - *((p, True) for p in sorted(SAMPLE.glob("*.json"))), - ]: - logic = json.loads(path.read_text(encoding="utf-8")) - out.append( - { - "key": path.stem, - "name": logic.get("name") or path.stem, - "resultUnit": logic.get("resultUnit") or "", - "saved": logic.get("saved") or "", - "sample": sample, - } - ) - return {"logics": out} - - -def get(key: str) -> dict: - logic, sample = _read(key) - return {"logic": {**logic, "key": key}, "sample": sample} - - -def delete(key: str) -> dict: - if _sample(key) is not None: - raise store.StoreError(403, "견본은 지울 수 없음") - _read(key) - _mine(key).unlink() - return {"key": key} - - -# ── 사용자 요소(Model CfUserElement — id · group · name · spec · unit · value) ───── -def _users() -> list[dict]: - path = DATA / USERS - return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else [] - - -def user_elements() -> dict: - return {"elements": _users()} - - -def user_edit(op: str, element: dict | None, uid: str | None) -> dict: - rows = _users() - if op == "delete": - if not any(r["id"] == uid for r in rows): - raise store.StoreError(404, f"없는 사용자 요소 「{uid}」") - _write(DATA / USERS, [r for r in rows if r["id"] != uid]) - return {"id": uid} - if op != "add" or not isinstance(element, dict): - raise store.StoreError(400, "op 는 add(element) 또는 delete(id)") - name = str(element.get("name") or "").strip() - if not name: - raise store.StoreError(400, "이름을 넣어 주세요") - group = element.get("group") or "재료" - if group not in GROUPS: - raise store.StoreError(400, f"모르는 그룹 「{group}」") - value = element.get("value") - if value is not None and (isinstance(value, bool) or not isinstance(value, (int, float))): - raise store.StoreError(400, "값은 수 또는 빈 값") - used = [int(m.group(1)) for r in rows if (m := _UKEY.match(r["id"]))] - one = { - "id": f"UE-{max(used, default=0) + 1:04d}", - "group": group, - "name": name, - "spec": str(element.get("spec") or ""), - "unit": str(element.get("unit") or ""), - "value": value, - } - _write(DATA / USERS, [*rows, one]) - return {"element": one} - - -# ── 저장 · 계산(`M02_DF_Compute`) ───────────────────────────────────── +# ── 다른 로직(`M02_DF_Compute`) ──────────────────────────────────────── def _load(key: str) -> dict: - """다른 로직 입력이 부르는 로직 — 내 것(`_data`) · 견본(`_sample`) · 마스터 키(GX · GF …)는 - M01 계산 길(흐름이면 흐름 · 옛 로직이면 옛 셈 · 정함 11).""" - if not _KEY.match(key or "") and _sample(key) is None: - return sflow.load(key) - logic, _ = _read(key) - return {**logic, "key": key} - - -def _local(logic: dict) -> list[dict]: - return [*(logic.get("userElements") or []), *_users()] - - -def save(logic: dict) -> dict: - """저장 — 입력 값 다시 받기 → 함수 상자 시트를 Node 로 셈 → 받는 칸에 값만 되돌려 `_data`.""" - if not str(logic.get("name") or "").strip(): - raise store.StoreError(400, "이름을 넣어 주세요") - if not isinstance(logic.get("boxes"), list) or not isinstance(logic.get("links"), list): - raise store.StoreError(400, "상자(boxes) · 선(links) 이 없음") - key = logic.get("key") or None - if key and _sample(key) is not None: - key = None # 견본은 그대로 두고 새 번호로 - elif key: - _read(key) # 있는 것만 고침 — 없으면 404 - got = dc.run(logic, {}, _load, _local(logic), [key or ""]) - if not key: - used = [int(m.group(1)) for p in DATA.glob("DF-*.json") if (m := _KEY.match(p.stem))] - key = f"DF-{max(used, default=0) + 1:04d}" - done = {**got.pop("filled"), "key": key, "saved": datetime.now().isoformat(timespec="seconds")} - _write(DATA / f"{key}.json", _num(done)) - return {"key": key, "logic": done, "values": got} + """다른 로직 입력이 부르는 마스터 로직 — 흐름이면 흐름 · 아니면 지금 계산(정함 11).""" + return sflow.load(key) def logic_value(key: str, designs: dict) -> dict: - """다른 로직 값 — 저장 로직 · 견본을 받은 설계 값(없으면 예시값)으로 셈 · 출력 이름 → 값.""" + """다른 로직 값 — 받은 설계 값(없으면 예시값)으로 셈 · 출력 이름 → 값.""" other = _load(key) - if callable(other.get("calc")): # 옛 마스터 로직 — 노무비 · 재료비 · 경비 · 계 + if callable(other.get("calc")): # 흐름 없는 로직 — 노무비 · 재료비 · 경비 · 계 got = other["calc"](designs) return {"key": key, "outputs": got["outputs"], "cost": {}, "errors": got["errors"]} - got = dc.run(other, dc._design_values(other, designs), _load, _local(other), [key]) + local = other.get("userElements") or [] + got = dc.run(other, dc._design_values(other, designs), _load, local, [key]) names = {b["id"]: b.get("name") for b in other.get("boxes") or []} outputs = {names[i]: v for i, v in got["outputs"].items() if i in names} return {"key": key, "outputs": outputs, "cost": got["cost"], "errors": got["errors"]} @@ -237,83 +76,7 @@ def design_inputs(key: str) -> dict: return {"key": key, "designs": dc.design_inputs(other)} -# ── 길 ──────────────────────────────────────────────────────────────── -def _call(fn, *args): - try: - return _num(fn(*args)) - except store.StoreError as e: - raise HTTPException(status_code=e.status, detail=e.detail) from e - - -@router.get("/list") -def get_list() -> dict: - return _call(listing) - - -@router.get("/get") -def get_one(key: str) -> dict: - return _call(get, key) - - -@router.post("/save") -def post_save(body: LogicBody) -> dict: - return _call(save, body.logic) - - -@router.post("/delete") -def post_delete(body: KeyBody) -> dict: - return _call(delete, body.key) - - -@router.post("/values") -def post_values(body: ValuesBody) -> dict: - return _call(dv.values, body.items, [*body.userElements, *_users()]) - - -@router.get("/user-elements") -def get_user_elements() -> dict: - return _call(user_elements) - - -@router.post("/user-elements") -def post_user_elements(body: UserBody) -> dict: - return _call(user_edit, body.op, body.element, body.id) - - -# ── 자재 찾기 · 표준 표(정함 5 · 11) ───────────────────────────────────── -STANDARD = HERE / "_standard" - - -class LogicValueBody(BaseModel): - key: str - designs: dict[str, Any] = {} - - -@router.post("/logic-value") -def post_logic_value(body: LogicValueBody) -> dict: - return _call(logic_value, body.key, body.designs) - - -@router.get("/design-inputs") -def get_design_inputs(key: str) -> dict: - return _call(design_inputs, key) - - -class StandardsBody(BaseModel): - unit: dict[str, Any] - money: dict[str, Any] - - -@router.get("/materials") -def get_materials(q: str = "", sub: str = "", detail: str = "", page: int = 1, size: int = 30): - return _call(dv.materials, q, sub, detail, page, size) - - -@router.get("/standards") -def get_standards() -> dict: - return _call(dv.standards, STANDARD) - - +# ── 표준 표 ──────────────────────────────────────────────────────────── def _standards_put(body: dict) -> dict: """고친 표준 표 = 본 프로그램 값 — git 이 따라가는 `_standard/` 파일에 바로(시스템 관리자).""" dv.standards_check(body) @@ -330,6 +93,39 @@ def _standards_reset() -> dict: return dv.standards(STANDARD) +# ── 길 ──────────────────────────────────────────────────────────────── +def _call(fn, *args): + try: + return _num(fn(*args)) + except store.StoreError as e: + raise HTTPException(status_code=e.status, detail=e.detail) from e + + +@router.post("/values") +def post_values(body: ValuesBody) -> dict: + return _call(dv.values, body.items, body.userElements) + + +@router.post("/logic-value") +def post_logic_value(body: LogicValueBody) -> dict: + return _call(logic_value, body.key, body.designs) + + +@router.get("/design-inputs") +def get_design_inputs(key: str) -> dict: + return _call(design_inputs, key) + + +@router.get("/materials") +def get_materials(q: str = "", sub: str = "", detail: str = "", page: int = 1, size: int = 30): + return _call(dv.materials, q, sub, detail, page, size) + + +@router.get("/standards") +def get_standards() -> dict: + return _call(dv.standards, STANDARD) + + @router.put("/standards") def put_standards(body: StandardsBody) -> dict: return _call(_standards_put, body.model_dump()) diff --git a/M02_MasterTemplete/M02_Item_Copy.py b/M02_MasterTemplete/M02_Item_Copy.py index faf70a09d..ba8fec059 100644 --- a/M02_MasterTemplete/M02_Item_Copy.py +++ b/M02_MasterTemplete/M02_Item_Copy.py @@ -211,7 +211,7 @@ def write_file( def _kept_values(values: Any, labor: dict[str, Any]) -> dict[str, Any]: """같은 로직 · 같은 입력 이름만 살림.""" names = { - str(row.get("키")): {spec["이름"] for spec in prices.mf.inputs_of(row)} + str(row.get("키")): {spec["이름"] for spec in row.get("입력", [])} for row in labor.get("로직", []) } out: dict[str, Any] = {} @@ -225,9 +225,18 @@ def _kept_values(values: Any, labor: dict[str, Any]) -> dict[str, Any]: def _kept_picks(picks: Any, labor: dict[str, Any]) -> dict[str, Any]: """같은 로직 · 있는 호표 차례만 살림.""" lines = {str(row.get("키")): len(row.get("호표", [])) for row in labor.get("로직", [])} + slots = { # 흐름 로직 — 고른 자재는 재료 자리 이름(= 입력 이름) + str(row.get("키")): {s["이름"] for s in row.get("입력", [])} + for row in labor.get("로직", []) + if isinstance(row.get("흐름"), dict) + } out: dict[str, Any] = {} for logic, chosen in (picks or {}).items(): - kept = {k: v for k, v in (chosen or {}).items() if int(k) < lines.get(logic, 0)} + kept = { + k: v + for k, v in (chosen or {}).items() + if (str(k).isdigit() and int(k) < lines.get(logic, 0)) or k in slots.get(logic, ()) + } if kept: out[logic] = kept return out diff --git a/M02_MasterTemplete/M02_Item_Prices.py b/M02_MasterTemplete/M02_Item_Prices.py index baa316471..c7ab8fee0 100644 --- a/M02_MasterTemplete/M02_Item_Prices.py +++ b/M02_MasterTemplete/M02_Item_Prices.py @@ -248,7 +248,7 @@ def missing_inputs(snapshot: Any, labor: dict[str, Any], values: dict[str, Any]) except mf.FormulaError: continue mine = values.get(key) or {} - for spec in mf.inputs_of(row): + for spec in row.get("입력", []): got = mine.get(spec["이름"]) if not isinstance(got, dict) or got.get("값") in (None, ""): out.append({"로직": key, "입력": spec["이름"]}) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py index 5c48d3232..91bc32d87 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py @@ -84,7 +84,7 @@ def inputs(key: str) -> dict[str, Any]: for logic in logics: auto = m01.mau.values(whole, logic) specs = [] - for spec in m01.cm.mf.inputs_of(logic): + for spec in logic.get("입력") or []: one = {k: spec[k] for k in _SPEC if k in spec} one.setdefault("설명", None) one.setdefault("단위", None) @@ -126,9 +126,7 @@ def write_file(column: str, doc: dict[str, Any]) -> None: def _known(key: str) -> dict[str, set[str]]: """{로직 키: {입력 이름}} — 그 항목 조합이 지금 담은 것.""" _, logics, _ = combo_logics(_item(key).get("일위대가")) - return { - str(lg.get("키")): {str(s.get("이름")) for s in m01.cm.mf.inputs_of(lg)} for lg in logics - } + return {str(lg.get("키")): {str(s.get("이름")) for s in lg.get("입력") or []} for lg in logics} def read_links(key: str) -> dict[str, Any]: @@ -206,7 +204,7 @@ def pills(key: str, place: str) -> dict[str, Any]: if row.get("자리") != place or row.get("사라짐"): continue logic = by_key[str(row["로직"])] - spec = next(s for s in m01.cm.mf.inputs_of(logic) if s.get("이름") == row["입력"]) + spec = next(s for s in logic.get("입력") or [] if s.get("이름") == row["입력"]) short = combo_store._TAIL.sub("", str(logic.get("이름") or "")) or row["로직"] linked.append( { diff --git a/resources/master_data/scripts/check_master.py b/resources/master_data/scripts/check_master.py index 86ee20f45..9d304d8d8 100644 --- a/resources/master_data/scripts/check_master.py +++ b/resources/master_data/scripts/check_master.py @@ -297,7 +297,7 @@ def check_form(name: str, data: dict) -> list[str]: cols = COLS.get(book) flow = isinstance(row.get("흐름"), dict) # 흐름 로직 — 옛 식 칸 없음 for need in cols or ROW_KEYS[kind]: - if need not in row and not (flow and need in mf.OLD_FIELDS): + if need not in row and not (flow and need in mf.CALC_FIELDS): out.append(f"{where} · 칸 「{need}」 없음") if "출처" in row and not _source_ok(row["출처"]): out.append(f"{where} · 출처 모양 「{row['출처']}」") @@ -498,7 +498,7 @@ def 시험입력(row: dict) -> dict: """일괄 시험 계산용 입력 한 벌 — 고르기 첫 값 · 범위 위끝 · 줄의 「시험입력」 이 이김. 시험 계산 전용 — 정본 계산은 화면이 준 값만 씀.""" given = {} - for spec in mf.inputs_of(row): + for spec in row.get("입력", []): if "고르기" in spec: given[spec["이름"]] = spec["고르기"][0] elif "범위" in spec: diff --git a/resources/master_data/scripts/master_formula.py b/resources/master_data/scripts/master_formula.py index 66ba92833..56b7e5a34 100644 --- a/resources/master_data/scripts/master_formula.py +++ b/resources/master_data/scripts/master_formula.py @@ -36,7 +36,7 @@ _ARITH = re.compile(r"[\d.\s+\-*/()]*\d[\d.\s+\-*/()]*") # 분수·식 값 칸( FLOW_RUN = None # 흐름 로직 입력 = 「설계 때 정할 것」 → 입력 꼴(이름 · 단위 · 고르기 · 범위 · 예시) — 서버가 꽂음 FLOW_INPUTS = None -OLD_FIELDS = ("입력", "중간", "호표", "결과", "덧줄") # 흐름 로직 줄에는 없음(흐름 하나가 정본) +CALC_FIELDS = ("중간", "호표", "결과", "덧줄") # 계산 칸 — 흐름 로직 줄엔 없음(입력은 둠) def _flow_hooks() -> None: @@ -49,11 +49,9 @@ def _flow_hooks() -> None: def inputs_of(row: dict) -> list[dict]: - """로직 입력 — 흐름 로직이면 흐름의 「설계 때 정할 것」 · 아니면 줄의 「입력」.""" - if isinstance(row.get("흐름"), dict): - _flow_hooks() - return FLOW_INPUTS(row) - return row.get("입력", []) + """흐름의 「설계 때 정할 것」 → 입력 꼴(이름 · 단위 · 고르기 · 범위 · 예시).""" + _flow_hooks() + return FLOW_INPUTS(row) class FormulaError(ValueError): @@ -508,7 +506,7 @@ def fill_empty(row: dict, given: dict) -> dict: """화면이 안 보낸 가격 입력(단위에 「원」)은 null 로 채움 — 빈 입력과 같이 그 줄만 비움.""" gap = { s["이름"]: None - for s in inputs_of(row) + for s in row.get("입력", []) if s["이름"] not in given and "원" in str(s.get("단위") or "") } return {**given, **gap} @@ -665,7 +663,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict: # ── 검사 ────────────────────────────────────────────────────────────── def _choices(row: dict, name: str, master: Master): """이름이 가질 수 있는 값 — 고르기 입력, 또는 고르기 입력만 끼운 요소 칸인 중간. 모르면 None.""" - for spec in inputs_of(row): + for spec in row.get("입력", []): if spec["이름"] == name: return spec.get("고르기") for step in row.get("중간", []): @@ -753,7 +751,7 @@ def _check_node(row, node, known: set, master: Master, where: str, out: list, ca except FormulaError as e: out.append(f"{where} · {e}") continue - names = {s["이름"] for s in inputs_of(target)} + names = {s["이름"] for s in target.get("입력", [])} given = {k for k, _ in part[2]} for extra in sorted(given - names): out.append(f"{where} · 「{master.label(part[1])}」 에 입력 「{extra}」 없음") diff --git a/resources/tester/test_m01_logic_flow_engine.py b/resources/tester/test_m01_logic_flow_engine.py index c825a15eb..d3f7be0ac 100644 --- a/resources/tester/test_m01_logic_flow_engine.py +++ b/resources/tester/test_m01_logic_flow_engine.py @@ -9,6 +9,7 @@ from __future__ import annotations +import itertools import shutil from decimal import Decimal from pathlib import Path @@ -134,9 +135,41 @@ def test_흐름_셈_단가는_부른_쪽_자리_프로젝트_사본(client: Test } _save_flow(client, KEY, flow) files = store.cm.load(folder=store.FOLDER) - master = store.cm.mf.run(store.cm.mf.Master(files), KEY, {}) + master = store.cm.mf.run(store.cm.mf.Master(files), KEY, {"길이": 1, "폭": 1}) labor = next(r for d in files.values() for r in d.get("줄") or [] if r.get("키") == "LB000001") assert master["노무비"] == labor["값"] labor["값"] = Decimal(100000) # 프로젝트 사본 노임 - copy = store.cm.mf.run(store.cm.mf.Master(files), KEY, {}) + copy = store.cm.mf.run(store.cm.mf.Master(files), KEY, {"길이": 1, "폭": 1}) assert copy["노무비"] == 100000 and master["노무비"] != 100000 + + +def test_프로젝트에서_고른_자재는_흐름_재료_자리로(client: TestClient) -> None: + """고름(`mp.choosing`) — 흐름 로직은 호표 차례 대신 재료 자리 이름으로 · 고른 품목 단가로 셈.""" + items = (r["키"] for r in dv._group_rows("재료") if not str(r["키"]).startswith("MO")) + first, other = list(itertools.islice((k for k in items if dv._price("재료", k).get("ok")), 2)) + flow = _flow() + el = { + "ref": "", + "name": "자재", + "spec": "", + "unit": "", + "mode": "choice", + "default": first, + "slot": "자재 자리", + } + flow["boxes"].append({"id": "mat", "kind": "input", "name": "재료", "color": "", "unit": "-", "x": 0, "y": 0, + "input": {"mode": "group", "group": "재료", "elements": [el], "variables": []}}) # fmt: skip + flow["boxes"][2]["output"]["cost"] = "재료비" + flow["links"][2] = { + "id": "l3", + "from": {"box": "mat", "name": "자재 자리"}, + "to": {"box": "out", "name": "값"}, + } + _save_flow(client, KEY, flow) + row = store.logic(KEY)["logic"] + assert "자재 자리" in [s["이름"] for s in row["입력"]] # 재료 자리도 입력 약속에 + whole = store.cm.mf.Master(store.cm.load(folder=store.FOLDER)) + given = {"길이": Decimal(1), "폭": Decimal(1), "자재 자리": None} + with store.cm.mf.mp.choosing({"자재 자리": other}): + got = store.cm.mf.run(whole, KEY, given) + assert got["재료비"] == dv._price("재료", other)["value"] diff --git a/resources/tester/test_m01_logic_flow_project.py b/resources/tester/test_m01_logic_flow_project.py index 2c0d8264f..5a4e75951 100644 --- a/resources/tester/test_m01_logic_flow_project.py +++ b/resources/tester/test_m01_logic_flow_project.py @@ -1,10 +1,11 @@ # ruff: noqa: E501 -"""흐름 로직(옛 식 칸 지운 줄)이 조합 · 프로젝트 길에서도 옛 셈과 같음(PLAN 36 · 사용자 결정 · 사본 폴더). +"""흐름 로직이 일위대가 조합 · 프로젝트 길에서도 그대로(PLAN 36 · 사용자 원칙 「조합은 로직을 가져가 더하기만」 · 사본 폴더). -못박는 것 — 옛 로직 하나를 변환해 흐름으로 저장(옛 칸 지움)한 뒤 - - 옛 엔진 `mf.run`(조합 · 프로젝트 길) 합계 = 바꾸기 전 옛 셈 합계 - - 프로젝트 단가 묶음(`M02_Item_Prices.collect`)만으로 셈 = 같은 합계(흐름이 쓰는 요소 · 표 · 다른 로직이 담김) - - 입력 목록(`mf.inputs_of`) = 옛 입력 이름 · 안 준 입력은 「빈 입력」 알림(예시값으로 안 넘어감) +못박는 것 — 조합에 담은 로직을 모두 흐름으로 저장(계산 칸 지움 · 「입력」 은 흐름에서 같은 꼴로)한 뒤 + - 로직마다 입력 목록(이름 · 단위 · 고르기 · 범위) = 바꾸기 전 + - 조합 셈(`master_combo.run`) 로직별 결과 · 비목 합 = 바꾸기 전 + - 프로젝트 단가 묶음(`M02_Item_Prices.collect`)만으로 셈 = 바꾸기 전(흐름이 쓰는 요소 · 표 · 다른 로직이 담김) + - 빈 값으로 준 입력은 「빈 입력」 알림(예시값으로 안 넘어감) · 안 준 입력은 멈춤 """ from __future__ import annotations @@ -22,44 +23,74 @@ from M02_MasterTemplete.DataFlow import M02_DF_Convert as cv REAL = store.FOLDER KEYS = ("GF000185", "GF000219") # 레미콘 검색어 · 펌프차 기계 · 인력 / 표 찾기 · 인력 +COMBO = { + "키": "UA999999", + "이름": "시험 조합", + "담은로직": [{"로직": k, "메모": None} for k in KEYS], +} -@pytest.fixture -def folder(tmp_path: Path, monkeypatch) -> Path: +def _specs(row: dict) -> list: + return [ + (s["이름"], s.get("단위"), s.get("고르기"), s.get("범위")) for s in row.get("입력") or [] + ] + + +def _round(v): + return round(Decimal(str(v)), 6) if v is not None else None + + +def _combo(folder: Path) -> tuple[list, dict]: + mf, mcb = store.cm.mf, store.cm.mcb + given = {k: dict(store.cm.시험입력(store.logic(k)["logic"])) for k in KEYS} + got = mcb.run(mf.Master(store.cm.load(folder=folder)), COMBO, given) + lines = [ + (x["로직"], x["ok"], {k: _round(x.get(k)) for k in ("노무비", "재료비", "경비", "계")}) + for x in got["줄"] + ] + return lines, {k: _round(got.get(k)) for k in ("노무비", "재료비", "경비", "계")} + + +def _bundle_sums() -> dict: + mf = store.cm.mf + snapshot = prices.load_prices(prices.to_json(prices.collect(list(KEYS)))) + out = {} + for k in KEYS: + got = mf.run(snapshot, k, dict(store.cm.시험입력(store.logic(k)["logic"]))) + out[k] = {n: _round(got[n]) for n in ("노무비", "재료비", "경비", "계")} + return out + + +def test_조합에_담은_로직을_흐름으로_바꿔도_같음(tmp_path: Path, monkeypatch) -> None: for path in REAL.glob("*.json"): shutil.copy(path, tmp_path / path.name) monkeypatch.setattr(store, "FOLDER", tmp_path) - return tmp_path - - -def _sums(got: dict) -> dict: - return {k: round(Decimal(str(got[k])), 6) for k in ("노무비", "재료비", "경비", "계")} - - -@pytest.mark.parametrize("key", KEYS) -def test_흐름으로_바꾼_로직_조합_프로젝트_길도_옛_셈과_같음(folder: Path, key: str) -> None: + specs = {k: _specs(store.logic(k)["logic"]) for k in KEYS} + lines, sums = _combo(tmp_path) + assert all(ok for _, ok, _ in lines) and sums["계"] > 0 + bundle = _bundle_sums() + for key in KEYS: + got = store.logic(key) + flow = cv.convert(got["logic"], {"key": key, "master": store.loaded()[1]})["flow"] + flow.pop("key", None) + change = {"op": "edit", "key": key, "row": {**got["logic"], "흐름": flow}} + sflow.save( + [store._dec({"file": got["file"], "version": got["version"], "changes": [change]})] + ) + for key in KEYS: + row = store.logic(key)["logic"] + assert not set(row) & set(store.cm.mf.CALC_FIELDS) and row["흐름"] # 계산 칸만 지움 + assert _specs(row) == specs[key] # 입력 약속 그대로 + assert _combo(tmp_path) == (lines, sums) + assert "_빠진" not in prices.collect(list(KEYS)) + assert _bundle_sums() == bundle mf = store.cm.mf - got = store.logic(key) - old_row = got["logic"] - names = [s["이름"] for s in old_row.get("입력") or []] - given = {k: v for k, v in store.cm.시험입력(old_row).items()} - before = _sums(mf.run(mf.Master(store.cm.load(folder=folder)), key, dict(given))) - flow = cv.convert(old_row, {"key": key, "master": store.loaded()[1]})["flow"] - flow.pop("key", None) - part = { - "file": got["file"], - "version": got["version"], - "changes": [{"op": "edit", "key": key, "row": {**old_row, "흐름": flow}}], - } - sflow.save([store._dec(part)]) - row = store.logic(key)["logic"] - assert not set(row) & set(mf.OLD_FIELDS) - assert [s["이름"] for s in mf.inputs_of(row)] == names - assert _sums(mf.run(mf.Master(store.cm.load(folder=folder)), key, dict(given))) == before - bundle = prices.collect([key]) - assert "_빠진" not in bundle, bundle.get("_빠진") - snapshot = prices.load_prices(prices.to_json(bundle)) - assert _sums(mf.run(snapshot, key, dict(given))) == before # 단가 묶음만으로 같은 합계 - if key == "GF000185": # 빈 입력 — 재료 찾기 자리(표 찾기 빈 조건은 sub7 lookup 몫) - blank = mf.run(snapshot, key, {k: v for k, v in given.items() if k != names[0]}) - assert any(f"「{names[0]}」 빈 입력" in n for n in blank.get("알림") or []) + whole = mf.Master(store.cm.load(folder=tmp_path)) + given = dict(store.cm.시험입력(store.logic(KEYS[0])["logic"])) + first = next(iter(given)) + blank = mf.run( + whole, KEYS[0], {**given, first: None} + ) # 빈 값으로 준 입력 = 그 값을 쓰는 곳만 비움 + assert any(f"「{first}」 빈 입력" in n for n in blank.get("알림") or []) + with pytest.raises(mf.FormulaError, match="없음"): # 안 준 입력 = 멈춤(엔진과 같음) + mf.run(whole, KEYS[0], {k: v for k, v in given.items() if k != first}) diff --git a/resources/tester/test_m01_logic_flow_save.py b/resources/tester/test_m01_logic_flow_save.py index 7a0efd9fd..24571235e 100644 --- a/resources/tester/test_m01_logic_flow_save.py +++ b/resources/tester/test_m01_logic_flow_save.py @@ -72,7 +72,6 @@ def folder(tmp_path: Path, monkeypatch) -> Path: for path in REAL.glob("*.json"): shutil.copy(path, master / path.name) monkeypatch.setattr(store, "FOLDER", master) - monkeypatch.setattr(df, "DATA", tmp_path / "_data") return master @@ -169,7 +168,7 @@ def test_신규_흐름_로직은_자체_GX_복사는_흐름도(client: TestClien saved = _get(client, "/api/m01/logic", key=key)["logic"] assert saved["구분"] == "자체" and saved["흐름"]["name"] == "시험 흐름 로직" assert not set(store.logic(key)["logic"]) & set( - store.cm.mf.OLD_FIELDS + store.cm.mf.CALC_FIELDS ) # 파일 줄에 옛 식 칸 없음 assert ( Decimal(str(_post(client, "/api/m01/calc", {"key": key, "inputs": {}})["sums"]["계"])) @@ -262,7 +261,7 @@ def test_흐름_로직_줄은_옛_식_칸_없음_읽는_곳은_흐름에서(clie mf = store.cm.mf _save_flow(client, KEY, _flow()) row = store.logic(KEY)["logic"] # 파일 줄 그대로 - assert not set(row) & set(mf.OLD_FIELDS) and "끝수" in row and row["흐름"] + assert not set(row) & set(mf.CALC_FIELDS) and "끝수" in row and row["흐름"] view = _get(client, "/api/m01/logic", key=KEY)[ "logic" ] # 화면 보기 = 설계 때 정할 것을 입력 꼴로 diff --git a/resources/tester/test_m02_df_router.py b/resources/tester/test_m02_df_router.py index 686cfc776..676776a66 100644 --- a/resources/tester/test_m02_df_router.py +++ b/resources/tester/test_m02_df_router.py @@ -13,6 +13,7 @@ from __future__ import annotations +import copy import json import shutil import subprocess @@ -26,6 +27,7 @@ from fastapi.testclient import TestClient from common_util.common_util_auth import require_system_admin, verify_session from M01_MasterData import M01_MasterData_Router as m01_router from M01_MasterData import M01_MasterData_Store as store +from M01_MasterData import M01_MasterData_Store_Flow as sflow from M02_MasterTemplete.DataFlow import M02_DF_Router as cf from M02_MasterTemplete.DataFlow import M02_DF_Values as dv @@ -137,14 +139,7 @@ def folder(tmp_path: Path, monkeypatch) -> Path: data = json.loads(host.read_text(encoding="utf-8")) data["표"].append(TABLE) host.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") - sample = tmp_path / "_sample" - sample.mkdir() - one = {"key": None, "name": "견본 돌쌓기", "resultUnit": "원/㎡", "boxes": [], "links": [], - "userElements": []} # fmt: skip - (sample / "견본 돌쌓기.json").write_text(json.dumps(one, ensure_ascii=False), encoding="utf-8") monkeypatch.setattr(store, "FOLDER", master) - monkeypatch.setattr(cf, "DATA", tmp_path / "_data") - monkeypatch.setattr(cf, "SAMPLE", sample) return master @@ -166,13 +161,34 @@ def _d(v) -> Decimal: return Decimal(str(v)) +def _save(logic: dict) -> dict: + """서버 셈(`M02_DF_Compute.run`) — 받는 칸에 값을 되돌린 흐름 · 값(흐름 저장은 M01 로직 줄 「흐름」 칸).""" + got = cf.dc.run(logic, {}, cf._load, logic.get("userElements") or [], [logic.get("key") or ""]) + return {"key": logic.get("key"), "logic": got.pop("filled"), "values": got} + + +def _store(logic: dict) -> str: + """흐름을 마스터 사본의 새 자체 로직 줄 「흐름」 칸에 저장(M01 `/save` 와 같은 길) — 다른 로직 입력이 부름.""" + own = store.FOLDER / "로직_자체.json" + version = store.read(own.name)[1] if own.is_file() else "" + row = { + "키": "", + "이름": logic["name"], + "결과단위": logic.get("resultUnit") or "원", + "흐름": {**logic, "key": None}, + } + done = sflow.save( + [{"file": own.name, "version": version, "changes": [{"op": "add", "row": row}]}] + ) + return done["added"][0]["key"] + + @pytest.mark.skipif(NO_NODE, reason="node 가 없음") def test_저장_서버가_다시_셈_값만_되돌림(client: TestClient, folder: Path) -> None: labor, price = _labor() before = {p.name: p.read_bytes() for p in folder.glob("*.json")} - saved = _post(client, "/api/m02/df/save", {"logic": _logic(labor)}) - assert saved["key"] == "DF-0001" + saved = _save(_logic(labor)) v = saved["values"] labor_cost = price * 2 * 3 * Decimal("1.1") assert _d(v["ports"]["f1"]["노무비"]) == labor_cost @@ -193,27 +209,7 @@ def test_저장_서버가_다시_셈_값만_되돌림(client: TestClient, folder assert [c3[f"{c}1"]["값"] for c in "EFGHIJ"] == HEAD and c3["F3"]["값"] == c3["G3"]["값"] == 4 assert saved["logic"]["links"] == _logic(labor)["links"] - # 열기 · 목록 · 견본 · 고치기 · 지우기 - listed = client.get("/api/m02/df/list").json()["logics"] - assert [(x["key"], x["sample"]) for x in listed] == [("DF-0001", False), ("견본 돌쌓기", True)] - sample = client.get("/api/m02/df/get", params={"key": "견본 돌쌓기"}).json() - assert sample["sample"] is True - assert _post(client, "/api/m02/df/save", {"logic": sample["logic"]})["key"] == "DF-0002" - _post(client, "/api/m02/df/delete", {"key": "견본 돌쌓기"}, want=403) - renamed = {**saved["logic"], "name": "고친 이름"} - assert _post(client, "/api/m02/df/save", {"logic": renamed})["key"] == "DF-0001" - got = client.get("/api/m02/df/get", params={"key": "DF-0001"}).json()["logic"] - assert ( - got["name"] == "고친 이름" and got["boxes"][3]["func"]["sheet"]["칸"]["A1"]["값"] == price - ) - _post(client, "/api/m02/df/delete", {"key": "DF-0002"}) - assert client.get("/api/m02/df/get", params={"key": "DF-0002"}).status_code == 404 - _post(client, "/api/m02/df/save", {"logic": _logic(labor, "DF-0009")}, want=404) - _post(client, "/api/m02/df/save", {"logic": {**_logic(labor), "name": " "}}, want=400) - _post(client, "/api/m02/df/save", {"logic": {"name": "x"}}, want=400) - assert {p.name: p.read_bytes() for p in folder.glob("*.json")} == before # 마스터 사본 그대로 - assert sorted(p.name for p in cf.DATA.glob("*.json")) == ["DF-0001.json"] @pytest.mark.skipif(NO_NODE, reason="node 가 없음") @@ -221,7 +217,7 @@ def test_빈_입력_값은_쉬운_말(client: TestClient) -> None: labor, _ = _labor() logic = _logic(labor) logic["boxes"][0]["input"]["elements"][0]["ref"] = "LB999999" - v = _post(client, "/api/m02/df/save", {"logic": logic})["values"] + v = _save(logic)["values"] assert v["errors"] and "석공" in v["errors"][0] @@ -278,23 +274,17 @@ def test_입력_값_그룹마다(client: TestClient) -> None: assert got[k]["ok"] is False and got[k]["reason"], got[k] -def test_사용자_요소_만들기_지우기(client: TestClient, folder: Path) -> None: +def test_사용자_요소는_흐름_안_것만(client: TestClient, folder: Path) -> None: before = {p.name: p.read_bytes() for p in folder.glob("*.json")} - url = "/api/m02/df/user-elements" - a = _post(client, url, {"op": "add", "element": {"name": "가", "group": "재료", "value": 1.5}}) - b = _post(client, url, {"op": "add", "element": {"name": "나", "unit": "회"}}) - assert [a["element"]["id"], b["element"]["id"]] == ["UE-0001", "UE-0002"] - assert b["element"]["group"] == "재료" and b["element"]["value"] is None - _post(client, url, {"op": "add", "element": {"name": " "}}, want=400) - _post(client, url, {"op": "add", "element": {"name": "다", "value": "x"}}, want=400) - _post(client, url, {"op": "add", "element": {"name": "라", "group": "모름"}}, want=400) - _post(client, url, {"op": "delete", "id": "UE-0001"}) - _post(client, url, {"op": "delete", "id": "UE-0001"}, want=404) - assert [e["id"] for e in client.get(url).json()["elements"]] == ["UE-0002"] - got = _post( - client, "/api/m02/df/values", {"items": [{"id": "x", "group": "재료", "ref": "UE-0002"}]} - ) - assert got["values"][0]["ok"] is False # 값이 비어 있으면 쉬운 말 + mine = [{"id": "UE-0002", "group": "재료", "name": "나", "spec": "", "unit": "회", "value": None}, + {"id": "UE-0003", "group": "재료", "name": "다", "spec": "", "unit": "개", "value": 2.5}] # fmt: skip + items = [ + {"id": "x", "group": "재료", "ref": "UE-0002"}, + {"id": "y", "group": "재료", "ref": "UE-0003"}, + ] + got = _post(client, "/api/m02/df/values", {"items": items, "userElements": mine})["values"] + assert got[0]["ok"] is False and _d(got[1]["value"]) == Decimal("2.5") # 빈 값은 쉬운 말 + assert client.get("/api/m02/df/user-elements").status_code in (404, 405) # 공용 목록 길 없음 assert {p.name: p.read_bytes() for p in folder.glob("*.json")} == before # 마스터에 안 씀 @@ -305,11 +295,10 @@ def test_시스템_관리자가_아니면_403(folder: Path) -> None: ) app.dependency_overrides[verify_session] = lambda: {"role": "USER", "company_id": 1} client = TestClient(app) - assert client.get("/api/m02/df/list").status_code == 403 assert client.post("/api/m02/df/values", json={"items": []}).status_code == 403 - assert client.get("/api/m02/df/user-elements").status_code == 403 + assert client.get("/api/m02/df/design-inputs", params={"key": "GF000219"}).status_code == 403 app.dependency_overrides[verify_session] = lambda: {"role": "SYSTEM_ADMIN", "company_id": None} - assert [x["key"] for x in client.get("/api/m02/df/list").json()["logics"]] == ["견본 돌쌓기"] + assert client.post("/api/m02/df/values", json={"items": []}).json() == {"values": []} @pytest.mark.skipif(NO_NODE, reason="node 가 없음") @@ -353,9 +342,8 @@ def test_기계_키_칸_꼴과_시간당_경비(client: TestClient) -> None: body = {"key": None, "name": "기계 견본", "resultUnit": "원/시간", "boxes": [box, f1, o1], "links": [_link("a", "m1", "기계 · 시간당 경비", "f1", "단가"), _link("b", "f1", "경비", "o1", "값")], "userElements": []} # fmt: skip - saved = _post(client, "/api/m02/df/save", {"logic": body}) + saved = _save(body) assert _d(saved["values"]["cost"]["경비"]) == 2 * _d(want) - _post(client, "/api/m02/df/delete", {"key": saved["key"]}) # ── 35-9 넓힘 — 재료 셋 · 자재 찾기 · 다른 로직 · 설계 때 정할 것 · 선 규칙 · 끝수 · 표준 표 ───── @@ -450,7 +438,7 @@ def _area(x: int = 100) -> dict: @pytest.mark.skipif(NO_NODE, reason="node 가 없음") def test_다른_로직_값_설계_값_순환(client: TestClient) -> None: - a = _post(client, "/api/m02/df/save", {"logic": _area()})["key"] + a = _store(_area()) assert _post(client, "/api/m02/df/logic-value", {"key": a})["outputs"] == {"결과": 200} assert _post(client, "/api/m02/df/logic-value", {"key": a, "designs": {"면적": 5}})[ "outputs" @@ -467,7 +455,7 @@ def test_다른_로직_값_설계_값_순환(client: TestClient) -> None: "links": [_link("a", "L", "결과", "o", "값")], "userElements": [], } # fmt: skip - saved = _post(client, "/api/m02/df/save", {"logic": b}) + saved = _save({**b, "key": _store(b)}) assert ( saved["values"]["ports"]["L"] == {"결과": 200} and _d(saved["values"]["cost"]["경비"]) == 200 @@ -478,9 +466,9 @@ def test_다른_로직_값_설계_값_순환(client: TestClient) -> None: assert [(d["box"], d["port"], d["kind"]) for d in designs] == [("L", "면적", "다른 로직")] # 순환 — 로직이 자기를 부름 - loop = {**b, "key": saved["key"]} + loop = copy.deepcopy({**b, "key": saved["key"]}) loop["boxes"][0]["input"]["logic"] = {**ref, "key": saved["key"]} - got = _post(client, "/api/m02/df/save", {"logic": loop})["values"] + got = _save(loop)["values"] assert any("서로를 부름" in e for e in got["errors"]) @@ -526,7 +514,7 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien ], "userElements": [], } # fmt: skip - got = _post(client, "/api/m02/df/save", {"logic": lg}) + got = _save(lg) v = got["values"] assert v["ports"]["m"] == { "관 자리": 30000, @@ -542,21 +530,18 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien "식": "A1+A2+A3" } # 받는 칸에 값만 - designs = client.get("/api/m02/df/design-inputs", params={"key": got["key"]}).json()["designs"] + key = _store(lg) # 로직 줄 「흐름」 칸에 저장한 뒤 길로 읽음 + designs = client.get("/api/m02/df/design-inputs", params={"key": key}).json()["designs"] assert [(d["port"], d["kind"]) for d in designs] == [ ("돌", "변수"), ("높이", "변수"), ("관경", "변수"), ("골라 쓰는 관", "재료 자리")] # fmt: skip lg["boxes"][0]["input"]["variables"][1]["example"] = 3 # 「~3」 줄(위 끝 포함) - assert _d( - _post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"] - ) == int(Decimal("0.5") + 33000 + 3) + assert _d(_save(lg)["values"]["ports"]["f"]["합"]) == int(Decimal("0.5") + 33000 + 3) lg["boxes"][0]["input"]["variables"][0]["example"] = ( "견치돌" # 「높이 4」 줄 = 아래 = 위 → 같음(정함) ) lg["boxes"][0]["input"]["variables"][1]["example"] = 4 - assert _d( - _post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"] - ) == int(Decimal("0.7") + 33000 + 4) + assert _d(_save(lg)["values"]["ports"]["f"]["합"]) == int(Decimal("0.7") + 33000 + 4) lookup = { "kind": "lookup", "col": "석공", @@ -570,7 +555,7 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien ] lg["boxes"][0]["input"]["variables"][2]["example"] = "D999" # 맞는 재료 없음 lg["boxes"][0]["input"]["variables"][0]["example"] = "없는돌" # 표에 없는 조건 → 함수 멈춤 - bad = _post(client, "/api/m02/df/save", {"logic": lg})["values"] + bad = _save(lg)["values"] assert any("맞는 재료 없음" in e for e in bad["errors"]) and any( "표에 없는 조건" in e for e in bad["errors"] )