feat(807-1): 엑셀 불러오기 = 수식 문서 · 단가산출 · 일위대가 하나씩 개인 층으로 · 원문 패널 기초 찾기 + 「어디서 왔나」 카드

- 임도품셈 판 대응표 CSV 새 자리(STmate/old/30_원자료) — 불러오기 시험 26 실패 → 0
- 가져온 식 SK = 수식 문서(멈춘 wip/802-4b-1 M02_Doc_Import 를 메인 판에 다시 얹음 · 빈 산근 = 엑셀 0 원 줄) —
  줄 쌓기 「수식」 탭 길 열림 · 셈 = 단가기준(엑셀 값 기억 | 연결 값) · 화면 캐시 기록 · 수식 길 저장 `단가기준`
- 1,021 블록 전수(실무 여덟 + 표본 밖 셋) 엑셀 검산 · 엑셀 전후 · 우리 전후 모두 같음 · 셈됨 줄지 않음
- 옮기기 도구 「가져온식」 상태 걷음(가져온 줄 문서로 대조)
- [내 일위대가로] = 호표 하나 + 하위 → 개인 층 SC · 부르는 단산 = 가져온 식 SK(같은 내용 = 있는 것 · 못 이은 줄 = 고정)
- [내 단가산출로] = 단산 블록 판 → 개인 층 가져온 식 SK(개인 층이면 [가져온 식으로 열기] 숨김)
- 원문 패널(M01 로직 · M02 단가산출) 맨 위 「단가산출 기초 찾기」 · 가져온 식 SK 첫 화면 = 「어디서 왔나」 카드
  (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 · 엑셀 글의 품셈 절 글만)
- 수식 탭 [엑셀 단가 그대로 | 우리 단가로](단가산출 화면 · 저장 뒤 페이지 문서 단가기준 맞춤)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 17:11:09 +09:00
co-authored by Claude Opus 5.5
parent 5a0144004d
commit 9c803024e0
25 changed files with 1465 additions and 58 deletions
+53 -7
View File
@@ -137,6 +137,9 @@ class Builder:
self.src, self.dry, self._sk = Path(source), False, None
self.same, self.reused = lsame.Same(), False # 같은 내용 SC 찾기(한 요청 메모)
self.strict = True # 남은 줄 있으면 409(붙여넣기 `build_all` = 거짓)
self.lenient = (
False # 하나 들이기(807-1) = 이름 이음은 하되 남은 줄 = 고정 줄(엑셀 숫자 그대로)
)
self.doc_id = ledger.doc_id(source, key) # 739-1 이 불러오기에서 이은 짝
# ── 재검사 ───────────────────────────────────────────────────────
@@ -300,7 +303,7 @@ class Builder:
doc["본뜬"]["품셈"] = poomsem
if self.dry:
return {"남은줄": left}
if left and save and self.strict:
if left and save and self.strict and not self.lenient:
raise store.StoreError(409, {"까닭": sangeun.LEFT, "남은줄": left})
fixed_n = sum(1 for x in lines if "고정" in x)
links = sum(1 for x in lines if any(((x.get("고정") or {}).get("이음") or {}).values()))
@@ -315,6 +318,7 @@ class Builder:
ref, again, self.reused = hkids.save(doc, self.same)
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub}
out["남은줄"] = left
out["장부"] = {"씀": wrote}
return {**out, "줄수": {**count, "이음후보": linker.near}}
@@ -365,8 +369,8 @@ class Builder:
}
def _order(units: dict[int, dict[str, Any]]) -> list[int]:
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음."""
def _order(units: dict[int, dict[str, Any]], start: int | None = None) -> list[int]:
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음 · `start` = 그 호표와 하위만."""
out: list[int] = []
seen: set[int] = set()
@@ -380,20 +384,54 @@ def _order(units: dict[int, dict[str, Any]]) -> list[int]:
visit(int(ref.get("번") or 0))
out.append(no)
for no in sorted(units):
for no in [start] if start is not None else sorted(units):
visit(no)
return out
def _imported(folder: Path, source: Path, key: str, nos: list[int], units: dict) -> dict[str, str]:
"""하나 들이기(807-1) — 호표들이 부르는 단산 블록 = 이 층 가져온 식 SK(`FromHopyo.make` · 같은 내용 =
있는 것) · 이미 본뜬 SK 가 있는 블록 · 산근 시트 없음 · 못 만듦 = 건너뜀(그 줄 = 고정) → {본뜬 키: SK}."""
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
if not fh.has(source, key):
return {}
have, out = sangeun.made(folder, key, refs.where(source)), {}
for no in nos:
for line in units[no].get("줄") or []:
ref = line.get("참조") or {}
if ref.get("종류") != "단산" or not ref.get("번"):
continue
mark_ = sangeun.mark(key, int(ref["번"]))
if mark_ in have or mark_ in out:
continue
try:
out[mark_] = fh.make(source, key, int(ref["번"]))["이름"]
except store.StoreError:
continue
return out
def build_all(
folder: Path, source: Path, key: str, who: Any = None, strict: bool = False
folder: Path,
source: Path,
key: str,
who: Any = None,
strict: bool = False,
only: int | None = None,
) -> dict[str, Any]:
"""[모두 고정으로 들여오기](705-3) — 시트 호표 모두 · 하위 먼저 · 줄 = 고정(하위 = 참조) ·
`strict`(불러오기 길 · 729-2) = 남은 줄 있는 호표는 안 만듦(`실패 {번, 까닭, 남은줄}`) ·
붙여넣기(705-2)는 막지 않음 ·
이미 확인된 호표는 덮지 않음(`그대로`) → `{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보,
같음, 그대로, 다름 [{번, 이름, 엑셀, 우리, 차이}], 실패 [{번, 까닭}], 만듦 [{번, 이름, 키}]}`."""
같음, 그대로, 다름 [{번, 이름, 엑셀, 우리, 차이}], 실패 [{번, 까닭}], 만듦 [{번, 이름, 키}]}`.
`only`(807-1 [내 일위대가로]) = 그 호표 + 하위만 · 부르는 단산 = 가져온 식 SK(`단가산출`) ·
못 이은 줄 = 고정 줄(엑셀 숫자 그대로 · 만듦 `남은줄` 수)."""
units = {int(h["번"]): h for h in hopyo_store.read(source, key).get("호표") or []}
if only is not None and int(only) not in units:
raise _bad(f"없는 호표 「제{only}호표」", 404)
nos = _order(units, None if only is None else int(only))
sks = _imported(folder, source, key, nos, units) if only is not None else {}
sums = dict.fromkeys(("품셈", "고정", "호표", "빠짐", "이음", "이음후보"), 0)
out: dict[str, Any] = {
"호표": len(units),
@@ -405,12 +443,17 @@ def build_all(
"만듦": [],
}
shared, failed = lsame.Same(), set()
for no in _order(units):
if only is not None:
out["단가산출"] = sorted(set(sks.values()))
for no in nos:
out["줄"] += len(units[no].get("줄") or [])
bad = next((k for k in hkids.kids_of(units[no]) if k in failed), None)
try:
maker = Builder(folder, source, key, no, who)
maker.strict, maker.same = strict, shared
if only is not None:
maker.lenient = True
maker._sk = {**sangeun.made(folder, key, maker.source), **sks}
have = maker.done.get(mark(key, no))
if bad is not None and not have: # 777-1 — 하위 실패 연쇄 까닭 따로
raise store.StoreError(409, {"까닭": f"하위 제{bad}호표 실패", "하위": bad})
@@ -423,7 +466,10 @@ def build_all(
for k in sums:
sums[k] += made["줄수"][k]
got = compare(ref, maker.hopyo)
left = len(made.get("남은줄") or []) if not have else 0
out["만듦"].append({"번": no, "이름": units[no].get("이름"), "키": ref["키"]})
if only is not None:
out["만듦"][-1]["남은줄"] = left
except store.StoreError as e:
why = e.detail if isinstance(e.detail, dict) else {"까닭": str(e.detail)}
out["실패"].append({"번": no, **why})