- 부모 저장 때 확인 안 된 하위 호표(깊이 끝까지)를 하위 먼저 그 체크대로 함께 만듦(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>
248 lines
12 KiB
Python
248 lines
12 KiB
Python
"""M02 실무 호표가 다른 호표를 부를 때 = 일위대가 둘(PLAN 777-1 · 요구 621-4 · 요구 581 「SK · SC 서로 독립」).
|
|
|
|
하위 함께 만들기 `first` — 부모 저장(`M02_Hopyo_Build.build` · [구조물 조합으로])에서 확인 안 된 하위 호표(호표 줄 ·
|
|
호표 대상 · 깊이 끝까지)를 하위 먼저 그 하위의 서버 체크 제안 몸(`proposal`)으로 만듦(409 「먼저 확인」 대신) ·
|
|
부모 줄 = 하위 사본(740-1 그대로) · 하위 실패 = 409 `{까닭 「하위 제N호표 실패 — …」, 하위 N, 하위남은줄}`.
|
|
화면 캐시 고름은 화면이 하위를 먼저 보냄(`M02_MasterTemplete_Hopyo_Kids.ts`).
|
|
`proposal` 짝 = 화면 `bodyFor` · `wholeFor`(`M02_MasterTemplete_Hopyo_Kids.ts` · 캐시 빈 것) — 체크 제안 줄 · 대상 꼴 같음.
|
|
같은 내용 일위대가 재사용 `save` — 같은 본뜬(키 · 원본)이면 그 SC(혼자 쓰면 덮음 · 나눠 쓰면 내용 같을 때만) ·
|
|
아니면 이 층 같은 이름 · 같은 내용 SC(`M02_Library_Same.find_same` + 수량까지) 의 `본뜬.또` 에 출처를 더해 씀
|
|
(SK 꼴 `M02_Hopyo_Sangeun.Maker.write`) · 없으면 새로 → 다시 불러와도 SC 가 안 늚.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
|
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
|
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 entries(folder: str | Path):
|
|
"""층 사전조합 실무자료 출처 → (키, 본뜬 · 또 한 칸, 판) — 또 = 같은 내용으로 이 SC 를 쓴 뒤 출처."""
|
|
for path in sorted((Path(folder) / KIND).glob("SC-*.json")):
|
|
try:
|
|
raw = path.read_bytes()
|
|
doc = json.loads(raw)
|
|
except (OSError, ValueError):
|
|
continue
|
|
got = doc.get("본뜬") if isinstance(doc, dict) else None
|
|
if isinstance(got, dict) and got.get("층") == ORIGIN:
|
|
for one in [got, *(x for x in got.get("또") or [] if isinstance(x, dict))]:
|
|
yield path.stem, one, raw
|
|
|
|
|
|
def same_source(got: dict[str, Any], source: refs.Here | None) -> bool:
|
|
"""본뜬 원본 층이 같음 — 원본 없는 옛 것 · `source` None 은 가리지 않음."""
|
|
have = got.get("원본")
|
|
return source is None or not have or tuple(have) == tuple(source)
|
|
|
|
|
|
def _bare(value: Any) -> Any:
|
|
"""사본 블록 `주인`(몸 지문 · 메모까지 셈) 뺌 — 셈 내용은 `몸` 이 다 가짐."""
|
|
if isinstance(value, list):
|
|
return [_bare(x) for x in value]
|
|
if not isinstance(value, dict):
|
|
return value
|
|
return {k: _bare(x) for k, x in value.items() if not (k == "주인" and "몸" in value)}
|
|
|
|
|
|
def _rows(doc: dict[str, Any], part: str) -> list[str]:
|
|
return sorted(json.dumps(lsame.norm(_bare(x)), ensure_ascii=False, sort_keys=True) for x in doc.get(part) or []) # fmt: skip
|
|
|
|
|
|
def body(doc: dict[str, Any]) -> list:
|
|
"""셈 내용 그대로(수량까지 · 하위 = 펼친 사본 몸) — `find_same` 지문은 SC 수량을 안 봄."""
|
|
flat = lflat.flat_doc(KIND, doc)
|
|
head = [lsame.unit(flat.get("단위")), lsame.norm(flat.get("규격"))]
|
|
return [*head, _rows(flat, "담은로직"), _rows(flat, "자재")]
|
|
|
|
|
|
def _entry(doc: dict[str, Any]) -> dict[str, Any]:
|
|
return {**doc["본뜬"], "이름": doc["이름"]}
|
|
|
|
|
|
def _split(old: dict[str, Any], mine: dict[str, Any]) -> tuple[dict, list]:
|
|
got = old.get("본뜬") or {}
|
|
own = {k: v for k, v in got.items() if k != "또"}
|
|
rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)]
|
|
return own, rest
|
|
|
|
|
|
def _put(key: str, raw: bytes, old: dict, own: dict, also: list, doc: dict | None = None) -> None:
|
|
new = {**(doc or old), "본뜬": {**own, "또": also} if also else own}
|
|
if new != old:
|
|
store.write(KIND, key, store.version_of(raw), new)
|
|
|
|
|
|
def _empty(root: Path, doc: dict[str, Any]) -> list[str]:
|
|
"""빈 호표(줄 · 자재 없음 = 지문 없음) — 이름 · 단위 같은 실무자료 SC."""
|
|
if doc.get("담은로직") or doc.get("자재"):
|
|
return []
|
|
out = []
|
|
for path in sorted((root / KIND).glob("SC-*.json")):
|
|
try:
|
|
got = json.loads(path.read_bytes())
|
|
except (OSError, ValueError):
|
|
continue
|
|
same = (got.get("이름"), lsame.unit(got.get("단위"))) == (doc["이름"], lsame.unit(doc.get("단위"))) # fmt: skip
|
|
if same and (got.get("본뜬") or {}).get("층") == ORIGIN:
|
|
out.append(path.stem)
|
|
return out
|
|
|
|
|
|
def save(doc: dict[str, Any], same: lsame.Same | None = None) -> tuple[dict[str, Any], bool, bool]:
|
|
"""(참조, 다시씀, 재사용) — 엑셀 수량 = 저장(742-1) · `same` = 한 요청 안 지문 메모."""
|
|
hands = {
|
|
"줄": {str(i): x["수량"]["손"] for i, x in enumerate(doc["담은로직"]) if x.get("수량")},
|
|
"자재": {m["코드"]: m["수량"]["손"] for m in doc.get("자재") or [] if m.get("수량")},
|
|
}
|
|
doc = {**doc, "본뜬": {**doc["본뜬"], "실무수량": hands}}
|
|
same, root, mine = same or lsame.Same(), store.root(), doc["본뜬"]
|
|
here = refs.here()
|
|
ref = lambda key: {"층": here[0], "주인": here[1], "키": key} # noqa: E731
|
|
have = next(
|
|
(
|
|
(key, raw)
|
|
for key, got, raw in entries(root)
|
|
if got.get("키") == mine["키"] and same_source(got, tuple(mine["원본"]))
|
|
),
|
|
None,
|
|
)
|
|
if have:
|
|
key, raw = have
|
|
old = json.loads(raw)
|
|
own, rest = _split(old, mine)
|
|
owner, alike = _same_mark(own, mine), body(old) == body(doc)
|
|
if owner and (not rest or alike): # 혼자 · 내용 같음 = 제자리 덮음(나눈 출처 그대로)
|
|
_put(key, raw, old, mine, rest, doc)
|
|
same.add(root, KIND, key, doc)
|
|
return ref(key), True, False
|
|
if alike: # 남의 SC 를 같은 내용으로 씀 — 제 출처 칸만
|
|
_put(key, raw, old, own, [*rest, _entry(doc)])
|
|
return ref(key), True, False
|
|
head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own
|
|
_put(key, raw, old, head, rest[1:] if owner else rest) # 내용 달라짐 = 떼어 냄
|
|
for key in same.find_same(root, KIND, doc) or _empty(root, doc):
|
|
raw = (root / KIND / f"{key}.json").read_bytes()
|
|
old = json.loads(raw)
|
|
if old.get("이름") == doc["이름"] and body(old) == body(doc): # 이름까지 = 같은 호표
|
|
own, rest = _split(old, mine)
|
|
_put(key, raw, old, own, [*rest, _entry(doc)])
|
|
return ref(key), False, True
|
|
key = refs.create(KIND, doc)["이름"]
|
|
same.add(root, KIND, key, doc)
|
|
return ref(key), False, False
|
|
|
|
|
|
# ── 하위 함께 만들기 ─────────────────────────────────────────────────
|
|
def kids_of(hopyo: dict[str, Any]) -> list[int]:
|
|
"""호표 줄이 부르는 호표 번(줄 차례)."""
|
|
out = []
|
|
for line in hopyo.get("줄") or []:
|
|
ref = line.get("참조") or {}
|
|
if finger.kind_of(line) == "호표" or ref.get("종류") == "일위":
|
|
out.append(int(ref.get("번") or 0))
|
|
return out
|
|
|
|
|
|
def order(units: dict[int, dict[str, Any]], no: int, also: Any = ()) -> list[int]:
|
|
"""`no` 아래 하위 호표(+ `also` 와 그 아래) — 하위 먼저 · `no` 뺌 · 고리는 끊음."""
|
|
out: list[int] = []
|
|
seen = {int(no)}
|
|
|
|
def visit(at: int) -> None:
|
|
for kid in kids_of(units.get(at) or {}):
|
|
if kid not in seen and kid in units:
|
|
seen.add(kid)
|
|
visit(kid)
|
|
out.append(kid)
|
|
|
|
visit(int(no))
|
|
for kid in also:
|
|
if kid not in seen and kid in units:
|
|
seen.add(kid)
|
|
visit(kid)
|
|
out.append(kid)
|
|
return out
|
|
|
|
|
|
def _target(p: dict[str, Any]) -> dict[str, Any]:
|
|
"""대상 → 만들기 몸 대상(화면 `toTarget` 짝)."""
|
|
times = 1 if p.get("배") is None else p["배"]
|
|
if p.get("호표") is not None:
|
|
return {"호표": p["호표"], "배": times}
|
|
link = next((k for k in ("노임", "기계", "비율") if p.get(k)), None)
|
|
if link:
|
|
return {link: p[link], "배": 1}
|
|
head = (
|
|
{"로직": p["로직"], "입력값": p.get("입력값") or {}}
|
|
if p.get("로직")
|
|
else {"전국": True}
|
|
if p.get("전국")
|
|
else {"자재": p.get("자재")}
|
|
)
|
|
return {**head, "배": times, **({"몫": p["몫"]} if p.get("몫") is not None else {})}
|
|
|
|
|
|
def proposal(hopyo: dict[str, Any]) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
"""서버 체크 제안 몸 (줄, 통째) — 보기(`M02_Hopyo_Line.hopyo_view`) 체크 제안 줄만(화면 `bodyFor` 짝)."""
|
|
whole = hopyo.get("통째")
|
|
if whole and whole.get("체크"):
|
|
return [], {"대상": [_target(p) for p in whole.get("대상") or []]}
|
|
lines = []
|
|
for line in hopyo.get("줄") or []:
|
|
got = line.get("맞춤") or {}
|
|
parts = got.get("대상") if isinstance(got.get("대상"), list) else []
|
|
if got.get("상태") == "%":
|
|
ok = bool(parts and parts[0].get("비율"))
|
|
else:
|
|
ok = bool(parts) and all((p.get("배") or 0) > 0 for p in parts)
|
|
if got.get("체크") and ok:
|
|
lines.append({"차례": line["차례"], "대상": [_target(p) for p in parts]})
|
|
return lines, None
|
|
|
|
|
|
def first(
|
|
folder: Path, source: Path, key: str, no: int, who: Any, lines: Any = ()
|
|
) -> list[dict[str, Any]]:
|
|
"""확인 안 된 하위 호표(호표 줄 + 몸 `lines` 의 호표 대상)를 하위 먼저 서버 체크 제안대로 만듦 →
|
|
`[{번, 키}]`(새로 만든 것) ·
|
|
실패 = 409 `{까닭 「하위 제N호표 실패 — …」, 하위, 하위남은줄}`(만든 하위는 남음)."""
|
|
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
|
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
|
|
|
units = {int(h["번"]): h for h in hopyo_store.read(source, key).get("호표") or []}
|
|
also = [
|
|
t["호표"] for x in lines or [] for t in x.get("대상") or [] if t.get("호표") is not None
|
|
]
|
|
todo = order(units, no, also)
|
|
done = hb.confirmed(folder, refs.where(source)) if todo else {}
|
|
made: list[dict[str, Any]] = []
|
|
for kid in todo:
|
|
if hb.mark(key, kid) in done:
|
|
continue
|
|
try:
|
|
lines, whole = proposal(hm.view(source, key, kid, to=folder)["호표"])
|
|
maker = hb.Builder(folder, source, key, kid, who)
|
|
got = maker.build(*maker.check(lines, whole))
|
|
except store.StoreError as e:
|
|
why = e.detail if isinstance(e.detail, dict) else {"까닭": str(e.detail)}
|
|
left = why.get("남은줄") or []
|
|
detail = {"까닭": f"하위 제{kid}호표 실패 — {why.get('까닭')}", "하위": kid}
|
|
raise store.StoreError(409, {**detail, "하위남은줄": left}) from e
|
|
done[hb.mark(key, kid)] = got["참조"]["키"]
|
|
made.append({"번": kid, "키": got["참조"]["키"]})
|
|
return made
|