diff --git a/M02_MasterTemplete/M02_Hopyo_Marks.py b/M02_MasterTemplete/M02_Hopyo_Marks.py new file mode 100644 index 000000000..aefdb82a1 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Marks.py @@ -0,0 +1,63 @@ +"""M02 불러오기 엑셀 보기 하이라이트 재료(PLAN 779-2 · 요구서 621 3장 5) — 읽기만(파일 · storage 변화 0). + +블록 `[{번, 판정, SK, 갈래, 못옮김}]` = 단가산출 만들기 결과(`Sangeun.made`) + 맞춤셈 캐시(`Fit.result`)를 읽기만. +- `판정` = 그 블록을 부르는 첫 줄의 맞춤셈 판정 상태(맞음 · 다른 점 · 후보 …) · 안 불리거나 셈 전이면 None +- `SK` = 만든 SK 키 · 없으면 None / `갈래` · `못옮김` = 그 SK 본뜬(출처별) · 없으면 None · [] +줄 상태는 `Match.view` 호표마다 `줄상태 [[차례, 상태, 체크]]` · `만든`(SC 키)로 실려 감. + +칠 꼴(화면 779-4 가 정함 · 상태 이름 맞춤표 — 「이어짐」 = 확인 창과 한 규칙 `체크`): +| 칠 | 줄 상태 (`줄상태`) | 블록 (`판정` · `SK` · `못옮김`) | +| 초록 이어짐 | 맞음 · 별칭 · 호표(`체크`) · 체크한 다른 점 | SK 있음 + 못옮김 [] (갈래 「기초」 포함) | +| 주황 엑셀과 다름 | 다른 점 + `체크` | SK 있음 + 갈래 「SK」(차이) | +| 파랑 고를 것 | 후보 · `체크` 아닌 다른 점 · 호표(`체크` 아님) | SK 없음 + 판정 후보 · 고름 대기 | +| 빨강 못 이음 | 없음 · 노임 · (안 이은 줄 = 마법사 몫) | SK 있음 + 못옮김 남음 | +| 흐림 밖 | % · 뺌 | 판정 None (안 불리는 블록) | +| 띠 내가 고름 | 화면 캐시(서버 아님) | — | +최종 판정은 화면(`SheetMap`)이 이 재료 + 캐시 고름으로 내림 — 여기는 상태 이름을 그대로 실음. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun +from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store +from M02_MasterTemplete import M02_Labor_Ref as refs + + +def _origin(to: Path, sk: str, mark: str, here: refs.Here) -> dict[str, Any]: + try: + doc = json.loads((to / sangeun.KIND / f"{sk}.json").read_bytes()) + except (OSError, ValueError): + return {} + return sangeun.origin_of(doc, mark, here) + + +def blocks(folder: Path, key: str, to: Path | None = None) -> list[dict[str, Any]]: + """블록마다 `{번, 판정, SK, 갈래, 못옮김}` — `to` = 만든 SK 를 볼 층(없으면 파일 층).""" + folder, to = Path(folder), Path(to or folder) + hp = hopyo_store.read(folder, key) + here = refs.where(folder) + cache = (fit.result(folder, key) or {}).get("블록") or {} + have = sangeun.made(to, key, here) + all_, first = sangeun.calls(hp) + out = [] + for no in sorted(all_): + line = first.get(no) + entry = cache.get(fit.unit_key(line)) if line else None + mark = sangeun.mark(key, no) + sk = have.get(mark) + origin = _origin(to, sk, mark, here) if sk else {} + out.append( + { + "번": no, + "판정": ((entry or {}).get("판정") or {}).get("상태"), + "SK": sk, + "갈래": origin.get("갈래"), + "못옮김": origin.get("못옮김") or [], + } + ) + return out diff --git a/M02_MasterTemplete/M02_Hopyo_Match.py b/M02_MasterTemplete/M02_Hopyo_Match.py index 2a2bf69c7..01579d082 100644 --- a/M02_MasterTemplete/M02_Hopyo_Match.py +++ b/M02_MasterTemplete/M02_Hopyo_Match.py @@ -447,7 +447,8 @@ def units_of(doc: dict[str, Any]) -> dict[int, str]: def view(folder: Path, key: str, no: int | None = None, to: Path | None = None) -> dict[str, Any]: - """실무자료 보기 — `no` 없으면 호표마다 [체크 제안 수, 줄 수] · 있으면 그 호표 줄마다 맞춤(3-3) + """실무자료 보기 — `no` 없으면 호표마다 [체크 제안 수, 줄 수] · `줄상태 [[차례, 상태, 체크]]` · + `만든`(그 호표 SC 키 · 없으면 None) · 있으면 그 호표 줄마다 맞춤(3-3) · `to` = 만들 층(장부 · 확인된 사전조합 · 없으면 파일 층) · 맞춤셈이 없으면 뒤 셈을 넣음.""" lines = _line_mod() @@ -457,6 +458,11 @@ def view(folder: Path, key: str, no: int | None = None, to: Path | None = None) hopyos = [lines.hopyo_view(h, ctx, False) for h in doc.get("호표") or []] keep = ("번", "이름", "규격", "단위", "금액", "맞춤", "행", "연결할", "연결된") brief = [{k: h.get(k) for k in keep if k in h} for h in hopyos] + for one, h in zip(brief, hopyos): # 779-2 · 이미 센 줄 상태를 버리지 않음(덧 셈 0) + one["줄상태"] = [ + [i, x["맞춤"]["상태"], x["맞춤"]["체크"]] for i, x in enumerate(h["줄"]) + ] + one["만든"] = ctx.done.get(lines.hopyo_build.mark(key, int(h["번"]))) ready = [sum(h["맞춤"][0] for h in hopyos), sum(h["맞춤"][1] for h in hopyos)] head = {**hopyo_store.brief(doc), "검산": doc.get("검산"), "셈": ctx.calc} links = {k: sum(h[k] for h in hopyos) for k in ("연결할", "연결된")} diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index 8705b2a01..b2d861c4d 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -31,6 +31,7 @@ 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_Paste as hopyo_paste from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink @@ -622,3 +623,18 @@ def hopyo_to_item( 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)}) diff --git a/resources/tester/test_779_2_hopyo_marks.py b/resources/tester/test_779_2_hopyo_marks.py new file mode 100644 index 000000000..2aa2643ea --- /dev/null +++ b/resources/tester/test_779_2_hopyo_marks.py @@ -0,0 +1,78 @@ +"""779-2 불러오기 엑셀 하이라이트 재료 — 목록 `줄상태` 수 = 줄 수 · 상태 = 확인 창 상태 · +`hopyo-marks` 길 읽기만(층 폴더 파일 변화 0) · 블록 판정 = 맞춤셈 캐시 · SK = 만든 것.""" + +import hashlib + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.test_m02_hopyo_excel import NORTH, URL, _post, client # noqa: F401 + +# ruff: noqa: F811 +KEY = "HP-000001" + + +def _tree(root) -> dict[str, str]: + return { + str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() + for p in sorted(root.rglob("*")) + if p.is_file() + } + + +def _ready(client): + res = _post(client, data=NORTH.read_bytes(), name=NORTH.name) + assert res.status_code == 200 and res.json()["키"] == KEY + folder = layers.personal_dir(7, 42) + fit.run_job(folder, KEY) + return folder + + +def test_줄상태_수_줄_수_확인창_같음(client): + _ready(client) + base = URL.format("personal") + whole = client.get(f"{base}/{KEY}").json() + assert whole["호표"] + diff = {} + for h in whole["호표"]: + one = client.get(f"{base}/{KEY}/{h['번']}").json()["호표"] + assert len(h["줄상태"]) == len(one["줄"]) == h["맞춤"][1], h["번"] + for (i, state, check), line in zip(h["줄상태"], one["줄"]): + full = line["맞춤"]["상태"] + if state != full: + diff[(state, full)] = diff.get((state, full), 0) + 1 + assert check == line["맞춤"]["체크"] + assert h["만든"] is None # 확인된 사전조합 전 + assert [x[0] for x in h["줄상태"]] == list(range(len(h["줄상태"]))) + # 목록 = 줄 맞춤 `full=False`(덧 셈 0) — 이름 후보 안 찾은 줄(없음)은 「후보」로 옴 + assert set(diff) <= {("후보", "없음")}, diff + + +def test_marks_읽기만_판정_캐시_같음_SK(client): + folder = _ready(client) + base = URL.format("personal") + before = _tree(folder) + got = client.get(f"{base}-marks/{KEY}") + assert got.status_code == 200, got.text + rows = got.json()["블록"] + assert _tree(folder) == before # 읽기만 — 파일 변화 0 + cache = fit.result(folder, KEY)["블록"] + hp = client.get(f"{base}-grid/{KEY}") # 길 살아 있음(블록 수는 파일에서) + assert hp.status_code == 200 + assert rows and [r["번"] for r in rows] == sorted(r["번"] for r in rows) + judged = [r for r in rows if r["판정"]] + assert judged and all(r["판정"] in ("맞음", "다른 점", "후보", "없음") for r in judged) + assert {r["판정"] for r in judged} <= {e["판정"]["상태"] for e in cache.values()} + assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows) + # [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림 + made = client.post(f"{base}/{KEY}/sangeun") + assert made.status_code == 200, made.text + after = {r["번"]: r for r in client.get(f"{base}-marks/{KEY}").json()["블록"]} + for one in made.json()["만듦"]: + row = after[one["번"]] + assert (row["SK"], row["갈래"], row["못옮김"]) == (one["키"], one["갈래"], one["못옮김"]) + assert sangeun.made(folder, KEY) + snap = _tree(folder) + client.get(f"{base}-marks/{KEY}") + assert _tree(folder) == snap + assert client.get(f"{base}-marks/HP-999999").status_code in (404, 400)