"""Review summary for the one-screen confirmation (2026-10-20): what changed in a bundle, where its values came from, and the reason text the app writes into the confirmation record. Pure functions (no I/O); the web layer (web/app.py `review`) supplies the data. Why: confirming 20+ bundles one by one took too long, the reasons written under time pressure were empty ('12', 'asf'), and it was hard to see what changed. The confirmation stays a HUMAN act (nothing is confirmed without the person's ticks and name); the app only prepares what the person looks at and writes an informative reason from facts it knows (기획서 4.3 / 6.9). """ import json from engine import verify MAX_EXAMPLES = 3 def _short(v): s = json.dumps(v, ensure_ascii=False) if not isinstance(v, (int, float, str)) else str(v) return s if len(s) <= 24 else s[:21] + "…" def bundle_change(value, origin_value): """Change of a bundle against the pier's creation snapshot: {mode, n_changed, n_total, examples:[(path, old, new)], text}. mode 'none' when there is no snapshot (older piers), 'same' when nothing changed.""" if origin_value is None: return {"mode": "none", "n_changed": None, "n_total": None, "examples": [], "text": "생성 시점 값 기록이 없어 무엇이 바뀌었는지 비교하지 못함"} old, new = dict(verify._walk(origin_value)), dict(verify._walk(value)) paths = sorted(set(old) | set(new)) diff = [p for p in paths if old.get(p) != new.get(p)] if not diff: return {"mode": "same", "n_changed": 0, "n_total": len(paths), "examples": [], "text": f"생성 시점 값 그대로({len(paths)}개 값)"} ex = [(".".join(p), old.get(p), new.get(p)) for p in diff[:MAX_EXAMPLES]] txt = f"생성 시점 대비 {len(diff)}/{len(paths)}개 값이 바뀜 — " + "; ".join(f"{p}: {_short(o)} → {_short(n)}" for p, o, n in ex) + (" …" if len(diff) > MAX_EXAMPLES else "") return {"mode": "changed", "n_changed": len(diff), "n_total": len(paths), "examples": ex, "text": txt} def provenance(which, key, read_log=None, anchor_log=None, candidates_log=None): """Facts about where values of the bundle were written from (tools of the app) -> list of short sentences.""" out = [] pre = f"{which}:{key}." for e in read_log or []: n = sum(1 for f in e.get("fields", []) if f.startswith(pre)) if n: out.append(f"도면에서 읽기 {n}건 반영 ({e.get('at', '')[:16]} {e.get('by') or '-'})") for e in anchor_log or []: n = sum(1 for f in e.get("fields", []) if isinstance(f, dict) and f.get("field", "").startswith(pre)) if n: out.append(f"표 밖 치수 후보 {n}건 반영 ({e.get('at', '')[:16]})") return out def not_checked(checks, st=None): """What the app did NOT verify, one sentence each: the checks that are 근거 없음 / 주의 / 불일치 (id + text); when none of the checks says so (a status that comes from 'counts only' or from the lack of any evidence) the general statement.""" out = [f"{c['id']} {c['text']}" for c in checks if c["st"] in ("n", "w", "b")] if not out and st == "w": out = ["수량만 확인했고 위치·좌표·치수는 도면과 직접 대조하지 못함"] if not out and st == "n": out = ["앱이 독립적으로 대조할 근거가 없음 — 도면과 직접 대조"] return out def flags(item): """Things the person should know before ticking a bundle in bulk (shown as chips, written into the reason): the bundle is still the template's original, or it carries a provisional registration that waits for the designer.""" out = [] if item["change"]["mode"] == "same" and item["st"] in ("n", "w"): out.append("템플릿 원본 그대로(새 도면 근거 없음)") if any("설계자 확인" in d for d in item.get("detail") or []): out.append("설계자 확인 전 임시 등록 포함") return out def group_of(st): """review group: A 일치, B 주의·근거 없음, D 사람 결정 기록 / 참고, C 불일치 (cannot be confirmed)""" return {"g": "A", "w": "B", "n": "B", "x": "D", "b": "C"}.get(st, "B") def auto_reason(item, note=""): """The reason text written into the confirmation record: facts the app knows + the person's note. Never empty.""" st = item["st"] head = "[검토 요약 일괄 확정] " if st == "g": why = head + f"앱 확인 일치 — {item['summary']}" else: why = head + f"사용자가 도면과 대조했다고 표시함. 앱 확인 {verify.ST_NAME.get(st, st)} — {item['summary']}" if item["change"]["mode"] in ("changed", "same"): why += f" | 변경: {item['change']['text']}" if item["prov"]: why += " | 출처: " + "; ".join(item["prov"]) if item.get("flags"): why += " | 주의 표시: " + "; ".join(item["flags"]) if note.strip(): why += f" | 의견: {note.strip()}" return why[:900]