feat(785-5): 만들기 · 길 — 확인한 고른 것 → SC · SK · 장부 · 새 M02_Hopyo_Router · 판정 · 검산 · 옮기기

- 새 M02_Hopyo_Router — 옛 hopyo 길 모두 M02_Labor_Router 에서 옮김(옛 화면 785-7 까지 돎) · 새 길 hopyo-judge 목록 · 호표 · 블록 · hopyo/{키}/check · hopyo/{키}/move · main 등록 · 시동 warm · Fit.ensure 걷음 · 올림 때 엑셀 사전(Row.lexicon) · 블록 풀이 뒤 셈 · 끝내기 때 저장본 지움
- 새 M02_Hopyo_Judge — 1차 줄 판(Row) + 단산 줄 2차 풀이(Clause → Solve) · 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록 금액 · 절 몫 · 블록 풀이 파생 저장본(HP 폴더 _derived · 입력 지문 · 지문 다를 때만) · judged(folder, key) · 검산(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움 · 맡은 절 단위 줄 · 몫과 품 대조 · 금액 = 일위대가 로직 줄과 같은 셈)
- 새 M02_Hopyo_Make — 고른 것 → SC 줄(같은 별칭 · 줄 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫 × 배 · 같은 기초 둘째 = 한 줄 SC 사본 · 별도계상 자재 · 이음 · 율 · 고정 · 참조) · SK 더한줄(바탕 없음 · SkUnit.adds · Sangeun.Maker.write) · 하위 먼저 · 입력 필요 · 단위 환산 못 정함 = 그 줄(블록)만 막힘 · 장부 고른 것 꼴 짝 · 이미 옮긴 것 = 덮음 없으면 409(남의 SC 덮지 않음) · 확인한 줄 몸(Lines) = Build 에서 옮김
- Build 나눔 — 모두 고정 길 · Builder · _order · _origins · build_all 그대로(B09 빌림) · 확인한 줄 몸 → Make.Lines
- SkUnit · Sangeun — SkPatch · Fit · Basis 부름 걷음(기계경비 기초 차례 SkUnit 으로 · 근거 꼬리표 = Clause.basis · 맞춤 셈 409 없앰 · 재료 고름 MO 됨) · Kids 가 _same_mark 가짐
- 시험 새 test_642_5_make(판정 · 검산 · 옮기기 · 예시 1 · 2 · 3 · 덮음 409 · 막는 길) · test_m02_hopyo_excel 마당에 새 Router 더함 · HOPYO_MAX_BYTES 자리(길 옮긴 까닭)
- 낡은 시험 고침: test_718_1 · test_729 「맞춤 셈 전 409」 뺌(맞춤 뒤 셈 안 기다림) · test_628 맞춤셈 캐시 넣기 뺌(근거 = 조항 색인) · test_729 근거 없음 꼴 · 고정 이음 유가 칸(785-3 LINKS 늘어난 까닭)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 23:13:53 +09:00
co-authored by Claude Opus 5.5
parent b2c2bb7ad3
commit a22af84b62
14 changed files with 1630 additions and 556 deletions
+16 -73
View File
@@ -33,6 +33,8 @@
`{까닭, 남은줄}`(저장 안 함 · `save` 거짓 = 막지 않고 `남은줄` 만) · 미리 보기 = `unlinked`(쓰기 없음).
수량(742-1 · 요구 584-1) = 엑셀 수량을 줄 · 자재 저장 수량(`{손}`)으로 · 같은 값 `본뜬.실무수량 {줄: {차례: 수},
자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `M02_Hopyo_Compare` — 하위 호표 것까지 `excel_given`).
785-5 — 확인한 줄 몸 = `M02_Hopyo_Make.Lines`(옛 대상 꼴 `put` · 새 고른 것 꼴 `picks` = 몸 `{차례: {고른}}`) ·
여기는 모두 고정 길 · 줄 차례 · 쓰기(`Builder` · `_order` · `_origins` · `build_all` = B09 빌림).
"""
from __future__ import annotations
@@ -234,82 +236,17 @@ class Builder:
self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]], save: bool = True
) -> dict:
"""`save` 거짓 = 사전조합 안 씀 · `{문서, 빠진줄, 줄수}`(704-4 [구조물 조합으로])."""
from M02_MasterTemplete import M02_Hopyo_Make as hmake # 확인한 줄 몸(785-5)
stamp = utc_now_text()
hopyo, file = self.hopyo, self.doc.get("파일") or ""
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
nation: dict[tuple, dict[str, Any]] = {}
taken, linker, poomsem, kids, kid_n = set(), hfix.Linker(self.strict and save), [], set(), 0
left: list[dict[str, Any]] = []
where = {"파일": file, "호표": self.no}
rows = hopyo.get("줄") or []
def put(line: dict[str, Any], one: dict[str, Any], alias: str | None) -> None:
if "이음" in one:
others = [x for x in rows if x is not line]
lines.append(hfix.line_of(line, others, where, taken, linker, one["이음"]))
return
qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
memo = _memo(line, one)
if "전국" in one:
at = (one["이름"], one["규격"], one["단위"])
have = nation.get(at)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
k: have[k] for k in labor.MARKS if k in have
})} # fmt: skip
missing.append(_missing(line, NATIONAL))
return
if "자재" in one:
have = mats.get(one["자재"])
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
name = self.mat_names.get(one["자재"], "")
mats[one["자재"]] = {
"코드": one["자재"],
"수량": {"손": _num(hand)},
**(
have
and {k: have[k] for k in labor.MARKS if k in have}
or hmat.marks(line, name)
),
}
if not have:
mat_lines[one["자재"]] = line
return
if "호표" in one:
kids.add(one["호표"])
ref = {
"층": refs.here()[0],
"주인": refs.here()[1],
"키": self.done.get(mark(self.key, one["호표"]), "SC-미리"),
}
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
return
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
kept, why = sklink.kept(self, line, one, qty, alias, memo)
if why:
left.append(sklink.left(line, why))
if kept is not None:
lines.append(kept)
return
if one["로직"] not in used:
used.add(one["로직"])
lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}})
return
at = int(line.get("차례", 0))
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
"이름": f"{hopyo['이름']} — {line.get('이름')}",
"단위": line.get("단위") or None,
"구분": ORIGIN,
"상세구분": file,
"담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}],
"자재": [],
"본뜬": self._origin(f"{mark(self.key, self.no, at)} {one['로직']}", stamp),
}
ref = (
{"키": "SC-미리"} if self.dry else hkids.save(wrap, self.same)[0]
) # 미리 보기 = 쓰기 없음
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
where = {"파일": file, "호표": self.no}
body = hmake.Lines(self, rows, where, stamp, hfix.Linker(self.strict and save))
lines, mats, missing, left = body.lines, body.mats, body.missing, body.left
taken, linker, kids, put = body.taken, body.linker, body.kids, body.put
nation, mat_lines = body.nation, body.mat_lines
poomsem, kid_n = [], 0
book: dict[str, dict[str, Any]] = {}
if whole:
@@ -318,6 +255,12 @@ class Builder:
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
for at, line in enumerate(rows):
got = checked.get(at)
if got and "고른" in got: # 새 길 고른 것(785-5 · Make 가 검사 · 다시 셈)
poomsem.append(at)
body.picks(at, line, got["고른"])
pairs = [hmake.ledger_pick(p) for p in got["고른"]]
book[ledger.key(line)] = self._row(line, "짝", pairs)
continue
if whole:
missing.append(_missing(line, WHOLE))
continue
+340
View File
@@ -0,0 +1,340 @@
"""M02 엑셀 불러오기 판정 · 검산(PLAN 785-5 · 요구 642 · 설계 642-A 2-6 ⑤ · 2-9) — 서버 단독 · 읽기만.
판정 = 1차 줄 판(`M02_Hopyo_Row.rows`) + 단산 줄 2차 블록 풀이(`Clause.basis` 후보 → `Solve.parts` →
`Solve.block`) · 목록(`listing`) · 호표 하나(`hopyo`) · 블록 하나(`block`).
블록 풀이 = 파생 저장본(HP 폴더 `_derived/hopyo_judge_{키}` · 입력 지문 = HP + M01 파일 내용 + 풀이 판 ·
지문 다를 때만 다시 셈 · GET 은 안 지움 · 올림 뒤 한 번 뒤에서 `ensure` · 셈 중 = `셈중` · 판정 None) ·
블록 판정 읽기 = `judged`(Marks 칠) · [끝내기] = `forget`.
줄 판 줄에 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록에 엑셀 금액 · 절에 몫(785-6 화면이 읽음).
검산(`check`) = 고른 것마다 — 기초 = 입력값(`inputs` · 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종
비움) · 입력필요 · 엑셀 단위 줄과 품 대조(`Solve.pair`) · 금액(엔진 한 번 · 입력 필요면 셈 안 함) /
그 밖 = 「확인」 · 수량 = 엑셀 × 몫 × 배(`Row.quantity` · 사용자 배 우선).
저장 없음 · 지어 넣은 값 없음(입력 필요 칸은 비워 둠 · 안 쓰는 칸 = 기초 기본값).
"""
from __future__ import annotations
import shutil
import threading
from collections import Counter
from decimal import Decimal
from pathlib import Path
from typing import Any
from common_util import common_util_derived_store as derived
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Clause as clause
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Row as hrow
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_Solve as solve
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
mf, ma = hb.mf, hb.ma
EXCEL = ("이름", "규격", "단위", "단가")
CHECKED = "확인"
KIND = "hopyo_judge_" # 파생 저장본 종류 = 이것 + HP 키(HP 폴더 `_derived/`)
VERSION = "785-5.1" # 풀이 꼴이 바뀌면 올림
_LOCK, _RUNNING = threading.Lock(), set()
def master() -> mf.Master:
return hb.m01.cm.master()
def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
def _blocks(book: dict[str, Any]) -> dict[int, dict[str, Any]]:
return {int(b["번"]): b for b in book.get("단산") or [] if b.get("번") is not None}
def _unit(book: dict[str, Any], no: int) -> dict[str, Any]:
got = next((h for h in book.get("호표") or [] if int(h["번"]) == int(no)), None)
if got is None:
raise _bad(f"없는 호표 「제{no}호표」", 404)
return got
def _made(folder: Path, key: str, to: Path) -> tuple[dict[str, str], dict[str, str]]:
"""(만든 SC {본뜬 키: SC}, 만든 SK {본뜬 키: SK}) — 만들 층 · 이 파일 층 것만."""
source = refs.where(folder)
return hbuild.confirmed(to, source), sangeun.made(to, key, source)
# ── 기초 입력값 ───────────────────────────────────────────────────────
def inputs(m: mf.Master, code: str, given: dict[str, Any] | None) -> tuple[dict, list[str]]:
"""(저장 입력값, 입력필요) — 고른 값(그 기초 칸 · 빈 값 뺌) + 안 쓰는 칸 = 기초 기본값 ·
쓰는데 빈 칸 = 입력필요(비워 둠) · 지역 · 자재지역 · 조종원직종 = 비움(프로젝트가 정함)."""
row = m.logic(code)
names = [s["이름"] for s in hb.specs_of(row)]
have = {
k: v
for k, v in (given or {}).items()
if k in names and k not in hb.SKIP and v not in (None, "")
}
need = [n for n in names if n in hb._needed(m, code, row, have) and n not in hb.SKIP]
base = ma.defaults(m, row)
idle = [n for n in names if n not in have and n not in need and n not in hb.SKIP]
full = {n: hb.plain(base[n]) for n in idle if base.get(n) is not None}
return {**full, **{k: hb.plain(v) for k, v in have.items()}}, need
def money(m: mf.Master, code: str, given: dict[str, Any]) -> dict[str, Any]:
"""기초 한 줄 셈(일위대가 로직 줄과 같은 길 `Labor_Doc.run_line` · 끝수 · 비운 지역 칸 = 자동값 ·
참고 셈만) → {노무비, 재료비, 경비, 계} · 못 셈 = {계 None, 까닭}."""
got = labor.run_line(m, code, given)
if got.get("결과") is not None:
return {"결과": hb.plain(got["결과"])}
if got.get("기준") is None:
return {"계": None, "까닭": got.get("까닭")}
return {k: hb.plain(v) for k, v in got["기준"].items()}
# ── 판정 ─────────────────────────────────────────────────────────────
def _row(line: dict[str, Any], one: dict[str, Any]) -> dict[str, Any]:
return {**one, **{k: line.get(k) for k in EXCEL}}
def _solve(m: mf.Master, blk: dict, line: dict | None, same: list[dict]) -> dict[str, Any]:
"""블록 풀이 — 조항 색인 후보 → 절 → 검산 · 판정 + 근거 · 엑셀 금액 · 절 몫."""
basis = clause.basis(blk)
parts = solve.parts(blk, basis["후보"])
got = solve.block(m, blk, parts, {"줄": line, "같은줄": same})
for sec, part in zip(got["절"], parts, strict=True):
share = part.get("몫")
sec["몫"] = share.get("값") if isinstance(share, dict) else share
sec["번"] = part.get("번")
got["근거"] = {k: basis[k] for k in ("표지", "글", "출처", "후보", "뺌")}
got["금액"] = blk.get("금액")
return got
# ── 블록 풀이 파생 저장본(올림 뒤 한 번 · 뒤에서) ─────────────────────────
def _stamp(key: str, book: dict[str, Any]) -> str:
"""입력 지문 — HP(지문 · 판 · 올린때) + M01 첫 층 파일 내용 + 풀이 판."""
folder = Path(hb.m01.FOLDER)
files = {
p.name: derived.file_digest(p)
for p in sorted(folder.glob("*.json"))
if not p.name.startswith("_")
}
head = (VERSION, key, book.get("지문"), book.get("판"), book.get("올린때"))
return derived.derived_key(*head, files)
def _caller(book: dict[str, Any], no: int) -> tuple[dict | None, list[dict]]:
"""블록을 부르는 첫 줄 · 그 호표 다른 줄(B1 · B6) — 일위대가 밖 = (None, [])."""
line = sangeun.calls(book)[1].get(no)
if not line:
return None, []
unit = _unit(book, line["_호표"])
same = [x for x in unit.get("줄") or [] if x.get("차례") != line.get("차례")]
return {k: v for k, v in line.items() if k != "_호표"}, same
def solved(folder: Path, key: str) -> dict[str, Any] | None:
"""저장본 `{번 글: 풀이}` — 지문 다르거나 없으면 None(GET 은 안 지움)."""
book = hopyo_store.read(folder, key)
got = derived.read_derived(hopyo_store._dir(folder), KIND + key, _stamp(key, book))
return got["value"] if got else None
def solve_all(folder: Path, key: str) -> dict[str, Any]:
"""블록 모두 풀어 저장본으로(부르는 첫 줄 기준)."""
book = hopyo_store.read(folder, key)
m, out = master(), {}
for no, blk in sorted(_blocks(book).items()):
out[str(no)] = _solve(m, blk, *_caller(book, no))
root = hopyo_store._dir(folder)
derived.write_derived(root, KIND + key, _stamp(key, book), out, keep=1)
return out
def _work(folder: Path, key: str) -> None:
try:
solve_all(folder, key)
except Exception: # noqa: BLE001 — 뒤 셈 실패 = 다음 읽기에서 다시(파생 저장본)
pass
finally:
with _LOCK:
_RUNNING.discard(f"{folder}|{key}")
def _start(folder: Path, key: str) -> None:
tag = f"{folder}|{key}"
with _LOCK:
if tag in _RUNNING:
return
_RUNNING.add(tag)
threading.Thread(target=_work, args=(Path(folder), key), daemon=True).start()
def ensure(folder: Path, key: str) -> bool:
"""저장본 있음 = 참 · 없으면 뒤 셈 시작(화면 안 막음 · 같은 파일 한 번) = 거짓."""
if solved(folder, key) is not None:
return True
_start(folder, key)
return False
def judged(folder: Path, key: str) -> dict[int, dict[str, Any]] | None:
"""블록 판정 `{번: {판정, 고를수}}` — 셈 중 = None(뒤 셈 시작 · Marks 칠이 부름)."""
got = solved(folder, key)
if got is None:
_start(folder, key)
return None
return {int(no): {"판정": v["판정"], "고를수": v["고를수"]} for no, v in got.items()}
def forget(folder: Path, key: str) -> None:
"""[끝내기] — 그 HP 저장본 지움."""
shutil.rmtree(derived.derived_dir(hopyo_store._dir(folder), KIND + key), ignore_errors=True)
def listing(folder: Path, key: str, to: Path) -> dict[str, Any]:
"""호표 목록(1차 판정 수 · 만든 SC) · 블록 목록(부름 · 표지 · 판정 · 고를수 · 만든 SK) ·
`셈중` = 블록 풀이 뒤 셈 중(블록 판정 · 고를수 None)."""
book = hopyo_store.read(folder, key)
rows = hrow.rows(book, folder, ledger.doc_id(folder, key))
sc, sk = _made(folder, key, to)
units = []
for h in book.get("호표") or []:
no = int(h["번"])
count = Counter(r["판정"] for r in rows.get(h["번"]) or [])
head = {"번": no, **{k: h.get(k) for k in ("이름", "규격", "단위", "단가")}}
units.append(
{**head, "줄수": len(h.get("줄") or []), "판정": dict(count),
"만든": sc.get(hbuild.mark(key, no))}
) # fmt: skip
call, done = sangeun.callers(book), judged(folder, key)
blocks = []
for no, b in sorted(_blocks(book).items()):
got = (done or {}).get(no) or {"판정": None, "고를수": None}
blocks.append(
{"번": no, "이름": b.get("이름"), "단위": b.get("단위"), "금액": b.get("금액"),
"부름": call.get(no, []), "표지": clause.basis(b)["표지"], **got,
"만든": sk.get(sangeun.mark(key, no))}
) # fmt: skip
out = {"키": key, "파일": book.get("파일"), "셈중": done is None}
return {**out, "호표": units, "블록": blocks}
def hopyo(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
"""호표 하나 — 줄 판(+ 엑셀 칸) · 단산 줄 `풀이`(부르는 첫 줄이면 저장본)."""
book = hopyo_store.read(folder, key)
unit = _unit(book, no)
rows = hrow.rows({**book, "호표": [unit]}, folder, ledger.doc_id(folder, key))[unit["번"]]
lines, blocks, m = unit.get("줄") or [], _blocks(book), None
cache, first = solved(folder, key) or {}, sangeun.calls(book)[1]
out = []
for r, line in zip(rows, lines, strict=True):
r = _row(line, r)
sub = r.get("번")
if "단가산출" in r["갈래"] and sub is not None and int(sub) in blocks:
head = first.get(int(sub)) or {}
mine = head.get("_호표") == unit["번"] and head.get("차례") == line.get("차례")
if mine and str(sub) in cache:
r["풀이"] = cache[str(sub)]
else:
m = m or master()
others = [x for x in lines if x is not line]
r["풀이"] = _solve(m, blocks[int(sub)], line, others)
out.append(r)
sc, _ = _made(folder, key, to)
head = {k: unit.get(k) for k in ("번", "이름", "규격", "단위", "단가")}
return {**head, "만든": sc.get(hbuild.mark(key, no)), "줄": out}
def block(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
"""단산 블록 하나 — 풀이(저장본 › 셈) + 이름 · 단위 · 부름 · 만든(부르는 첫 줄로 B1 · B6)."""
book = hopyo_store.read(folder, key)
blocks = _blocks(book)
if no not in blocks:
raise _bad(f"없는 블록 「단산{no}」", 404)
line, same = _caller(book, no)
got = (solved(folder, key) or {}).get(str(no)) or _solve(master(), blocks[no], line, same)
_, sk = _made(folder, key, to)
blk = blocks[no]
head = {"이름": blk.get("이름"), "단위": blk.get("단위") or (line or {}).get("단위")}
head |= {"부름": sangeun.callers(book).get(no, []), "만든": sk.get(sangeun.mark(key, no))}
return {**got, **head}
# ── 검산 ─────────────────────────────────────────────────────────────
def _want(m: mf.Master, pick: dict[str, Any]) -> Any:
"""고른 것 단위 — 기초 = 결과단위(「원/㎥」 → ㎥ · 「원/자재 단위」 → 자재) · 그 밖 = 고른 것 단위."""
if pick.get("갈래") == "기초" and pick.get("키"):
unit = str(m.logic(pick["키"]).get("결과단위") or "")
unit = unit.split("/", 1)[1] if unit.startswith("원/") else unit
return unit.removesuffix(" 단위").strip() # 「원/자재 단위」 = 줄 단위 그대로
return pick.get("단위")
def quantity(m: mf.Master, line: dict, pick: dict) -> dict[str, Any]:
"""줄 수량 `{엑셀, 몫, 배, 갈래, 우리}` — 사용자 배(단위 환산 값)가 있으면 그것."""
got = hrow.quantity(line, _want(m, pick), pick.get("몫"))
times = pick.get("배")
if times is not None and line.get("수량") is not None:
ours = Decimal(str(line["수량"])) * Decimal(str(pick.get("몫") or 1))
got |= {"배": times, "갈래": "사용자", "우리": hrow._plain(ours * Decimal(str(times)))}
return got
def one_pick(m: mf.Master, pick: dict[str, Any], items: list[dict], share: Any) -> dict:
"""고른 것 하나 검산."""
out = {"갈래": pick["갈래"], "키": pick.get("키")}
if pick["갈래"] != "기초":
return {**out, "판정": CHECKED}
code = str(pick.get("키") or "")
try:
m.logic(code)
except Exception as e: # noqa: BLE001 — 없는 키
raise _bad(f"없는 기초 「{code}」") from e
full, need = inputs(m, code, pick.get("입력값"))
rows = solve.pair(hb.quantities(m, code, full), solve._usable(items), share) if items else []
if need:
verdict = solve.NEED
elif any(r["판정"] == solve.DIFF for r in rows):
verdict = solve.DIFF
else:
verdict = solve.SAME if any(r["판정"] == solve.SAME for r in rows) else CHECKED
got = money(m, code, full) if not need else {"계": None, "까닭": f"입력 필요 {len(need)}"}
return {**out, "입력값": full, "입력필요": need, "줄": rows, "판정": verdict, "금액": got}
def check(
folder: Path, key: str, hno: int | None, at: int | None, bno: int | None, picks: list[dict]
) -> dict[str, Any]:
"""`{고른 [...], 수량}` — 호표 줄(`hno` · `at`) · 블록(`bno` · 줄이 단산이면 그 블록) · 고른 기초는
맡은 절(`Solve.parts`) 단위 줄 · 몫과 대조."""
book = hopyo_store.read(folder, key)
blocks, line = _blocks(book), None
if hno is not None:
lines = _unit(book, hno).get("줄") or []
if at is None or not 0 <= at < len(lines):
raise _bad(f"없는 줄 「제{hno}호표 {at}」")
line = lines[at]
ref = line.get("참조") or {}
if bno is None and ref.get("종류") == "단산" and ref.get("번") is not None:
bno = int(ref["번"])
if bno is not None and bno not in blocks:
raise _bad(f"없는 블록 「단산{bno}」", 404)
blk = blocks[bno] if bno is not None else {}
parts = solve.parts(blk, clause.basis(blk)["후보"]) if blk else []
m = master()
out = []
for i, pick in enumerate(picks):
part = next((x for x in parts if pick.get("키") in x["후보"]), None) or {}
items = part.get("항목") # 그 기초가 맡은 절 단위 줄(없으면 블록 통째)
items = (blk.get("항목") or []) if items is None else items
share = part.get("몫")
share = pick.get("몫") or (share.get("값") if isinstance(share, dict) else share) or 1
out.append({"차례": i, **one_pick(m, pick, items, share)})
qty = quantity(m, line, picks[0]) if line is not None and picks else None
return {"고른": out, "수량": qty}
+7 -1
View File
@@ -22,13 +22,19 @@ from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Flat as lflat
from M02_MasterTemplete import M02_Library_Same as lsame
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_Hopyo_Sangeun import _same_mark
ORIGIN = "실무자료"
KIND = "precombo"
# ── 같은 내용 재사용 ─────────────────────────────────────────────────
def _same_mark(a: dict, b: dict) -> bool:
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음(SC · SK 함께 · `Sangeun` 이 빌림)."""
if a.get("키") != b.get("키"):
return False
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
def entries(folder: str | Path):
"""층 사전조합 실무자료 출처 → (키, 본뜬 · 또 한 칸, 판) — 또 = 같은 내용으로 이 SC 를 쓴 뒤 출처."""
for path in sorted((Path(folder) / KIND).glob("SC-*.json")):
+399
View File
@@ -0,0 +1,399 @@
"""M02 엑셀 불러오기 만들기(PLAN 785-5 · 요구 642 · 설계 642-A 2-7 ⑥) — 확인한 것 → 우리 SC · SK · 장부.
옮기기 몸 = `{호표 {번: {차례: [고른 것]}}, 블록 {번: [고른 것]}}` · 고른 것 = 785-3 꼴
`{갈래 기초|단가산출|일위대가|인력|재료|유가|기계|율|고정, 키, 입력값, 몫, 배?, 비목?, 기준?, 별도계상?, 층?,
주인?, 항목?}` · 서버가 다시 셈(브라우저 금액 · 품 안 받음 · 배 = `Row.factor` · 사용자 배 우선 ·
입력값 = 엔진이 쓴 칸 모두 `Judge.inputs`) · 「입력 필요」 · 배 못 정함 · 없는 키 = 그 줄(블록)만 `막힘`.
SC(`M02_Hopyo_Build.Builder` 가 줄 차례 · 확인 안 한 줄 = 모두 고정 길 그대로) — 한 엑셀 줄 = 고른 것마다
우리 줄(같은 별칭 = 엑셀 품명 · 줄 `본뜬 {호표, 차례, 행}`) · 수량 {손 = 엑셀 수량 × 몫 × 배} ·
기초 = 로직 줄(같은 로직 둘째 = 한 줄 SC 사본) · 재료 · 유가 = 자재 목록(별도계상) · 인력 · 기계 · 유가 이음 ·
율 = 고정 줄 `비율 {기준, 비목}` · 고정 = 엑셀 단가 고정 줄 · 단가산출 · 일위대가 = 참조
(`단산N` = 같은 옮기기 블록 SK · `제N호표` = 같은 HP 호표 SC — 하위 먼저 · 확인 안 된 하위 = 모두 고정).
SK(블록) = 바탕 없음 + 더한줄 — 기초 `{종류 로직, 입력값, 수량 = 몫}` · 참조 · 단위 줄(`항목`)에 이은
인력 · 기계 · 재료 · 유가 · 율 = `SkUnit.adds` · 고정 = 엑셀 금액 · 쓰기 = `Sangeun.Maker.write`.
같은 내용 재사용(`Kids.save` · `Sangeun.Maker.write`) · 장부 = 고른 것 꼴 짝(`Ledger.check_pair`) ·
엑셀 대조 = `Compare.compare`(참고 · 비목마다 1 원).
`Lines` = 확인한 줄 몸(옛 대상 꼴 `put` 은 옛 길 몫 · 785-7 지움).
"""
from __future__ import annotations
import re
from decimal import Decimal
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Kids as hkids
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_SkLink as sklink
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as lsame
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Hopyo_Compare import compare
_SUB = {"단가산출": re.compile(r"^단산(\d+)$"), "일위대가": re.compile(r"^제(\d+)호표$")}
LINK = {"인력": "노임", "기계": "기계", "유가": "유가"}
OLD_PICK = {"인력": "노임", "기계": "기계", "재료": "자재", "유가": "자재"}
NEED = "입력 필요"
COVER = "이미 옮긴 것이 있음 — 덮으려면 [덮어 옮기기](덮음)"
class Stuck(Exception):
"""그 줄(블록)만 못 옮김 — `{까닭, 칸?}`."""
def __init__(self, why: str, cells: list[str] | None = None) -> None:
super().__init__(why)
self.why, self.cells = why, cells or []
def _num(value: Decimal) -> int | float:
return hbuild._num(value)
# ── 확인한 줄 몸(SC) ─────────────────────────────────────────────────────
class Lines:
"""한 호표 줄 모음 — `Builder.build` 가 줄 차례대로 부름 · 확인 안 한 줄은 Builder 가 고정 길로."""
def __init__(self, b: Any, rows: list[dict], where: dict, stamp: str, linker: Any) -> None:
self.b, self.rows, self.where, self.stamp, self.linker = b, rows, where, stamp, linker
self.lines: list[dict] = []
self.mats: dict[str, dict] = {}
self.missing: list[dict] = []
self.used: set[str] = set()
self.mat_lines: dict[str, dict] = {}
self.nation: dict[tuple, dict] = {}
self.taken: set[str] = set()
self.kids: set[int] = set()
self.left: list[dict] = []
def put(self, line: dict, one: dict, alias: str | None, qty: Decimal | None = None,
mark: dict | None = None) -> None: # fmt: skip
"""대상 하나 → 줄 · 자재 — 옛 꼴 `{로직|자재|호표|전국|이음, 입력값, 배, 몫}` · `qty` = 새 길 수량
(엑셀 × 몫 × 배) · `mark` = 새 길 줄 `본뜬`."""
keep = len(self.lines)
if "이음" in one:
others = [x for x in self.rows if x is not line]
fx = hfix.line_of(line, others, self.where, self.taken, self.linker, one["이음"])
self.lines.append(fx)
else:
qty = (
qty
if qty is not None
else Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
)
self._put(line, one, alias, qty, mark is not None)
for row in self.lines[keep:]:
row.update({"본뜬": mark} if mark else {})
def _put(self, line: dict, one: dict, alias: str | None, qty: Decimal, new: bool) -> None:
b = self.b
memo = hbuild._memo(line, one)
if "전국" in one:
at = (one["이름"], one["규격"], one["단위"])
have = self.nation.get(at)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
self.nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
k: have[k] for k in labor.MARKS if k in have
})} # fmt: skip
self.missing.append(hbuild._missing(line, hbuild.NATIONAL))
return
if "자재" in one:
code = one["자재"]
have = self.mats.get(code)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
name = b.mat_names.get(code, "")
marks = (
have and {k: have[k] for k in labor.MARKS if k in have} or hmat.marks(line, name)
)
if one.get("별도계상") is not None: # 세 칸 값(빈 칸 = 기본 「별도계상」)
marks = {**marks, "별도계상": "별도계상" if one["별도계상"] else "직접계산"}
self.mats[code] = {"코드": code, "수량": {"손": _num(hand)}, **marks}
if not have:
self.mat_lines[code] = line
return
if "호표" in one or "참조" in one:
if "호표" in one:
self.kids.add(one["호표"])
key = b.done.get(hbuild.mark(b.key, one["호표"]), "SC-미리")
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": key}
else:
ref = one["참조"]
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
return
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
if not new:
kept, why = sklink.kept(b, line, one, qty, alias, memo)
if why:
self.left.append(sklink.left(line, why))
if kept is not None:
self.lines.append(kept)
return
if one["로직"] not in self.used:
self.used.add(one["로직"])
self.lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}})
return
at = int(line.get("차례", 0))
hopyo, file = b.hopyo, b.doc.get("파일") or ""
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
"이름": f"{hopyo['이름']} — {line.get('이름')}",
"단위": line.get("단위") or None,
"구분": hbuild.ORIGIN,
"상세구분": file,
"담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}],
"자재": [],
"본뜬": b._origin(f"{hbuild.mark(b.key, b.no, at)} {one['로직']}", self.stamp),
}
ref = {"키": "SC-미리"} if b.dry else hkids.save(wrap, b.same)[0] # 미리 보기 = 쓰기 없음
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
def picks(self, at: int, line: dict, picks: list[dict]) -> None:
"""고른 것(검사 · 다시 셈 끝난 것) → 그 엑셀 줄의 우리 줄 · 자재."""
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
mark = {"호표": self.b.no, "차례": at, "행": line.get("행")}
for p in picks:
kind, code = p["갈래"], p.get("키")
one = {"배": p["배"], "몫": p.get("몫")}
qty = p["수량"]
if kind == "기초":
self.put(line, {**one, "로직": code, "입력값": p["입력값"]}, alias, qty, mark)
elif kind in ("재료", "유가") and not p.get("이음"):
self.put(
line, {**one, "자재": code, "별도계상": p.get("별도계상")}, alias, qty, mark
)
elif kind in ("단가산출", "일위대가"):
self.put(line, {**one, "참조": p["참조"]}, alias, qty, mark)
elif kind == "율":
self.put(line, {"이음": {"비율": p["비목"]}}, alias, None, mark)
self.lines[-1]["고정"]["비율"] = {"기준": p["기준"], "비목": p["비목"]}
elif kind == "고정":
self.put(line, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark)
else:
self.put(line, {"이음": {LINK[kind]: code}}, alias, None, mark)
def ledger_pick(p: dict[str, Any]) -> dict[str, Any]:
"""장부 짝(고른 것 꼴 · `Ledger.check_pair`) — 참조 = 만든 SK · SC 키."""
keep = ("갈래", "키", "입력값", "배", "몫", "비목", "기준", "별도계상")
out = {k: p[k] for k in keep if p.get(k) is not None}
return out | ({"키": p["참조"]["키"]} if "참조" in p else {})
# ── 고른 것 검사 · 다시 셈 ──────────────────────────────────────────────
class Move:
"""한 번 옮기기 — 파일 · M01 · 만든 SC · SK 를 한 번 읽음(쓰기 = `store.root()`)."""
def __init__(self, folder: Path, source: Path, key: str, who: Any) -> None:
self.folder, self.source, self.key, self.who = Path(folder), Path(source), key, who
self.hp = hopyo_store.read(source, key)
self.units = {int(h["번"]): h for h in self.hp.get("호표") or []}
self.m = judge.master()
self.known = ledger._known()
self.sk = sangeun.Maker(source, key, who)
self.same = lsame.Same()
self.out: dict[str, Any] = {"만듦": [], "단가산출": [], "막힘": [], "하위": []}
self.out["장부"] = {"씀": 0}
def _ref(self, p: dict) -> dict[str, Any]:
"""단가산출 · 일위대가 키 → 참조 칸(같은 HP 블록 · 호표 = 만든 것) · 없으면 막힘."""
code, kind = str(p.get("키") or ""), p["갈래"]
hit = _SUB[kind].match(code)
here = refs.here()
if hit:
n = int(hit.group(1))
made = (
self.sk.keys.get(sangeun.mark(self.key, n))
if kind == "단가산출"
else hbuild.confirmed(self.folder, refs.where(self.source)).get(
hbuild.mark(self.key, n)
)
)
if not made:
raise Stuck(f"{code} 먼저 옮김")
return {"층": here[0], "주인": here[1], "키": made}
head = "SK-" if kind == "단가산출" else "SC-"
ref = {"층": p.get("층") or here[0], "주인": p.get("주인"), "키": code}
if p.get("층") is None:
ref["주인"] = here[1]
if not code.startswith(head) or refs.read_ref(ref) is None:
raise Stuck(f"없는 {kind} 「{code}」")
return ref
def pick(self, p: dict, line: dict | None, where: str) -> dict[str, Any]:
"""고른 것 하나 재검사 · 다시 셈 → `{갈래, 키, 입력값, 배, 몫, 수량, 참조?, …}`."""
kind = p["갈래"]
got = {k: p[k] for k in ("갈래", "키", "몫", "비목", "기준", "별도계상") if k in p}
if kind == "기초":
full, need = judge.inputs(self.m, str(p.get("키") or ""), p.get("입력값"))
if need:
raise Stuck(f"{NEED} {len(need)}", need)
got["입력값"] = full
if kind in _SUB:
got["참조"] = self._ref(p)
else:
plain = {k: v for k, v in got.items() if k in ledger.PICK_KEYS}
ledger.check_pair(plain, where, self.known)
if kind in ("재료", "유가") and not got.get("키"):
raise Stuck(f"{kind} 키 없음")
if line is not None:
qty = judge.quantity(self.m, line, p)
if qty["배"] is None:
raise Stuck("단위 환산 입력")
got["배"], got["수량"] = qty["배"], Decimal(str(qty["우리"] or 0))
return got
# ── SC ──
def hopyo(self, no: int, rows: dict[int, list[dict]]) -> None:
unit = self.units[no]
lines = unit.get("줄") or []
checked = {}
for at, picks in sorted(rows.items()):
if not 0 <= at < len(lines):
raise store.StoreError(400, f"없는 줄 「제{no}호표 {at}」")
where = f"제{no}호표 {at + 1}째 줄"
if not picks:
continue
try:
checked[at] = {"고른": [self.pick(p, lines[at], where) for p in picks]}
except Stuck as e:
self.out["막힘"].append({"호표": no, "차례": at, "까닭": e.why, "칸": e.cells})
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
b.strict, b.same = False, self.same
made = b.build(checked, [])
ref = made["참조"]
head = {"번": no, "키": ref["키"], "이름": unit.get("이름")}
head |= {k: made[k] for k in ("다시씀", "재사용")}
self.out["만듦"].append({**head, "비교": compare(ref, unit)})
self.out["장부"]["씀"] += made["장부"]["씀"]
def kid(self, no: int) -> None:
"""확인 안 된 하위 호표 — 모두 고정(705-3 길)으로 먼저."""
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
b.strict, b.same = False, self.same
if b.done.get(hbuild.mark(self.key, no)):
return
made = b.build({}, [])
self.out["하위"].append({"번": no, "키": made["참조"]["키"], "새로": True})
# ── SK ──
def block(self, no: int, picks: list[dict]) -> None:
blk = self.sk.blocks[no]
items = blk.get("항목") or []
us = self.sk.units()
made: list[dict] = []
rows: list[dict] = []
for p in picks:
got = self.pick(p, None, f"단산{no}")
name = str(p.get("이름") or "").strip() or str(blk.get("이름") or f"단산{no}")
share = got.get("몫") or 1
at = p.get("항목")
item = items[at] if isinstance(at, int) and 0 <= at < len(items) else None
if got["갈래"] == "기초":
made.append({"종류": "로직", "로직": got["키"], "입력값": got["입력값"],
"이름": name, "수량": share}) # fmt: skip
continue
if got["갈래"] in _SUB:
qty = float(item.get("품") or 1) if item else share
made.append({"참조": got["참조"], "이름": name, "수량": qty})
continue
if item is None:
raise Stuck(f"{got['갈래']} — 블록 단위 줄(항목) 고름")
if got["갈래"] == "고정":
parts = hfix._parts(item.get("금액"))
fx = {"이름": name, "단가": {k: float(v) for k, v in parts.items()}, "이음": {}}
made.append({"고정": fx, "이름": name, "수량": 1})
continue
old = {"기준": got.get("기준"), "비목": got.get("비목")}
if got["갈래"] in OLD_PICK:
old = {OLD_PICK[got["갈래"]]: got["키"], "별도계상": got.get("별도계상")}
unit = skunit._picked(item, old, us)["단위"]
rows.append({"차례": at, "행": item.get("행"), "상태": units.LINKED, "단위": unit})
extra, _ = skunit.adds(blk, rows, us, self.sk.ref_of)
doc = {"키": "SK-000000", "이름": "옮김", "단위": blk.get("단위") or "식", "바탕": None}
try:
adds = sd.clean({**doc, "더한줄": made + extra}, None, self.sk.whole)["더한줄"]
except (ValueError, store.StoreError) as e:
raise Stuck(f"더한줄 틀림 — {e}") from e
plan = {"갈래": "고름", "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []}
plan |= {"더한줄": adds, "못옮김": [], "놓음": []}
mark_ = sangeun.mark(self.key, no)
key = self.sk.write(no, plan, self.sk.keys.get(mark_))
self.sk.keys[mark_] = key
self.out["단가산출"].append({"번": no, "키": key, "이름": blk.get("이름")})
def blocks(self, sure: dict[int, list[dict]]) -> None:
"""하위 단산(블록 단위 줄 하위) 먼저."""
done: set[int] = set()
def visit(no: int, path: tuple = ()) -> None:
if no in done or no in path:
return
for one in self.sk.blocks[no].get("항목") or []:
sub = one.get("하위") or {}
if sub.get("종류") == "단산" and int(sub.get("번") or 0) in sure:
visit(int(sub["번"]), (*path, no))
done.add(no)
try:
self.block(no, sure[no])
except Stuck as e:
self.out["막힘"].append({"블록": no, "까닭": e.why, "칸": e.cells})
for no in sorted(sure):
visit(no)
def move(
folder: Path,
source: Path,
key: str,
units_: dict[int, dict[int, list[dict]]],
blocks: dict[int, list[dict]],
who: Any = None,
cover: bool = False,
) -> dict[str, Any]:
"""[옮기기] → `{만듦, 단가산출, 막힘, 하위, 장부}` — 블록 SK 먼저 · 호표 하위 먼저 ·
이미 옮긴 호표 · 블록(같은 HP 출처 SC · SK)은 `cover`(덮음) 때만 덮음 · 아니면 409 아무것도 안 씀."""
mv = Move(folder, source, key, who)
bad = [f"제{n}호표" for n in units_ if n not in mv.units]
bad += [f"단산{n}" for n in blocks if n not in mv.sk.blocks]
if bad:
raise store.StoreError(404, f"없는 {bad[0]}")
done = hbuild.confirmed(mv.folder, refs.where(mv.source))
had = {
"호표": [
{"번": n, "키": done[k]} for n in sorted(units_) if (k := hbuild.mark(key, n)) in done
]
}
had["블록"] = [
{"번": n, "키": mv.sk.keys[k]}
for n in sorted(blocks)
if (k := sangeun.mark(key, n)) in mv.sk.keys
]
if not cover and (had["호표"] or had["블록"]):
raise store.StoreError(409, {"까닭": COVER, **had})
mv.blocks(blocks)
wanted = {
int(hit.group(1))
for rows in units_.values()
for picks in rows.values()
for p in picks
if p.get("갈래") == "일위대가" and (hit := _SUB["일위대가"].match(str(p.get("키") or "")))
}
order = hbuild._order(mv.units)
for no in order: # 고른 것이 부르는 확인 안 된 하위 = 모두 고정으로 먼저
if no in wanted and no not in units_:
mv.kid(no)
for no in order:
if no in units_:
mv.hopyo(no, units_[no])
return mv.out
+590
View File
@@ -0,0 +1,590 @@
"""M02 일위대가 실무자료 길(PLAN 785-5 · 요구 642 · 설계 642-A 2-9) — `/api/m02/layers/{층}/hopyo…`.
새 길(글 먼저 · 결정적 매칭) — 판정 `hopyo-judge`(1차 `M02_Hopyo_Row` + 2차 `M02_Hopyo_Solve` ·
`M02_Hopyo_Judge`) · 검산 `hopyo/{키}/check`(저장 없음) · 옮기기 `hopyo/{키}/move`(확인한 것 →
SC · SK · 장부 · `M02_Hopyo_Make`) · 경로 층 = 파일 층(판정 · 검산 · `to` = 만들 층) / 만들 층(옮기기).
올림 = Book + 격자 + 엑셀 사전(`Row.lexicon`) + 블록 풀이 뒤 셈(`Judge.ensure` · 파생 저장본) ·
옛 맞춤 뒤 셈 · 시동 데움 없음 · [끝내기] = 저장본도 지움.
옛 길(`M02_Labor_Router` 에서 옮김 · 옛 화면 = 785-6 끝까지 · 지움 = 785-7) · 그대로 쓰는 길(격자 ·
지우기 · 장부 · 붙여넣기 · 다시 잇기 · 구조물 조합 · 칠 = B09 · M01 이 씀).
층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Literal
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel, ConfigDict, Field, model_validator
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
from M02_MasterTemplete import M02_Hopyo_Judge as hopyo_judge
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
from M02_MasterTemplete import M02_Hopyo_Make as hopyo_maker
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
from M02_MasterTemplete import M02_Hopyo_Row as hopyo_row
from M02_MasterTemplete import M02_Hopyo_Sangeun as hopyo_sangeun
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
router = APIRouter(prefix="/api/m02", tags=["M02 Hopyo"])
# ── 새 길(785-5) — 판정 · 검산 · 옮기기 ─────────────────────────────────────
def _to(session: Session, folder: Path, to: Layer | None, to_owner: str | None) -> Path:
return _root(session, to, to_owner) if to is not None else folder
@router.get("/layers/{layer}/hopyo-judge/{key}")
def hopyo_judge_list(
layer: Layer,
key: str,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""호표 목록 + 1차 판정 수 · 블록 목록 `{키, 파일, 호표 [...], 블록 [...]}`(읽기만)."""
folder = _root(session, layer, owner)
target = _to(session, folder, to, to_owner)
return _call(folder, lambda: hopyo_judge.listing(folder, key, target))
@router.get("/layers/{layer}/hopyo-judge/{key}/{no}")
def hopyo_judge_one(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""호표 하나 — 줄 판(1차) · 단산 줄 `풀이`(2차 블록 풀이)."""
folder = _root(session, layer, owner)
target = _to(session, folder, to, to_owner)
return _call(folder, lambda: hopyo_judge.hopyo(folder, key, no, target))
@router.get("/layers/{layer}/hopyo-judge/{key}/block/{no}")
def hopyo_judge_block(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""단산 블록 하나 — 블록 풀이(`Solve.block`) + 이름 · 단위 · 부름 · 근거 · 만든."""
folder = _root(session, layer, owner)
target = _to(session, folder, to, to_owner)
return _call(folder, lambda: hopyo_judge.block(folder, key, no, target))
class Pick(BaseModel):
"""고른 것(785-3 꼴) — 금액 · 품 칸 없음(서버가 셈) · 고정 단가 = 엑셀 줄 것."""
model_config = ConfigDict(extra="ignore")
갈래: Literal["기초", "단가산출", "일위대가", "인력", "재료", "유가", "기계", "율", "고정"]
키: str | None = Field(None, max_length=40)
이름: str | None = Field(None, max_length=200)
단위: str | None = Field(None, max_length=40)
입력값: dict[str, Any] = Field(default_factory=dict)
몫: float | None = None
배: float | None = None
비목: str | None = None
기준: str | list[str] | None = None
별도계상: bool | None = None
층: Layer | None = None
주인: str | None = None
항목: int | None = None # 블록 고른 것 — 블록 단위 줄 차례(수량 = 그 줄 품)
@model_validator(mode="before")
@classmethod
def _no_money(cls, raw: Any) -> Any:
"""금액 · 품 · 수량 칸 = 400(서버가 셈 · 브라우저 값 안 받아 적음)."""
if isinstance(raw, dict) and {"금액", "품", "수량"} & set(raw):
raise ValueError("금액 · 품 · 수량은 서버가 셈")
return raw
class CheckBody(BaseModel):
"""`호표` + `차례` = 일위대가 줄 · `블록` = 단산 블록(항목 줄과 대조) · `고른` = 고른 것."""
호표: int | None = None
차례: int | None = None
블록: int | None = None
고른: list[Pick] = Field(default_factory=list, max_length=20)
@router.post("/layers/{layer}/hopyo/{key}/check")
def hopyo_check(
layer: Layer,
key: str,
body: CheckBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""검산(저장 없음) — 고른 것마다 입력값(엔진이 쓴 칸 모두) · 입력필요 · 줄 대조 · 금액(서버 셈)."""
folder = _root(session, layer, owner)
picks = [p.model_dump(exclude_none=True) for p in body.고른]
args = (folder, key, body.호표, body.차례, body.블록, picks)
return _call(folder, hopyo_judge.check, *args)
class MoveBody(BaseModel):
"""확인한 것 — 파일 층 `층` · `주인` · `호표 {번: {차례: [고른 것]}}` · `블록 {번: [고른 것]}`."""
model_config = ConfigDict(extra="forbid")
층: Layer
주인: str | None = None
호표: dict[int, dict[int, list[Pick]]] = Field(default_factory=dict, max_length=500)
블록: dict[int, list[Pick]] = Field(default_factory=dict, max_length=500)
덮음: bool = False # 이미 옮긴 호표 · 블록(같은 HP 출처) 덮기 — 아니면 409 {까닭, 호표, 블록}
@router.post("/layers/{layer}/hopyo/{key}/move")
def hopyo_move(
layer: Layer,
key: str,
body: MoveBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[옮기기] — 확인한 것 → 이 층 SC · SK · 장부(하위 먼저 · 서버가 다시 셈) →
`{만듦, 단가산출, 막힘, 하위, 장부}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
dump = lambda ps: [p.model_dump(exclude_none=True) for p in ps] # noqa: E731
units = {no: {at: dump(ps) for at, ps in rows.items()} for no, rows in body.호표.items()}
blocks = {no: dump(ps) for no, ps in body.블록.items()}
args = (folder, source, key, units, blocks, _who(session), body.덮음)
return _call(folder, hopyo_maker.move, *args)
# ── 옛 길(785-7 까지 · 옛 화면) · 그대로 쓰는 길 ─────────────────────────────
HOPYO_MAX_BYTES = 20 * 1024 * 1024
def _who(session: Session) -> dict[str, Any]:
return {"id": session.get("user_id"), "이름": session.get("name")}
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
mark = hopyo_store.fingerprint(data)
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
hopyo_judge.ensure(store.root(), same["키"])
return {**same, "이어서": True}
try:
parsed = hopyo_excel.read_book(name, data)
except hopyo_excel.ExcelError as error:
raise store.StoreError(400, str(error)) from error
try:
grid = hopyo_grid.read(name, data, parsed)
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
grid = None
sheets, _ = hopyo_excel._open(name, data, hopyo_row.SHEETS)
parsed["사전"] = hopyo_row.lexicon(sheets) # 엑셀 사전(785-3 · 1차 줄 판정)
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
hopyo_judge.ensure(store.root(), row["키"]) # 블록 풀이 뒤 셈(파생 저장본 · 화면 안 기다림)
return row
@router.get("/layers/{layer}/hopyo")
def hopyo_list(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
return {"줄": hopyo_store.rows(_root(session, layer, owner))}
@router.get("/layers/{layer}/hopyo-picks")
def hopyo_picks(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다
`{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합)."""
folder = _root(session, layer, owner)
return {"줄": _call(folder, _picks, folder)}
def _picks(folder: Path) -> list[dict[str, Any]]:
rows, here, done = hopyo_match.picks(folder), refs.where(folder), {}
for row in rows:
src = (row["층"], row.get("주인"))
if src not in done:
done[src] = hopyo_build.confirmed(folder, src)
key = done[src].get(hopyo_build.mark(row["키"], row["번"]))
row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None
return rows
@router.post("/layers/{layer}/hopyo")
def hopyo_upload(
layer: Layer,
file: UploadFile = File(...),
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
엉터리 · 시트 없음 400."""
folder = _root(session, layer, owner, write=True)
data = file.file.read(HOPYO_MAX_BYTES + 1)
if len(data) > HOPYO_MAX_BYTES:
raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)")
name = Path(file.filename or "실무자료").name[:120]
return _call(folder, _upload, name, data, _who(session))
@router.get("/layers/{layer}/hopyo/{key}")
def hopyo_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 하나 — 검산 · 호표마다 `맞춤` [들어감, 줄 수] · 파일 `맞춤` 합."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_match.view, folder, key)
@router.get("/layers/{layer}/hopyo-grid/{key}")
def hopyo_grid_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` ·
옛 파일 404."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_store.grid, folder, key)
@router.get("/layers/{layer}/hopyo/{key}/{no}")
def hopyo_one(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
호표 참조 줄 체크 · 만든)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target))
class GuessBody(BaseModel):
"""`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음)."""
차례: int
대상: list[dict[str, Any]] | dict[str, Any]
@router.post("/layers/{layer}/hopyo/{key}/{no}/guess")
def hopyo_guess(
layer: Layer,
key: str,
no: int,
body: GuessBody,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` ·
`to` = 만들 층(없으면 파일 층)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
args = (folder, key, no, body.차례, body.대상)
return _call(folder, lambda: hopyo_match.guess(*args, to=target))
class _Strict(BaseModel):
model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈)
class BuildTarget(_Strict):
"""대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫."""
로직: str | None = None
자재: str | None = None
호표: int | None = None
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
노임: str | None = None # 729-2 사용자 이음 — LB 키(엑셀 숫자 + 이음)
기계: str | None = None # EQ 키
비율: Literal["노무비", "재료비", "경비"] | None = None # % 줄 밑수
입력값: dict[str, Any] = Field(default_factory=dict)
배: float = 1
몫: float | None = None
class BuildLine(_Strict):
"""확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭."""
차례: int
대상: list[BuildTarget] = Field(default_factory=list)
뺌: Literal["우리에 없음", "안 씀"] | None = None
class BuildWhole(_Strict):
대상: list[BuildTarget]
class BuildBody(_Strict):
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
층: Layer
주인: str | None = None
키: str
번: int
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
통째: BuildWhole | None = None
@router.post("/layers/{layer}/hopyo-build")
def hopyo_make(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, _who(session))
return _call(folder, hopyo_build.build, *args)
@router.post("/layers/{layer}/hopyo-unlinked")
def hopyo_unlinked(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""안 이은 줄 미리 보기(729-2) — 만들기 몸 그대로 → `{남은줄 [{차례, 이름, 규격, 단위, 갈래, 까닭,
후보}], 수}`(저장 없음)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole)
return _call(folder, hopyo_build.unlinked, *args)
class BuildAllBody(_Strict):
"""시트 통째(705-3) — 파일 층 `층` · `주인` · `키` 만(숫자 · 줄 칸 없음 · 서버가 파일에서 읽음)."""
층: Layer
주인: str | None = None
키: str
@router.post("/layers/{layer}/hopyo-build-all")
def hopyo_make_all(
layer: Layer,
body: BuildAllBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[모두 고정으로 들여오기] — 시트 호표 모두 고정 줄로 · 하위 먼저 · 확인된 호표 그대로 →
`{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보, 같음, 그대로, 다름, 실패}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
args = (folder, source, body.키, _who(session), True) # 729-2 — 남은 줄 있는 호표 = 실패
return _call(folder, hopyo_build.build_all, *args)
@router.get("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_read(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
(고침 = 이 층 · 쓰기 권한)."""
folder = _root(session, layer, owner)
writable = access.grant(session, layer, owner).write
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
class LedgerBody(_Strict):
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
줄: dict[str, Any]
@router.put("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_write(
layer: Layer,
body: LedgerBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
@router.delete("/layers/{layer}/hopyo/{key}")
def hopyo_delete(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
folder = _root(session, layer, owner, write=True)
got = _call(folder, hopyo_forget.finish, folder, key)
hopyo_judge.forget(folder, key) # 블록 풀이 저장본(785-5)
return got
class SangeunBody(_Strict):
"""`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음)."""
확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
def hopyo_make_sangeun(
layer: Layer,
key: str,
body: SangeunBody | None = None,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK
(파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409."""
folder = _root(session, layer, owner, write=True)
picks = body.확인 if body else []
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
class PasteBody(_Strict):
"""엑셀 붙여넣기(705-2) — `글` = 붙인 글 그대로 · `열` = 기억한 열 짝(없으면 머리 글 · 짐작 + 물음) ·
`줄만` = 열린 문서에 줄로 · `있음` = 문서의 고정 줄 가름(이름 겹침 피함)."""
글: str = Field(max_length=hopyo_paste.TEXT_MAX)
열: dict[str, int] | None = None
줄만: bool = False
있음: list[str] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo-paste")
def hopyo_paste_run(
layer: Layer,
body: PasteBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[Ctrl+V] — `갈래` 물음(열 짝) · 줄(고정 줄 · 저장은 문서 [저장]) · 만듦(실무자료 + 호표마다 사전조합 ·
층 쓰기 · `build_all` 답 + 키)."""
folder = _root(session, layer, owner, write=True)
args = (folder, body.글, body.열, _who(session), body.줄만, body.있음)
return _call(folder, hopyo_paste.paste, *args)
class RelinkRowsBody(BaseModel):
"""다시 잇기 표(705-2) — `키` = 이 층 사전조합들 · `문서` = 열린 문서(저장 전 · 줄 · 후보만)."""
키: list[str] = Field(default_factory=list, max_length=500)
문서: dict[str, Any] | None = None
@router.post("/layers/{layer}/hopyo-relink/rows")
def hopyo_relink_rows(
layer: Layer,
body: RelinkRowsBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""「이 공사 단가로 다시 잇기」 표 — 고정 줄마다 붙인 단가 · 이은 단가 · 후보(층 읽기)."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_relink.rows, body.키, body.문서)
class RelinkBody(_Strict):
"""바꿈 `[{키, 줄, 노임?, 자재?}]` — 이음 키만(숫자 칸 없음 · 값 null = 풂)."""
바꿈: list[dict[str, Any]] = Field(max_length=hopyo_relink.CHANGES_MAX)
@router.post("/layers/{layer}/hopyo-relink")
def hopyo_relink_apply(
layer: Layer,
body: RelinkBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[바꿈] · [되돌리기] — 이 층 사전조합 이음 칸 고쳐 씀(층 쓰기) → `{바뀜, 문서, 되돌림}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_relink.apply, body.바꿈)
@router.get("/layers/{layer}/hopyo/{key}/{no}/siblings")
def hopyo_siblings(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표 `{묶음 [{번, 규격, 열}], 이름}`."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_item.siblings, folder, key, no)
class ItemBody(BuildBody):
"""[구조물 조합으로](704-4) — 확인 창 몸 그대로 + `묶음` = 규격 열로 묶을 호표 번 · `이름` = 항목 이름."""
묶음: list[int] = Field(default_factory=list, max_length=20)
이름: str | None = Field(None, max_length=120)
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
@router.post("/layers/{layer}/hopyo-item")
def hopyo_to_item(
layer: Layer,
body: ItemBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""실무 호표 → 이 층 구조물 조합 항목(사전조합 안 만듦 · 이 층 쓰기 · 파일 층 읽기) →
`{key, 열, 이름, 줄수, 열수, 빠진줄}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
args += (body.새로, body.씀)
return _call(folder, hopyo_item.make_item, *args)
@router.get("/layers/{layer}/hopyo-marks/{key}")
def hopyo_marks_read(
layer: Layer,
key: str,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""하이라이트 재료(779-2 · 읽기만) — `블록 [{번, 판정, SK, 갈래, 못옮김}]` · `to` = SK 를 볼 층."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to else folder
return _call(folder, lambda: {"블록": hopyo_marks.blocks(folder, key, target)})
+30 -31
View File
@@ -1,7 +1,7 @@
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다
줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK.
모두 줄로만(바탕 기초 안 씀 · 맞춤셈 1위 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
모두 줄로만(바탕 기초 안 씀 · 조항 색인 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 =
`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
@@ -19,24 +19,26 @@ from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Clause as clause
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_Hopyo_Kids import _same_mark
ORIGIN = "실무자료"
KIND = "sangeun"
WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]"
WAIT = "고름 대기"
skpatch = SimpleNamespace(WAIT=WAIT, SK="SK") # 옛 이름(SkLink 가 읽음 · 785-7 지움)
def mark(key: str, no: int) -> str:
@@ -64,13 +66,6 @@ def made(
return out
def _same_mark(a: dict, b: dict) -> bool:
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음."""
if a.get("키") != b.get("키"):
return False
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]:
"""그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬."""
own = doc.get("본뜬") or {}
@@ -147,14 +142,13 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
class Maker:
"""한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
"""한 번 만들기 — 파일 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
def __init__(self, source: Path, key: str, who: Any = None) -> None:
self.source, self.key, self.who = Path(source), key, who
self.hp = hopyo_store.read(source, key)
self.blocks, self.first = calls(self.hp)
got = fit.result(source, key) or {}
self.cache, self.done = got.get("블록") or {}, bool(got.get("끝"))
self.done = True # 옛 이름(SkLink) — 맞춤셈 안 기다림(785-5)
self.here = refs.where(source)
self.whole = m01.loaded()[1]
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
@@ -170,19 +164,27 @@ class Maker:
return self._units
def basis(self, no: int) -> dict[str, Any] | None:
"""근거 꼬리표 = 맞춤셈 1위 기초(참고) · 출처 조항이 우리 기초 밖이고 1위가 맞음 아니면
1위 대신 표지(781-2 · 「기초 없음 — 건설 공통 1-2-7 운반」 · 「옛 번호 — 근거 못 씀」)."""
if no not in self.first: # 일위대가 밖 = 맞춤셈 없음
"""근거 꼬리표(참고) — 조항 색인(`M02_Hopyo_Clause.basis`) · 표지(기초 없음 · 옛 번호 · 준용 ·
번호 없음) 있으면 표지 · 후보 하나면 그 기초 · 여럿이면 블록 풀이(`M02_Hopyo_Judge` 저장본 ›
그 블록만 셈) 첫 고름 · 일위대가 밖 블록 = 없음."""
if no not in self.first:
return None
entry = self.cache.get(fit.unit_key(self.first[no])) or {}
top = (entry.get("차례") or [None])[0]
outside = hopyo_basis.no_basis(self.blocks[no].get("출처"))
if outside and (entry.get("판정") or {}).get("상태") != "맞음":
return {"로직": None, "이름": outside["이름"], "판정": outside["판정"]}
got = clause.basis(self.blocks[no])
if got["표지"]:
return {"로직": None, "이름": got["글"], "판정": got["표지"]}
cands = got["후보"]
top = cands[0]["키"] if len(cands) == 1 else self._solved(no)
if not top:
return None
name = (self.whole.logic(top) or {}).get("이름")
return {"로직": top, "이름": name, "판정": (entry.get("판정") or {}).get("상태")}
return {"로직": top, "이름": (self.whole.logic(top) or {}).get("이름"), "판정": None}
def _solved(self, no: int) -> str | None:
from M02_MasterTemplete import M02_Hopyo_Judge as judge # Judge → 여기 — 늦게 읽음
cache = judge.solved(self.source, self.key) if getattr(self, "key", None) else None
line = {k: v for k, v in self.first[no].items() if k != "_호표"}
got = (cache or {}).get(str(no)) or judge._solve(judge.master(), self.blocks[no], line, [])
return next((s["고름"] for s in got["절"] if s["고름"]), None)
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
"""하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None."""
@@ -209,10 +211,10 @@ class Maker:
def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]:
"""확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기."""
if picks is None:
return {"갈래": skpatch.WAIT, "까닭": WAIT_WHY, "후보": []}
return {"갈래": WAIT, "까닭": WAIT_WHY, "후보": []}
got = self.judge(no, picks)
if got["고를줄"]:
return {"갈래": skpatch.WAIT, "까닭": got["판정"], "후보": []}
return {"갈래": WAIT, "까닭": got["판정"], "후보": []}
return skunit.plan(got, self.basis(no))
def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None:
@@ -229,7 +231,7 @@ class Maker:
got = self.judged[no]
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
name = self.blocks[no].get("이름") or (self.first.get(no) or {}).get("이름")
if plan["갈래"] == skpatch.WAIT or lost:
if plan["갈래"] == WAIT or lost:
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
return
@@ -296,9 +298,6 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
bad = sorted(set(sure) - set(maker.blocks))
if bad:
raise _bad(f"없는 블록 「단산{bad[0]}」", 404)
if not maker.done:
fit.ensure(folder, key)
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
out: dict[str, Any] = {"블록": len(maker.blocks), "만듦": [], "못만듦": []}
for no in sorted(sure):
maker.make(no, sure, out)
+35 -6
View File
@@ -8,7 +8,8 @@
단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계},
수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) ·
율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄.
근거 꼬리표 = 맞춤셈 1위 기초(본뜬 칸 · 참고). 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`.
근거 꼬리표 = 조항 색인(`Sangeun.Maker.basis` · 참고) · 고른 것(785-5 `M02_Hopyo_Make`)도 `adds` 를 부름 ·
기계경비 기초 차례(`_mach_logics`) = 옛 SkPatch 에서 옮김. 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`.
"""
from __future__ import annotations
@@ -16,7 +17,6 @@ from __future__ import annotations
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Sangeun_Calc as calc
@@ -29,13 +29,41 @@ OUT, DROP = "밖", "뺌"
UNIT = "단위" # 본뜬 갈래
MINE = "내가 고름"
WORD = {"L": "품", "X": "시간", "M": "수량", "R": "율", "S": "수량"}
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", "MT")}
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", ("MT", "MO"))}
MACH_IN = "기계" # 기계경비 기초 기계 입력 칸
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
MACHINE_ANY = "GC000268"
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
# (주연료, 조종원) 없는 기종 → 그 기계경비 기초(M01 로직이 부르는 것과 같음 · 778-2)
SOLO = {(False, False): "GC000273", (True, False): "GC000272", (False, True): "GC000276"}
class PickError(ValueError):
"""고름 틀림 — 부르는 쪽이 400."""
def _mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]:
"""기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면
연료 · 조종원 없는 기종 = 그 기초 하나 · 아니면 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞."""
first = next((c for k, c in MACHINE.items() if k in kinds), MACHINE_ANY)
order = [first, *sorted(MACHINE_SET - {first})]
if not eq:
return order
has = eq.get("값") if isinstance(eq.get("값"), dict) else {}
if solo := SOLO.get((bool(has.get("주연료")), bool(has.get("조종원")))):
return [solo]
eqs = whole.numbers.get("EQ", {})
def takes(code: str) -> bool:
ins = whole.logic(code).get("입력") or []
opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), [])
same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts)
return eq.get("원문번호") in opts or same
hit = [c for c in order if takes(c)]
return hit + [c for c in order if c not in hit]
def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
"""사용자 고름 → 이어짐 단위 — 노임 LB · 기계 EQ(→ 기계경비 로직) · 자재 MT · 율 기준 · 뺌."""
if not isinstance(pick, dict):
@@ -50,7 +78,8 @@ def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
field, head = PICKS.get(kind, ("", "?"))
code = str(pick.get(field) or "")
if not field or not code.startswith(head):
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({head}…)로 고름")
shown = head if isinstance(head, str) else head[0]
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({shown}…)로 고름")
try:
row = us.whole.get(code)
except Exception as e: # noqa: BLE001 — 없는 키
@@ -58,7 +87,7 @@ def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
unit: dict[str, Any] = {field: code}
if kind == "X":
kinds = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
unit["로직"] = skpatch._mach_logics(us.whole, kinds, row)[0]
unit["로직"] = _mach_logics(us.whole, kinds, row)[0]
unit |= {"할증": one["할증"]} if one.get("할증") else {}
if kind == "M" and (one.get("어떻게") == "별도" or pick.get("별도계상")):
unit["별도계상"] = True
@@ -110,7 +139,7 @@ def adds(block: dict, rows: list[dict], us: units.Units, ref_of: Any) -> tuple[l
add = {"종류": "인력", "요소": unit["노임"], "이름": name, "단위": "인", "수량": q}
elif kind == "X":
eq = us.whole.get(unit["기계"]) or {}
given = {skpatch.MACH_IN: eq.get("원문번호")}
given = {MACH_IN: eq.get("원문번호")}
q *= float(unit.get("할증") or 1)
add = {
"종류": "로직",
+3 -439
View File
@@ -5,38 +5,19 @@
지우기는 양식 길 그대로) + 개인 자재 목록 · 추가(148-2 · 개인 층만 · 품번은 서버) +
서고 나무 자유 폴더(158-1 · 716-4 `{종류}-tree` · `M02_Precombo_Folders` · 읽기 = 층 읽기 ·
고침 = 층 쓰기) +
일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) ·
338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 ·
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) ·
718-1 한시 불러오기 = 올림 지문(작업 중 같은 엑셀 = 그 파일 `이어서` · 끝낸 것 = 새로 · 743-1) ·
지우기 = [끝내기](`M02_Hopyo_Forget.finish` · 올림 때 하루 지난 HP 도 끝냄).
일위대가 실무자료 길 = `M02_Hopyo_Router`(785-5).
층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌).
⚠ 등록은 `main.py`(로그인만).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Literal
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel, ConfigDict, Field
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
from M02_MasterTemplete import M02_Hopyo_Fit as hopyo_fit
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Ours as hopyo_ours
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
from M02_MasterTemplete import M02_Hopyo_Sangeun as hopyo_sangeun
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Layer_Access as access
@@ -206,420 +187,3 @@ def new_material(
raise HTTPException(status_code=400, detail="신규 자재는 개인 층에서만")
folder = _root(session, layer, owner, write=True)
return _call(folder, own.add, folder, body)
# ── 일위대가 실무자료(324 · 338) ───────────────────────────────────────
HOPYO_MAX_BYTES = 20 * 1024 * 1024
hopyo_ours.warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠
def _who(session: Session) -> dict[str, Any]:
return {"id": session.get("user_id"), "이름": session.get("name")}
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
mark = hopyo_store.fingerprint(data)
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
return {**same, "이어서": True}
try:
parsed = hopyo_excel.read_book(name, data)
except hopyo_excel.ExcelError as error:
raise store.StoreError(400, str(error)) from error
try:
grid = hopyo_grid.read(name, data, parsed)
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
grid = None
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
hopyo_fit.ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
return row
@router.get("/layers/{layer}/hopyo")
def hopyo_list(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
folder = _root(session, layer, owner)
rows = hopyo_store.rows(folder)
for row in rows: # 지금 판 맞춤셈 없는 파일 = 목록 열 때 뒤 셈 예약(358-3 ③)
hopyo_fit.ensure(folder, row["키"])
return {"줄": rows}
@router.get("/layers/{layer}/hopyo-picks")
def hopyo_picks(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다
`{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합)."""
folder = _root(session, layer, owner)
return {"줄": _call(folder, _picks, folder)}
def _picks(folder: Path) -> list[dict[str, Any]]:
rows, here, done = hopyo_match.picks(folder), refs.where(folder), {}
for row in rows:
src = (row["층"], row.get("주인"))
if src not in done:
done[src] = hopyo_build.confirmed(folder, src)
key = done[src].get(hopyo_build.mark(row["키"], row["번"]))
row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None
return rows
@router.post("/layers/{layer}/hopyo")
def hopyo_upload(
layer: Layer,
file: UploadFile = File(...),
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
엉터리 · 시트 없음 400."""
folder = _root(session, layer, owner, write=True)
data = file.file.read(HOPYO_MAX_BYTES + 1)
if len(data) > HOPYO_MAX_BYTES:
raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)")
name = Path(file.filename or "실무자료").name[:120]
return _call(folder, _upload, name, data, _who(session))
@router.get("/layers/{layer}/hopyo/{key}")
def hopyo_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 하나 — 검산 · 호표마다 `맞춤` [들어감, 줄 수] · 파일 `맞춤` 합."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_match.view, folder, key)
@router.get("/layers/{layer}/hopyo-grid/{key}")
def hopyo_grid_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` ·
옛 파일 404."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_store.grid, folder, key)
@router.get("/layers/{layer}/hopyo/{key}/{no}")
def hopyo_one(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
호표 참조 줄 체크 · 만든)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target))
class GuessBody(BaseModel):
"""`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음)."""
차례: int
대상: list[dict[str, Any]] | dict[str, Any]
@router.post("/layers/{layer}/hopyo/{key}/{no}/guess")
def hopyo_guess(
layer: Layer,
key: str,
no: int,
body: GuessBody,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` ·
`to` = 만들 층(없으면 파일 층)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
args = (folder, key, no, body.차례, body.대상)
return _call(folder, lambda: hopyo_match.guess(*args, to=target))
class _Strict(BaseModel):
model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈)
class BuildTarget(_Strict):
"""대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫."""
로직: str | None = None
자재: str | None = None
호표: int | None = None
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
노임: str | None = None # 729-2 사용자 이음 — LB 키(엑셀 숫자 + 이음)
기계: str | None = None # EQ 키
비율: Literal["노무비", "재료비", "경비"] | None = None # % 줄 밑수
입력값: dict[str, Any] = Field(default_factory=dict)
배: float = 1
몫: float | None = None
class BuildLine(_Strict):
"""확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭."""
차례: int
대상: list[BuildTarget] = Field(default_factory=list)
뺌: Literal["우리에 없음", "안 씀"] | None = None
class BuildWhole(_Strict):
대상: list[BuildTarget]
class BuildBody(_Strict):
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
층: Layer
주인: str | None = None
키: str
번: int
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
통째: BuildWhole | None = None
@router.post("/layers/{layer}/hopyo-build")
def hopyo_make(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, _who(session))
return _call(folder, hopyo_build.build, *args)
@router.post("/layers/{layer}/hopyo-unlinked")
def hopyo_unlinked(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""안 이은 줄 미리 보기(729-2) — 만들기 몸 그대로 → `{남은줄 [{차례, 이름, 규격, 단위, 갈래, 까닭,
후보}], 수}`(저장 없음)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole)
return _call(folder, hopyo_build.unlinked, *args)
class BuildAllBody(_Strict):
"""시트 통째(705-3) — 파일 층 `층` · `주인` · `키` 만(숫자 · 줄 칸 없음 · 서버가 파일에서 읽음)."""
층: Layer
주인: str | None = None
키: str
@router.post("/layers/{layer}/hopyo-build-all")
def hopyo_make_all(
layer: Layer,
body: BuildAllBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[모두 고정으로 들여오기] — 시트 호표 모두 고정 줄로 · 하위 먼저 · 확인된 호표 그대로 →
`{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보, 같음, 그대로, 다름, 실패}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
args = (folder, source, body.키, _who(session), True) # 729-2 — 남은 줄 있는 호표 = 실패
return _call(folder, hopyo_build.build_all, *args)
@router.get("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_read(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
(고침 = 이 층 · 쓰기 권한)."""
folder = _root(session, layer, owner)
writable = access.grant(session, layer, owner).write
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
class LedgerBody(_Strict):
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
줄: dict[str, Any]
@router.put("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_write(
layer: Layer,
body: LedgerBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
@router.delete("/layers/{layer}/hopyo/{key}")
def hopyo_delete(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_forget.finish, folder, key)
class SangeunBody(_Strict):
"""`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음)."""
확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
def hopyo_make_sangeun(
layer: Layer,
key: str,
body: SangeunBody | None = None,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK
(파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409."""
folder = _root(session, layer, owner, write=True)
picks = body.확인 if body else []
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
class PasteBody(_Strict):
"""엑셀 붙여넣기(705-2) — `글` = 붙인 글 그대로 · `열` = 기억한 열 짝(없으면 머리 글 · 짐작 + 물음) ·
`줄만` = 열린 문서에 줄로 · `있음` = 문서의 고정 줄 가름(이름 겹침 피함)."""
글: str = Field(max_length=hopyo_paste.TEXT_MAX)
열: dict[str, int] | None = None
줄만: bool = False
있음: list[str] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo-paste")
def hopyo_paste_run(
layer: Layer,
body: PasteBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[Ctrl+V] — `갈래` 물음(열 짝) · 줄(고정 줄 · 저장은 문서 [저장]) · 만듦(실무자료 + 호표마다 사전조합 ·
층 쓰기 · `build_all` 답 + 키)."""
folder = _root(session, layer, owner, write=True)
args = (folder, body.글, body.열, _who(session), body.줄만, body.있음)
return _call(folder, hopyo_paste.paste, *args)
class RelinkRowsBody(BaseModel):
"""다시 잇기 표(705-2) — `키` = 이 층 사전조합들 · `문서` = 열린 문서(저장 전 · 줄 · 후보만)."""
키: list[str] = Field(default_factory=list, max_length=500)
문서: dict[str, Any] | None = None
@router.post("/layers/{layer}/hopyo-relink/rows")
def hopyo_relink_rows(
layer: Layer,
body: RelinkRowsBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""「이 공사 단가로 다시 잇기」 표 — 고정 줄마다 붙인 단가 · 이은 단가 · 후보(층 읽기)."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_relink.rows, body.키, body.문서)
class RelinkBody(_Strict):
"""바꿈 `[{키, 줄, 노임?, 자재?}]` — 이음 키만(숫자 칸 없음 · 값 null = 풂)."""
바꿈: list[dict[str, Any]] = Field(max_length=hopyo_relink.CHANGES_MAX)
@router.post("/layers/{layer}/hopyo-relink")
def hopyo_relink_apply(
layer: Layer,
body: RelinkBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[바꿈] · [되돌리기] — 이 층 사전조합 이음 칸 고쳐 씀(층 쓰기) → `{바뀜, 문서, 되돌림}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_relink.apply, body.바꿈)
@router.get("/layers/{layer}/hopyo/{key}/{no}/siblings")
def hopyo_siblings(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표 `{묶음 [{번, 규격, 열}], 이름}`."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_item.siblings, folder, key, no)
class ItemBody(BuildBody):
"""[구조물 조합으로](704-4) — 확인 창 몸 그대로 + `묶음` = 규격 열로 묶을 호표 번 · `이름` = 항목 이름."""
묶음: list[int] = Field(default_factory=list, max_length=20)
이름: str | None = Field(None, max_length=120)
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
@router.post("/layers/{layer}/hopyo-item")
def hopyo_to_item(
layer: Layer,
body: ItemBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""실무 호표 → 이 층 구조물 조합 항목(사전조합 안 만듦 · 이 층 쓰기 · 파일 층 읽기) →
`{key, 열, 이름, 줄수, 열수, 빠진줄}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
args += (body.새로, body.씀)
return _call(folder, hopyo_item.make_item, *args)
@router.get("/layers/{layer}/hopyo-marks/{key}")
def hopyo_marks_read(
layer: Layer,
key: str,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""하이라이트 재료(779-2 · 읽기만) — `블록 [{번, 판정, SK, 갈래, 못옮김}]` · `to` = SK 를 볼 층."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to else folder
return _call(folder, lambda: {"블록": hopyo_marks.blocks(folder, key, target)})
+2
View File
@@ -96,6 +96,7 @@ from E03_StabSection.E03_StabSection_Router_WhatIf import router as e03_stab_wha
from E03_StabSection.E03_StabSection_Router_Prob import router as e03_stab_prob_router
from E06_StabRock.E06_StabRock_Router import router as e06_stab_rock_router
from M01_MasterData.M01_MasterData_Router import router as m01_master_data_router
from M02_MasterTemplete.M02_Hopyo_Router import router as m02_hopyo_router
from M02_MasterTemplete.M02_Labor_Router import router as m02_labor_router
from M02_MasterTemplete.M02_MasterTemplete_Router import router as m02_master_template_router
from M02_MasterTemplete.M02_MasterTemplete_Router_Combos import router as m02_combos_router
@@ -563,6 +564,7 @@ app.include_router(m02_items_router, dependencies=protected)
app.include_router(m02_combos_router, dependencies=protected)
app.include_router(m02_drawing_router, dependencies=protected)
app.include_router(m02_labor_router, dependencies=protected)
app.include_router(m02_hopyo_router, dependencies=protected)
# 단가산출 로직 상세 읽기 · 셈(PLAN 717-1) — 로그인만 · 쓰기 없음
app.include_router(
__import__("M02_MasterTemplete.M02_Logic_Lib_Read", fromlist=["router"]).router,
@@ -82,7 +82,6 @@ def test_예시1_근거_인력운반_지게():
assert got["차례"][0] == "GC001140" # 지게(사람 줄) — 1141 기계설비 · 1142 벽돌 아님
maker = sangeun.Maker.__new__(sangeun.Maker)
maker.first, maker.blocks, maker.whole = {10: line}, {10: block}, m01.loaded()[1]
maker.cache = {fit.unit_key(line): got}
basis = maker.basis(10)
assert basis["로직"] == "GC001140" and basis["이름"].startswith("인력운반(지게)")
judged = skunit.judge(block, units.Units(), lambda sub: None, None, "㎡")
+200
View File
@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
"""642-5(PLAN 785-5) 만들기 · 길 지킴 시험(설계 642-A 5-3 넷째 줄) — 임시 storage(`client` 마당).
- 판정 길(목록 · 호표 · 블록) · 검산 길(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움)
- 옮기기 → SC 줄(고른 것마다 · 같은 별칭 · 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫) · 같은 기초 둘째 = 사본 ·
별도계상 자재 · 입력 필요 = 그 줄만 막힘(고정 줄로 남음) · SK 더한줄(바탕 없음) · 장부 · 다시 옮김 = SC 안 늚
- 금액 · 품 칸 막음 · M01 원본 그대로
"""
import json
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
API = "/api/m02/layers/personal"
KEY = "HP-000001"
def _mine():
return layers.personal_dir(7, 42)
def _doc(kind, key):
return json.loads((_mine() / kind / f"{key}.json").read_bytes())
def _at(unit, no):
"""호표 줄 판에서 단산 N 을 부르는 줄."""
return next(r for r in unit["줄"] if r.get("번") == no and "단가산출" in r["갈래"])
def _base(row, sec=0):
"""그 줄 풀이 절의 고른 기초 · 입력값."""
part = row["풀이"]["절"][sec]
cand = next(c for c in part["후보"] if c["키"] == part["고름"])
return {"갈래": "기초", "키": cand["키"], "입력값": cand["바인딩"]["입력값"]}
def _specs(code):
m = hb.m01.cm.master()
return {s["이름"] for s in hb.specs_of(m.logic(code))} - set(hb.SKIP)
def test_판정_검산_옮기기(client):
before = master_hash()
assert _post(client).status_code == 200
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
assert got["셈중"] is True and got["블록"][9]["판정"] is None # 뒤 셈 전
assert judge.judged(_mine(), KEY) is None
judge.solve_all(_mine(), KEY) # 올림 뒤 뒤 셈(시험은 바로)
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
assert got["셈중"] is False and len(got["호표"]) == 68 and len(got["블록"]) == 95
assert got["블록"][9]["번"] == 10 and got["블록"][9]["판정"] == "기초로 풂"
assert judge.judged(_mine(), KEY)[10] == {"판정": "기초로 풂", "고를수": 0}
assert got["호표"][0]["판정"] == {"제안 있음": 2, "2차 필요": 4}
two = client.get(f"{API}/hopyo-judge/{KEY}/2").json()
ten = _at(two, 10) # 예시 1 야면석소운반 단산10
assert ten["이름"] and "단가" in ten and ten["풀이"]["판정"] == "기초로 풂"
ex1 = _base(ten)
assert ex1["키"] == "GC001140" and ex1["입력값"]["운반무게"] == 575
blk = client.get(f"{API}/hopyo-judge/{KEY}/block/10").json()
assert blk["부름"] and "몫" in blk["절"][0] and "금액" in blk
check = client.post(
f"{API}/hopyo/{KEY}/check", json={"호표": 2, "차례": ten["차례"], "고른": [ex1]}
).json()
one = check["고른"][0]
assert one["판정"] == "품 같음" and one["입력필요"] == []
assert set(one["입력값"]) == _specs("GC001140")
assert not set(one["입력값"]) & set(hb.SKIP) # 지역 · 자재지역 · 조종원직종 비움
assert int(one["금액"]["계"]) == 94227 # 엑셀 94,227 (원 미만 버림)
assert check["수량"]["우리"] == ten["수량"]["엑셀"]
first = client.get(f"{API}/hopyo-judge/{KEY}/1").json()
rebar, form = _at(first, 47), _at(first, 24) # 예시 2 철근 · 예시 3 거푸집
a, b = _base(rebar, 0), _base(rebar, 1)
b["입력값"] = {**b["입력값"], "구분": "토목"} # 엇갈림 = 사용자 고름
a["몫"] = 0.5
mat = first["줄"][0] # 레미콘 별도계상
cement = {**mat["후보"][0], "별도계상": True}
stuck = {"갈래": "기초", "키": "GC000211", "입력값": {"유형": "보통"}}
ex3 = _base(form) # 예시 3 GC000211 보통 · 4회 · 높이감률 · 구멍땜 = 입력 필요
url = f"{API}/hopyo/{KEY}/check"
need = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()
assert (
need["고른"][0]["입력필요"] == ["높이감률", "구멍땜"]
and need["고른"][0]["금액"]["계"] is None
)
ex3["입력값"] |= {"높이감률": 0, "구멍땜": 0}
got3 = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()["고른"][0]
assert (
got3["판정"] == "품 같음" and abs(got3["금액"]["노무비"] - 38291) <= 1
) # 엑셀 노무 38,291
other = next(r for r in two["줄"] if r["차례"] > ten["차례"]) # 같은 기초 둘째 줄
twice = {**ex1, "입력값": {**ex1["입력값"], "운반거리": 12}}
body = {
"층": "personal",
"호표": {
"1": {
str(rebar["차례"]): [a, b],
str(form["차례"]): [stuck],
"0": [cement],
},
"2": {str(ten["차례"]): [ex1], str(other["차례"]): [twice]},
},
"블록": {"10": [{**ex1, "몫": 1}]},
}
res = client.post(f"{API}/hopyo/{KEY}/move", json=body)
assert res.status_code == 200, res.text
out = res.json()
assert [x["번"] for x in out["만듦"]] == [1, 2] and out["장부"]["씀"] >= 4
assert out["막힘"] == [
{
"호표": 1,
"차례": form["차례"],
"까닭": out["막힘"][0]["까닭"],
"칸": out["막힘"][0]["칸"],
}
]
assert out["막힘"][0]["까닭"].startswith("입력 필요") and out["막힘"][0]["칸"]
sc2 = _doc("precombo", out["만듦"][1]["키"])
lines = sc2["담은로직"]
mine = next(x for x in lines if x.get("로직") == "GC001140")
assert mine["별칭"] == ten["이름"][: len(mine["별칭"])]
assert mine["본뜬"] == {"호표": 2, "차례": ten["차례"], "행": ten["행"]}
assert mine["수량"]["손"] == ten["수량"]["엑셀"]
assert set(mine["입력값"]) == _specs("GC001140")
wrap = next(x for x in lines if x.get("별칭") == other["이름"][: len(x.get("별칭") or "")])
inner = wrap["참조"]["몸"]["담은로직"][
0
] # 같은 기초 둘째 = 한 줄 SC 사본(본뜬 = 사본 출처 740-1)
assert inner["로직"] == "GC001140" and inner["입력값"]["운반거리"] == 12
assert len(lines) == len(two["줄"]) # 확인 안 한 줄 = 고정(빠짐 0)
sc1 = _doc("precombo", out["만듦"][0]["키"])
pair = [x for x in sc1["담은로직"] if (x.get("본뜬") or {}).get("차례") == rebar["차례"]]
assert [x["로직"] for x in pair] == ["GC000206", "GC000207"]
assert pair[0]["별칭"] == pair[1]["별칭"] # 한 엑셀 줄 = 우리 두 줄 · 같은 별칭
excel = rebar["수량"]["엑셀"]
assert pair[0]["수량"]["손"] == excel * 0.5 and pair[1]["수량"]["손"] == excel
assert pair[1]["입력값"]["구분"] == "토목"
held = next(
x
for x in sc1["담은로직"]
if (x.get("고정") or {}).get("출처", {}).get("줄") == form["차례"]
)
assert "본뜬" not in held # 막힌 줄 = 고정 줄(엑셀 숫자)
cement_row = next(m for m in sc1["자재"] if m["코드"] == cement["키"])
assert labor.mark_of(cement_row, "별도계상") == "별도계상"
assert cement_row["수량"]["손"] == mat["수량"]["엑셀"]
sk = _doc("sangeun", out["단가산출"][0]["키"])
assert sk["바탕"] is None and [x["로직"] for x in sk["더한줄"]] == ["GC001140"]
assert sk["더한줄"][0]["입력값"]["운반무게"] == 575
book = ledger.read(_mine())
assert any(
p.get("갈래") == "기초" and p.get("키") == "GC000207"
for row in book.values()
for p in row.get("짝") or []
)
scs = sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json"))
stop = client.post(f"{API}/hopyo/{KEY}/move", json=body) # 이미 옮긴 것 = 덮음 없이 409
assert stop.status_code == 409 and [x["번"] for x in stop.json()["detail"]["호표"]] == [1, 2]
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
again = client.post(f"{API}/hopyo/{KEY}/move", json={**body, "덮음": True})
assert again.status_code == 200, again.text
again = again.json()
assert [x["키"] for x in again["만듦"]] == [x["키"] for x in out["만듦"]]
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
assert hbuild.confirmed(_mine())[hbuild.mark(KEY, 2)] == out["만듦"][1]["키"]
assert client.delete(f"{API}/hopyo/{KEY}").status_code == 200 # [끝내기] = 저장본도 지움
assert not list((_mine() / "실무자료" / "_derived").glob(f"*{KEY}*"))
assert master_hash() == before
def test_막는_길(client):
assert _post(client).status_code == 200
pick = {"갈래": "기초", "키": "GC001140", "입력값": {}, "금액": 1}
bad = client.post(
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [pick]}}}
)
assert bad.status_code == 422 # 금액 · 품 칸 = 서버가 셈
none = client.post(f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"99": {}}})
assert none.status_code == 404
wrong = {"갈래": "인력", "키": "LB999999"}
res = client.post(
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [wrong]}}}
)
assert res.status_code == 400
@@ -77,7 +77,6 @@ def test_결과_단가산출_확인한_블록만_단위_SK_겹침_없음(client)
folder = layers.personal_dir(7, 42)
assert _post(client).json()["키"] == "HP-000001"
url = URL.format("personal") + "/HP-000001/sangeun"
assert client.post(url).status_code == 409 # 729-1 — 맞춤 셈 전
fit.run_job(folder, "HP-000001")
got = client.post(url)
assert got.status_code == 200, got.text
+2 -3
View File
@@ -51,7 +51,6 @@ def test_실무_단가산출_확인한_블록만_단위_SK_일위대가_막음(c
folder = layers.personal_dir(7, 42)
key = _post(client).json()["키"]
url = f"{URL.format('personal')}/{key}/sangeun"
assert client.post(url).status_code == 409 # 맞춤 셈 전
fit.run_job(folder, key)
got = client.post(url, json={}).json()
assert got["만듦"] == [] and not _sk_docs(folder) # 확인 없음 = 판정만
@@ -64,7 +63,7 @@ def test_실무_단가산출_확인한_블록만_단위_SK_일위대가_막음(c
for no, head in SAMPLES.items():
doc = docs[no]
assert doc["이름"].startswith(head) and doc["바탕"] is None, no
assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"]["근거"] == rows[no]["근거"]
assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"].get("근거") == rows[no]["근거"]
mine = [x["수량"] for x in doc["더한줄"] if x.get("종류") == "인력"] # 엑셀 품 그대로
excel = [r["매개값"]["품"] for r in rows[no]["줄"] if r["묶음"] == "L"]
assert mine == excel, no
@@ -165,7 +164,7 @@ def test_노임_이름_이음_기계_EQ_이음_사용자_이음(client):
line = {"갈래": "자재", "이름": "없는자재", "규격": "", "단위": "개", "수량": 2,
"단가": {"재료비": 10}, "차례": 0} # fmt: skip
one = hfix.line_of(line, [], {"파일": "x", "호표": 1}, set(), linker, {"노임": "LB000002"})
assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None}
assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None, "유가": None}
assert hfix.linked(one) and not hfix.linked(hfix.line_of(line, [], {}, set(), linker))
+6 -1
View File
@@ -21,6 +21,7 @@ from config import config_system
from M02_MasterTemplete import M02_Hopyo_Excel as hx
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Ours as ours
from M02_MasterTemplete import M02_Hopyo_Router as hopyo_router
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Labor_Router as labor_router
from M02_MasterTemplete import M02_MasterTemplete_Store as store
@@ -133,6 +134,9 @@ def calc_cache(tmp_path, monkeypatch) -> Path:
shutil.copy(ours.path_of(), cache / ours.path_of().name)
monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache))
monkeypatch.setattr(fit, "ensure", fit.status)
monkeypatch.setattr(
hopyo_router.hopyo_judge, "_start", lambda *_: None
) # 블록 풀이 뒤 셈 안 돌림
return cache
@@ -148,6 +152,7 @@ def client(tmp_path, monkeypatch):
calc_cache(tmp_path, monkeypatch)
app = FastAPI()
app.include_router(labor_router.router)
app.include_router(hopyo_router.router) # 785-5 hopyo 길 옮김
who = dict(USER)
app.dependency_overrides[verify_session] = lambda: who
got = TestClient(app)
@@ -194,7 +199,7 @@ def test_엉터리_시트없음_큰_파일_400(client, monkeypatch):
assert bad.status_code == 400 and "읽지 못함" in bad.json()["detail"]
none = _post(client, data=NO_SHEET.read_bytes(), name=NO_SHEET.name)
assert none.status_code == 400 and none.json()["detail"] == "일위대가 시트 없음"
monkeypatch.setattr(labor_router, "HOPYO_MAX_BYTES", 1000)
monkeypatch.setattr(hopyo_router, "HOPYO_MAX_BYTES", 1000)
big = _post(client)
assert big.status_code == 400 and "20 MB" in big.json()["detail"]
assert client.get(URL.format("personal")).json()["줄"] == []