fix(778-2): 못옮김 기계 잇기 = 기계경비 기초 로직 줄 · 목록 밖 기계 셈 문맥 고침

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 11:54:09 +09:00
co-authored by Claude Opus 5.5
parent 560e2e0928
commit 026f6c667a
4 changed files with 103 additions and 15 deletions
+12 -3
View File
@@ -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:")]
@@ -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)
+13 -8
View File
@@ -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)
@@ -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("까닭")