feat(777-1): 호표가 다른 호표를 부를 때 = 일위대가 둘 — 하위 함께 만들기 · 같은 SC 재사용 · 결과 글 · 납작 길 막음

- 부모 저장 때 확인 안 된 하위 호표(깊이 끝까지)를 하위 먼저 그 체크대로 함께 만듦(409 「먼저 확인」 걷음)
  · 서버 = 체크 제안 몸(M02_Hopyo_Kids.proposal ↔ 화면 bodyFor 짝) · 화면 = 확인 창 캐시 고름(Hopyo_Kids.ts)
  · 하위 실패 = 409 「하위 제N호표 실패 — …」 · 하위 · 하위남은줄 → 결과 표 [고르기]가 그 하위를 엶
  · [모두 고정] 연쇄 실패 까닭 따로 · 미리 보기는 하위 호표 줄을 이음으로 셈
- 같은 엑셀 다시 불러오기 = 같은 이름 · 같은 내용(수량까지) SC 재사용 — 본뜬.또(SK 꼴) · 끝내기는 그 몫만 뺌
- 결과 표 글 「SC-12 · 하위 제4호표 SC-11(사본 담음)」 · 확인 창 호표 줄 글 「하위 일위대가 SC-11 사본」
- [구조물 조합으로] 도 하위 먼저(못 만들면 지금처럼 고정 줄) · 붙여넣기 · B09 통째 = 이미 하위 먼저(안 고침)
- 엑셀 대조(CUTS · _excel_round · excel_given · compare) → 새 M02_Hopyo_Compare.py(Build 608 → 540)
- M02_Labor_Router 병행 흔적 가드 걷음(_later · inspect · 몸 칸 새로)
- STmate [엑셀 보기] 단가산출근거로 감(형식별 블록 시트 이름)
- 낡은 시험 고침: 「제46호표 먼저 확인」 409 · 글 → 하위 함께(777-1 정함) · 718-1 실패 = 남은줄 또는 하위 ·
  화면 시험 StaleError 안내 → unmadeKids · leftAt · 새 지킴 시험 test_777_1_hopyo_kids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 09:24:48 +09:00
co-authored by Claude Opus 5.5
parent 4e0ea28aec
commit eaa8064f1a
17 changed files with 740 additions and 171 deletions
+5 -1
View File
@@ -28,7 +28,11 @@ def test_이어서_끝내기_지움_겹침_막음(client):
assert _keys(client) == ["HP-000001"]
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
n = len(made["만듦"]) # 729-2 — 남은 줄 있는 호표 = 실패(남은줄)
assert n and n + len(made["실패"]) == 68 and all(x["남은줄"] for x in made["실패"])
assert (
n
and n + len(made["실패"]) == 68
and all(x.get("남은줄") or x.get("하위") for x in made["실패"])
)
hs.remember(folder, {"장부 줄": {"갈래": "안 씀"}}) # 별칭 장부 = 끝내기 뒤 남음
cache = fit.cache_path(folder, "HP-000001")
cache.write_text("{}", encoding="utf-8")
+191
View File
@@ -0,0 +1,191 @@
"""777-1 호표가 다른 호표를 부를 때 = 일위대가 둘(요구 621-4 · `M02_Hopyo_Kids`).
- 하위 확인 전 부모 저장 = 409 없이 하위 · 부모 둘(하위 = 서버 체크 제안대로 · 부모 줄 = 하위 사본 · 답 `하위`)
- 하위가 남은 줄로 못 만들면 = 409 「하위 제N호표 실패」 · `하위` · `하위남은줄`(부모 · 하위 안 씀)
- 같은 엑셀 두 번([끝내기] 뒤 다시 올림) = 같은 내용 SC 재사용(`본뜬.또`) · SC 안 늚 · 끝내면 그 몫만 뺌
- 깊이 3 사슬 = 손자 → 자식 → 부모 차례로 · 부모 안 사본 안에 사본
- 서버 체크 제안 몸(`proposal`) = 화면 `bodyFor` 꼴(체크 줄만 · % = 밑수 고른 것만 · 통째)
층은 임시 폴더(공용 storage 안 씀).
"""
import json
import pytest
from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Forget as forget
from M02_MasterTemplete import M02_Hopyo_Kids as hk
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_705_3_hopyo_fixed import DOCS, FILES
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
BUILD = "/api/m02/layers/personal/hopyo-build"
LEFT = "/api/m02/layers/personal/hopyo-unlinked"
GEO = DOCS / FILES["거창"]
def _mine():
return layers.personal_dir(7, 42)
def _scs():
return sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json"))
def _doc(key):
return json.loads((_mine() / "precombo" / f"{key}.json").read_bytes())
def _decided(client, body):
left = client.post(LEFT, json=body).json()["남은줄"]
return {**body, "줄": [*body["줄"], *({"차례": x["차례"], "뺌": "우리에 없음"} for x in left)]}
def _geo(client, key="HP-000001"):
assert _post(client, data=GEO.read_bytes(), name=GEO.name).status_code == 200
fit.run_job(_mine(), key) # 숫자 판정 · 단가산출 맞춤셈(서버 체크 제안이 씀)
def test_하위_확인_전_부모_저장_둘_생김(client):
before = master_hash()
_geo(client)
body = _decided(client, {"층": "personal", "키": "HP-000001", "번": 3, "줄": []})
res = client.post(BUILD, json=body)
assert res.status_code == 200, res.text # 409 「제4호표 먼저 확인」 없음
got = res.json()
kid = hb.confirmed(_mine())["HP-000001 제4호표"]
assert got["하위"] == [{"번": 4, "키": kid, "새로": True}]
assert hb.confirmed(_mine())["HP-000001 제3호표"] == got["참조"]["키"] != kid
copy = next(x for x in _doc(got["참조"]["키"])["담은로직"] if "참조" in x)
assert copy["참조"]["층"] == "사본" and copy["참조"]["키"] == kid # 부모 줄 = 하위 사본(740-1)
again = client.post(BUILD, json=body).json() # 다시 저장 = 하위 그대로 · 부모 덮음
assert again["하위"] == [{"번": 4, "키": kid, "새로": False}] and again["다시씀"] is True
assert master_hash() == before
def test_하위_실패_까닭_따로_아무것도_안_씀(client):
assert _post(client).status_code == 200 # 북부청 임도 제45호표 → 제46호표(남은 줄 셋)
body = {"층": "personal", "키": "HP-000001", "번": 45, "줄": []}
res = client.post(BUILD, json=body)
assert res.status_code == 409, res.text
why = res.json()["detail"]
assert why["까닭"].startswith("하위 제46호표 실패 — ") and why["하위"] == 46
assert why["하위남은줄"] and "남은줄" not in why # 부모 [고르기]로 잘못 안 엶 — 하위 몫
assert hb.confirmed(_mine()) == {}
left = client.post(LEFT, json=body).json()["남은줄"] # 미리 보기 = 하위 호표 줄은 함께 만듦
assert all(x["갈래"] != "호표" for x in left)
def test_같은_엑셀_두_번_SC_안_늚(client):
assert _post(client).status_code == 200
folder = _mine()
with store.at(folder):
first = hb.build_all(folder, folder, "HP-000001")
made = _scs()
assert first["실패"] == [] and len(made) <= first["호표"] + 3
keys = {x["번"]: x["키"] for x in first["만듦"]}
forget.finish(folder, "HP-000001")
assert hb.confirmed(folder) == {} # 끝낸 뒤 = 출처만(키 뺌)
up = _post(client).json()
assert up["키"] == "HP-000002" and not up.get("이어서")
with store.at(folder):
second = hb.build_all(folder, folder, "HP-000002")
assert _scs() == made # 같은 내용 = 재사용 · 안 늚
assert {x["번"]: x["키"] for x in second["만듦"]} == keys
assert (second["같음"], second["다름"]) == (first["같음"], first["다름"])
shared = [k for k in made if any(x["키"].startswith("HP-000002 ") for x in _doc(k)["본뜬"].get("또", []))] # fmt: skip
assert shared and set(hb.confirmed(folder)) == {hb.mark("HP-000002", n) for n in keys}
forget.finish(folder, "HP-000002") # 그 몫만 뺌
assert _scs() == made and not any(_doc(k)["본뜬"].get("또") for k in made)
def test_깊이_3_사슬(client, monkeypatch):
_geo(client)
folder = _mine()
units = {int(h["번"]): h for h in hs.read(folder, "HP-000001")["호표"]}
deep = {
no: hk.order(units, no)
for no in units
if any(hk.kids_of(units[k]) for k in hk.kids_of(units[no]) if k in units)
}
top = min(deep)
plain = hk.proposal
def decided(hopyo): # 하위 남은 줄 = 「우리에 없음」(화면 [고르기] 몫을 시험이 대신)
lines, _ = plain({**hopyo, "통째": None}) # 줄로(통째면 하위 줄이 빠짐 — 사슬을 보려고)
body = {"층": "personal", "키": "HP-000001", "번": hopyo["번"], "줄": lines}
return _decided(client, body)["줄"], None
monkeypatch.setattr(hk, "proposal", decided)
body = _decided(client, {"층": "personal", "키": "HP-000001", "번": top, "줄": []})
res = client.post(BUILD, json=body)
assert res.status_code == 200, res.text
done = hb.confirmed(folder)
assert all(hb.mark("HP-000001", k) in done for k in deep[top]) # 손자까지 다 만듦
new = [x["번"] for x in res.json()["하위"] if x["새로"]]
assert new and set(new) <= set(deep[top])
def depth(doc):
inner = [
x["참조"]["몸"] for x in doc.get("담은로직") or [] if (x.get("참조") or {}).get("몸")
]
return 1 + max((depth(x) for x in inner), default=0)
assert depth(_doc(done[hb.mark("HP-000001", top)])) == 3 # 부모 안 사본 안에 사본
@pytest.mark.parametrize(
("view", "want"),
[
(
{
"줄": [
{
"차례": 0,
"맞춤": {
"체크": True,
"상태": "로직",
"대상": [{"로직": "GF1", "입력값": {"a": 1}, "배": 2, "몫": 0.5}],
},
}, # fmt: skip
{
"차례": 1,
"맞춤": {"체크": False, "상태": "로직", "대상": [{"로직": "GF2", "배": 1}]},
}, # fmt: skip
{"차례": 2, "맞춤": {"체크": True, "상태": "%", "대상": [{"비율": None}]}},
{
"차례": 3,
"맞춤": {"체크": True, "상태": "호표", "대상": [{"호표": 4, "배": None}]},
}, # fmt: skip
{
"차례": 4,
"맞춤": {"체크": True, "상태": "자재", "대상": [{"자재": "MT1", "배": 0}]},
}, # fmt: skip
{
"차례": 5,
"맞춤": {"체크": True, "상태": "자재", "대상": [{"전국": True, "배": 3}]},
}, # fmt: skip
]
},
(
[
{"차례": 0, "대상": [{"로직": "GF1", "입력값": {"a": 1}, "배": 2, "몫": 0.5}]},
{"차례": 5, "대상": [{"전국": True, "배": 3}]},
],
None,
),
),
(
{"줄": [], "통째": {"체크": True, "대상": [{"로직": "GF1", "배": 1}]}},
([], {"대상": [{"로직": "GF1", "입력값": {}, "배": 1}]}),
),
],
)
def test_서버_체크_제안_몸_화면_짝(view, want):
assert hk.proposal(view) == want
+5 -2
View File
@@ -1,6 +1,6 @@
"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · 338-3 · `M02_Hopyo_Build`) — 확인한 줄만 ·
몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 배 · 몫 · 자재 · 통째 ·
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 → 확인 뒤 참조 · 장부 씀 ·
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 실패 409(777-1) → 확인 뒤 참조 · 장부 씀 ·
`본뜬.확인` · 덮음 · 옛 즉시 확정 길 404 · 장부 길(보기 · 고치기 · 권한) · picks 확인 ·
master_data 원본 그대로. 층은 임시 폴더.
"""
@@ -244,7 +244,10 @@ def test_몸_없음_재검사_400(client):
def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
assert _post(client).status_code == 200
ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}]
assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"]
why = _build(client, 45, ref, want=409)[
"detail"
] # 777-1 — 하위 먼저 함께(제안대로) · 남은 줄로 실패
assert why["까닭"].startswith("하위 제46호표 실패") and why["하위"] == 46 and why["하위남은줄"]
child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}])
assert [x["까닭"] for x in child["빠진줄"]] == ["뺌 — 안 씀"] # % 줄 = 비율 고정 줄(705-3)
rate = _doc(layers.personal_dir(7, 42), child["참조"]["키"])["담은로직"][-1]["고정"]
+5 -1
View File
@@ -140,7 +140,11 @@ def test_새_상태_체크_제안(client):
assert dig["대상"][0]["로직"] == "GC000083" and len(dig["후보"]) == 5
assert "특별인부 엑셀 0.3542 · 우리 0.3704" in dig["꼬리표"]
ref = _hopyo(client, 45)["고화매트재료비"]
assert ref["상태"] == "호표" and ref["체크"] is False and ref["글"] == "제46호표 먼저 확인"
assert (
ref["상태"] == "호표"
and ref["체크"] is False
and ref["글"] == "제46호표 — 저장 때 하위로 함께 만듦"
) # 777-1
assert ref["대상"][0]["호표"] == 46 and ref["대상"][0]["단위"] == "㎡"
pct = _hopyo(client, 46)["잡재료비"]
assert pct["상태"] == "%" and pct["체크"] is False
+3 -1
View File
@@ -319,7 +319,9 @@ def test_옛_즉시_확정과_바로_만들기_없음():
save = sheet.split("async function save(")[1].split("async function makeAll(")[0]
assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save
assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save
assert "buildOrder(" in sheet and "error instanceof StaleError" in sheet # 하위 먼저 · 409 안내
assert (
"buildOrder(" in sheet and "unmadeKids(" in sheet and "leftAt(" in sheet
) # 하위 먼저 · 함께(777-1)
edit = sheet.split("const editButton =")[1].split("function paintList(")[0]
assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit
row = sheet.split("function paintList(")[1]