From 026f6c667a24e265401b3cad316f6635c028300f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 4 Nov 2026 11:54:09 +0900 Subject: [PATCH] =?UTF-8?q?fix(778-2):=20=EB=AA=BB=EC=98=AE=EA=B9=80=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=EC=9E=87=EA=B8=B0=20=3D=20=EA=B8=B0?= =?UTF-8?q?=EA=B3=84=EA=B2=BD=EB=B9=84=20=EA=B8=B0=EC=B4=88=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A4=84=20=C2=B7=20=EB=AA=A9=EB=A1=9D=20=EB=B0=96?= =?UTF-8?q?=20=EA=B8=B0=EA=B3=84=20=EC=85=88=20=EB=AC=B8=EB=A7=A5=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SkPatch LINK 기계 = 로직 · _Plan.link: EQ 키 → 기계경비 기초(그 기계를 고르기에 둔 기초 먼저) + 입력 기계 = 원문번호 · EQ 키 바로 더한줄 셈 오류 없앰 - master_pick resolve · machine · master_formula _inputs: 셈에 넘어온 마스터로 기계 찾음 — 까닭 = mp.EQ ContextVar 는 마스터를 지은 문맥에만 있어 서버 둘째 요청(스레드 풀 · 새 문맥)부터 목록 밖 기계(굴착기(타이어) 0.6㎥ 등)가 「고르기 밖」으로 막힘 · ContextVar 는 대신 길로 남김 - 새 시험 test_778_2_hopyo_machine_link(잇기 뒤 SK 셈 · 서버 두 요청 · 문맥 밖 스레드) - 여덟 파일 기초 80 · SK 37 · 고름 대기 89 그대로 · SK 셈 오류 0 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_SkPatch.py | 15 +++- .../master_data/scripts/master_formula.py | 8 +- resources/master_data/scripts/master_pick.py | 21 ++++-- .../tester/test_778_2_hopyo_machine_link.py | 74 +++++++++++++++++++ 4 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 resources/tester/test_778_2_hopyo_machine_link.py diff --git a/M02_MasterTemplete/M02_Hopyo_SkPatch.py b/M02_MasterTemplete/M02_Hopyo_SkPatch.py index bfc446977..2d402d67e 100644 --- a/M02_MasterTemplete/M02_Hopyo_SkPatch.py +++ b/M02_MasterTemplete/M02_Hopyo_SkPatch.py @@ -9,7 +9,8 @@ 기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄 자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음) 못옮김 = `[{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량(엑셀 1 단위 품), 글, 후보}]` · 사용자 `잇기` - `[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음. + `[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 재료 · 기계(EQ = 기계경비 기초 로직 줄 · 778-2) + (엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음. SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비 둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」). 고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초) · @@ -49,7 +50,7 @@ LOOKS = ("같음", "품다름", "엑셀만", "기초만") # 줄비교 꼴(746-1 COMPARE = 3 # 고름 대기 후보 앞 몇에 줄비교 LINK = { "노임": ("인력", "LB"), - "기계": ("기계", "EQ"), + "기계": ("로직", "EQ"), # EQ = 기계경비 기초 로직 줄 + 기계 입력(778-2) "자재": ("재료", "MT"), } # 잇기 칸 → 더한줄 · 키 머리 @@ -228,9 +229,17 @@ class _Plan: raise LinkError(f"「{got['id']}」 · 없는 키 「{key}」") from e self.lost.remove(got) name = str(elem.get("이름") or got["이름"]) + qty = _num(float(got["수량"]) * self.tb) + if kind == "로직": # EQ 키 바로 = 셈 못 함 → 기계경비 기초(기계 = 그 EQ) + kinds = mtypes(f"{elem.get('상세구분')} {elem.get('이름')}") + code = _mach_logics(self.whole, kinds, elem)[0] + name = f"{name} {elem.get('규격') or ''}".strip() + self.add({"종류": kind, "로직": code, "입력값": {MACH_IN: elem.get("원문번호")}, + "이름": name, "단위": got["단위"] or "시간", "수량": qty}) # fmt: skip + continue unit = got["단위"] or ("인" if kind == "인력" else str(elem.get("단위") or "")) self.add({"종류": kind, "요소": key, "이름": name, "단위": unit, - "수량": _num(float(got["수량"]) * self.tb)}) # fmt: skip + "수량": qty}) # fmt: skip def labor(self, excel: dict[str, float]) -> None: mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")] diff --git a/resources/master_data/scripts/master_formula.py b/resources/master_data/scripts/master_formula.py index ea8ddfa99..04e849bec 100644 --- a/resources/master_data/scripts/master_formula.py +++ b/resources/master_data/scripts/master_formula.py @@ -512,7 +512,7 @@ def fill_empty(row: dict, given: dict) -> dict: return {**given, **gap} -def _inputs(row: dict, given: dict) -> dict: +def _inputs(row: dict, given: dict, master=None) -> dict: env = {} for spec in row.get("입력", []): name = spec["이름"] @@ -529,8 +529,8 @@ def _inputs(row: dict, given: dict) -> dict: pass if isinstance(value, (int, float)): value = Decimal(str(value)) - value = mp.resolve(spec, value) # 기계 EQ 키 → 원문 분류번호 - if "고르기" in spec and value not in spec["고르기"] and not mp.machine(spec, value): + value = mp.resolve(spec, value, master) # 기계 EQ 키 → 원문 분류번호 + if "고르기" in spec and value not in spec["고르기"] and not mp.machine(spec, value, master): raise FormulaError(f"입력 「{name}」={value} 고르기 밖") if "범위" in spec: low, high = spec["범위"] @@ -567,7 +567,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict: if isinstance(row.get("흐름"), dict): # 흐름 로직 — 깔림에 기대지 않고 스스로 깖 _flow_hooks() return FLOW_RUN(master, ref, row, given) - env = _inputs(row, given) + env = _inputs(row, given, master) for step in row.get("중간", []): try: env[step["이름"]] = evaluate(parse(step["식"]), env, master, depth) diff --git a/resources/master_data/scripts/master_pick.py b/resources/master_data/scripts/master_pick.py index b28ff3453..6b9b38599 100644 --- a/resources/master_data/scripts/master_pick.py +++ b/resources/master_data/scripts/master_pick.py @@ -33,22 +33,27 @@ def choosing(picks: dict | None): CHOSEN.reset(token) -def _numbers() -> dict: - return EQ.get().numbers.get("EQ", {}) if EQ.get() else {} +def _eq(master=None): + """기계를 찾을 마스터 — 셈에 넘어온 것(기계 줄 있음) › 지은 문맥의 것(`EQ` · 다른 스레드는 빔).""" + return master if (getattr(master, "numbers", None) or {}).get("EQ") else EQ.get() -def resolve(spec: dict, value): +def _numbers(master=None) -> dict: + return _eq(master).numbers.get("EQ", {}) if _eq(master) else {} + + +def resolve(spec: dict, value, master=None): """기계 입력이 EQ 키로 오면 원문 분류번호로 풂 — 고르기 목록(원문번호)과 같은 꼴.""" - if not (isinstance(value, str) and value.strip()[:2] == "EQ" and EQ.get()): + if not (isinstance(value, str) and value.strip()[:2] == "EQ" and _eq(master)): return value - row = EQ.get().index.get("EQ", {}).get(value.strip()) + row = _eq(master).index.get("EQ", {}).get(value.strip()) number = str(row.get("원문번호") or "") if row else "" - return number if number and machine(spec, number) else value + return number if number and machine(spec, number, master) else value -def machine(spec: dict, value) -> bool: +def machine(spec: dict, value, master=None) -> bool: """기계 입력 — 고르기 목록 밖 원문 분류번호도 기계 마스터에 있으면 받음.""" - known = _numbers() + known = _numbers(master) picks = spec.get("고르기") or [] return bool(picks) and str(value).strip() in known and all(str(o) in known for o in picks) diff --git a/resources/tester/test_778_2_hopyo_machine_link.py b/resources/tester/test_778_2_hopyo_machine_link.py new file mode 100644 index 000000000..4fa102c4a --- /dev/null +++ b/resources/tester/test_778_2_hopyo_machine_link.py @@ -0,0 +1,74 @@ +"""못옮김 기계 잇기 = 기계경비 기초 로직 줄(PLAN 778-2 · 분석 621-B 4-2). + +EQ 키 바로 더한줄은 셈 오류 → 사용자 잇기 `{id, 기계: EQ…}` = SK 더한줄 +`{종류: 로직, 로직: 기계경비 기초, 입력값: {기계: 기계 번호}}` · SK 셈 됨(그 기계 시간당 기계경비 × +엑셀 시간 × 배) · 갈래 다른 키 · 없는 키 = LinkError. 고르기 목록 밖 기계 = 서버 둘째 요청 · +지은 문맥 밖 스레드에도 셈 됨(마스터를 셈에 넘김). master 읽기만. +""" + +import threading + +import pytest + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Combine as combine +from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch +from M02_MasterTemplete import M02_Sangeun_Calc as calc +from M02_MasterTemplete import M02_Sangeun_Router as sangeun_router +from resources.tester.test_m02_hopyo_excel import client # noqa: F401 + +BASE = "GC000267" # 바탕 아무 기초(시간 단위) — 셈만 봄 + + +def _plan(whole) -> skpatch._Plan: + doc = {"키": "SK-000000", "이름": "견줌", "단위": "시간", "바탕": {"로직": BASE}, + "입력값": {}, "덮개": {}, "뺀줄": [], "자재표시": {}, "더한줄": []} # fmt: skip + work = skpatch._Plan(doc, whole) + one = {"id": "기계:굴착기", "갈래": "기계", "이름": "굴착기", "규격": "0.6", "단위": "시간", + "수량": 0.25, "글": "엑셀만 장비 — 기계 못 찾음", "후보": []} # fmt: skip + work.lost = [one, {**one, "id": "기계:굴착기#1"}] + return work + + +def test_기계_잇기_뒤_SK_셈_됨(): + whole = m01.loaded()[1] + work = _plan(whole) + work.link([{"id": "기계:굴착기", "기계": "EQ000026"}]) + add = work.doc["더한줄"][-1] + assert add["종류"] == "로직" and add["로직"] in combine.MACHINE_SET - { + combine.MACHINE["굴착기"] + } + assert add["입력값"] == {skpatch.MACH_IN: "0211-0060"} and "요소" not in add + assert add["수량"] == pytest.approx(0.25 * work.tb) and add["단위"] == "시간" + ran = calc.run(work.doc, whole) # 전 = 「Decimal × dict」 셈 오류 + line = next(x for x in ran["줄"] if x.get("cid") == "add:#0") + assert float(line["금액"]) > 0 + one = {"종류": "로직", "로직": add["로직"], "입력값": add["입력값"], "수량": 1} + hour = calc.run({**work.doc, "바탕": None, "더한줄": [one]}, whole)["기준"] + assert float(hour["경비"]) == 27120 # 굴착기(타이어) 0.6㎥ 시간당 경비(엑셀 14-6 과 같음) + # 무한궤도 = 굴착기(무한궤도) 기계경비 · 갈래 다른 키 · 없는 키 = 틀림 + work.link([{"id": "기계:굴착기#1", "기계": "EQ000019"}]) + assert work.doc["더한줄"][-1]["로직"] == combine.MACHINE["굴착기"] + work.lost = _plan(whole).lost + with pytest.raises(skpatch.LinkError): + work.link([{"id": "기계:굴착기", "기계": "MT000010"}]) + with pytest.raises(skpatch.LinkError): + work.link([{"id": "기계:굴착기", "기계": "EQ999999"}]) + + +def test_목록_밖_기계_서버_둘째_요청에도_셈(client): # noqa: F811 + m01.loaded() # 마스터 = 이 스레드에서 지음 · 요청 = 다른 스레드(문맥 안 따라감) + client.app.include_router(sangeun_router.router) + one = {"종류": "로직", "로직": "GC000268", "입력값": {skpatch.MACH_IN: "0211-0060"}, + "이름": "굴착기(타이어) 0.6㎥", "단위": "시간", "수량": 1} # fmt: skip + doc = {"키": "SK-000000", "이름": "t", "단위": "시간", "바탕": None, "입력값": {}, "덮개": {}, + "뺀줄": [], "자재표시": {}, "더한줄": [one]} # fmt: skip + for _ in range(2): + got = client.post("/api/m02/layers/personal/sangeun/calc", json={"문서": doc}) + assert got.status_code == 200, got.text + assert got.json()["기준"]["경비"] == 27120, got.json().get("까닭") + whole, out = m01.loaded()[1], {} # 지은 문맥 밖 스레드(서버 요청 스레드 풀과 같음) + run = threading.Thread(target=lambda: out.update(calc.run(doc, whole))) + run.start() + run.join() + assert out["기준"] and float(out["기준"]["경비"]) == 27120, out.get("까닭")