fix(729-5): 별칭 체크 줄이 못옮김 남은 차이 SK 를 가리키면 남은 줄 · 만들기 응답 남은못옮김

- M02_Hopyo_SkLink.kept: 확인한 로직 줄이 차이 SK 바탕일 때 그 SK 에 못옮김이 남으면 남은 줄 {까닭, 단산, SK, 못옮김}(sk_line 과 같은 규칙) — hopyo-unlinked 에 나오고 hopyo-build 409
- M02_Hopyo_Sangeun.build: 응답에 남은못옮김 [{번, 이름, 키, 못옮김}](그대로 SK 중 못옮김 남은 것) · 잇기만 보냈는데 1위 기초가 고름 대기면 그 SK 바탕으로 다시 셈(고른 기초 SK 잇기)
- 시험: test_729 에 별칭 체크 줄 · 그대로 SK 남은못옮김 · 잇기 뒤 사라짐 · 차이 SK 참조

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 12:13:54 +09:00
co-authored by Claude Opus 5.5
parent ee2c5b1de3
commit 0c8c31cadd
4 changed files with 91 additions and 14 deletions
+4 -1
View File
@@ -315,7 +315,10 @@ class Builder:
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
return
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
if (kept := sklink.kept(self, line, one, qty, alias, memo)) is not None:
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:
+30 -4
View File
@@ -7,7 +7,9 @@
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) →
사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면
그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`).
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) ·
그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면
그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
@@ -136,6 +138,20 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
}
def _old_base(folder: Path, key: str) -> dict[str, Any]:
"""이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것."""
try:
doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes())
except (OSError, ValueError):
return {}
base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}}
return {
**base,
**({"환산": doc["환산"]} if doc.get("환산") else {}),
"본뜬": doc.get("본뜬") or {},
}
def _alias(line: dict, plan: dict) -> dict[str, Any]:
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
clauses = (line.get("근거") or {}).get("출처") or []
@@ -182,7 +198,8 @@ class Maker:
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
환산?}], 그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고름."""
환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}],
별칭}` · `picks` = 고름."""
chosen = _picks(picks)
maker = Maker(folder, key, who)
bad = sorted(set(chosen) - set(maker.first))
@@ -193,22 +210,31 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
have = made(folder, key, maker.here)
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []}
out["남은못옮김"] = []
out.update({skpatch.BASE: 0, skpatch.SK: 0})
books, alias = ledger.books(folder), {}
for no in sorted(maker.first):
old = have.get(mark(key, no))
line = maker.first[no]
name = maker.blocks[no].get("이름") or line.get("이름")
if old and no not in chosen:
out["그대로"] += 1
lost = _old_base(folder, old).get("본뜬", {}).get("못옮김")
if lost:
out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost})
continue
plan = maker.plan(no, chosen.get(no))
line = maker.first[no]
if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"):
base = _old_base(folder, old) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로
if base.get("로직"):
pick = {k: v for k, v in base.items() if k != "본뜬"}
plan = maker.plan(no, {**pick, **chosen[no]})
if plan["갈래"] == skpatch.WAIT:
head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")}
out["고름대기"].append({**head, "단위": line.get("단위"), **plan})
continue
got = maker.write(no, plan, old)
out[plan["갈래"]] += 1
name = maker.blocks[no].get("이름") or line.get("이름")
made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]}
out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})})
found = ledger.recall(line, folder, books)
+17 -9
View File
@@ -3,7 +3,7 @@
`M02_Hopyo_Build.Builder` 가 부름(`b` = 그 만들기) —
확인 안 한 단가산출 줄 = 그 블록 SK 참조(이 층 · 같은 원본 · 없으면 `M02_Hopyo_Sangeun.Maker` 로 그 블록만
만듦 · 미리 보기 = 키만 · 맞춤 셈 전 · 고름 대기 · 단위 다름 = 못 이음) ·
확인한 로직 줄이 차이 SK 의 바탕 = 그 SK 참조(엑셀 차이 보존 · 별칭 짝 = 기초까지만) ·
확인한 로직 줄이 차이 SK 의 바탕 = 그 SK 참조(엑셀 차이 보존 · 별칭 짝 = 기초까지만 · 못옮김 남으면 남은 줄 · 729-5) ·
남은 줄 = 엑셀 줄 · 갈래 · 까닭 · 후보(단가산출 고름 대기) · 못옮김 남은 SK = 못 이음(`단산` 번 · `못옮김` ·
잇기 = [단가산출 만들기] `바탕[번].잇기` · 729-4).
"""
@@ -84,8 +84,7 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di
doc = sk_doc(b, key)
lost = (doc.get("본뜬") or {}).get("못옮김") if lost is None else lost
if lost: # 연결 안 되면 못 만듦(내역 20-4)
why = f"단가산출 못옮김 {len(lost)} — [단가산출 만들기]에서 이음"
return None, {"까닭": why, "단산": no, "SK": key, "못옮김": lost}
return None, _lost(no, key, lost)
unit = doc.get("단위") or line.get("단위")
if mm_unit(unit) != mm_unit(line.get("단위")):
return None, {"까닭": f"단가산출 단위 「{unit}」 다름"}
@@ -94,17 +93,26 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di
return _ref(key, line, alias, f"실무 {qty}{line.get('단위') or ''} · 단산{no}", qty), None
def kept(b: Any, line: dict, one: dict, qty: Decimal, alias: Any, memo: str) -> dict | None:
"""확인한 로직 줄이 그 블록 차이 SK 의 바탕 = 그 SK 참조 · 아니면 None."""
def _lost(no: int, key: str, lost: list) -> dict[str, Any]:
why = f"단가산출 못옮김 {len(lost)} — [단가산출 만들기]에서 이음"
return {"까닭": why, "단산": no, "SK": key, "못옮김": lost}
def kept(
b: Any, line: dict, one: dict, qty: Decimal, alias: Any, memo: str
) -> tuple[dict | None, dict | None]:
"""확인한 로직 줄이 그 블록 차이 SK 의 바탕 = (그 SK 참조, 못옮김 남으면 까닭) · 아니면 (None, None)."""
ref = line.get("참조") or {}
if ref.get("종류") != "단산" or one.get("몫") is not None:
return None
key = sks(b).get(sangeun.mark(b.key, int(ref.get("번") or 0)))
return None, None
no = int(ref.get("번") or 0)
key = sks(b).get(sangeun.mark(b.key, no))
doc = sk_doc(b, key) if key else {}
base = (doc.get("바탕") or {}).get("로직")
if (doc.get("본뜬") or {}).get("갈래") != sangeun.skpatch.SK or base != one["로직"]:
return None
return _ref(key, line, alias, f"{memo} · 차이 SK", qty)
return None, None
lost = doc["본뜬"].get("못옮김") # sk_line 과 같은 규칙 — 차이 잃지 않음(내역 20-4)
return _ref(key, line, alias, f"{memo} · 차이 SK", qty), _lost(no, key, lost) if lost else None
def left(line: dict[str, Any], why: dict | None) -> dict[str, Any]:
@@ -186,6 +186,46 @@ def test_단위_성격_다름_계수_못옮김_잇기_남으면_일위대가_막
assert master_hash() == before
def test_별칭_체크_줄_못옮김_SK_남은줄_그대로_SK_남은못옮김(client):
"""729-5 — 확인한 로직 줄이 못옮김 남은 차이 SK 의 바탕이면 남은 줄(차이 잃지 않음) · 만들기 409 ·
그대로 SK 의 못옮김 = 응답 남은못옮김 · 잇기 뒤 사라짐."""
before = master_hash()
folder = layers.personal_dir(7, 42)
key = _post(client).json()["키"]
url = f"{URL.format('personal')}/{key}/sangeun"
fit.run_job(folder, key)
made = {x["번"]: x for x in client.post(url, json={}).json()["만듦"]}
sk = made[60] # 제27호표 2째 줄 와이어메시바닥깔기 — SK(바탕 GC000665) · 엑셀만 와이어메쉬
assert sk["갈래"] == "SK" and len(sk["못옮김"]) == 1
check = {"차례": 2, "대상": [{"로직": "GC000665", "배": 1}]} # 별칭 자동 체크와 같은 꼴
body = {"층": "personal", "키": key, "번": 27, "줄": [check]}
def mine(rows: list) -> list:
return [x for x in rows if x["차례"] == 2]
left = client.post(LEFT, json=body).json()["남은줄"]
assert [(x["단산"], x["SK"], x["못옮김"]) for x in mine(left)] == [(60, sk["키"], sk["못옮김"])]
res = client.post(BUILD, json=body)
assert res.status_code == 409 and mine(res.json()["detail"]["남은줄"]) == mine(left)
# 그대로 SK 의 못옮김 = 응답 남은못옮김(만듦[].못옮김 과 같은 꼴)
again = client.post(url, json={}).json()
rest = {x["번"]: x for x in again["남은못옮김"]}
assert again["만듦"] == [] and set(rest) == {n for n, x in made.items() if x["못옮김"]}
assert rest[60] == {"번": 60, "이름": sk["이름"], "키": sk["키"], "못옮김": sk["못옮김"]}
cut = {"id": sk["못옮김"][0]["id"], "자재": "MT000010"} # 이음 = SK 더한줄(차이 남음)
linked = client.post(url, json={"바탕": {"60": {"잇기": [cut]}}}).json()
assert [(x["번"], x["키"], x["못옮김"]) for x in linked["만듦"]] == [(60, sk["키"], [])]
assert 60 not in {x["번"] for x in client.post(url, json={}).json()["남은못옮김"]}
left = client.post(LEFT, json=body).json()["남은줄"]
assert mine(left) == []
picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left]
ok = client.post(BUILD, json={**body, "줄": [check, *picks]})
assert ok.status_code == 200, ok.text
doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes())
assert any((x.get("참조") or {}).get("키") == sk["키"] for x in doc["담은로직"]) # 차이 SK 참조
assert master_hash() == before
def _eq(one: dict) -> str:
return str((one.get("입력값") or {}).get("기계") or "")