Files
Pier_v1/Code/교각모델링앱/tools/patches/2026-10-12_patch_batch.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

190 lines
8.3 KiB
Python

"""Patch (2026-10-12, user request): batch confirmation of required sections and batch run + auto-approval of
gates G1..G5. Approvals are recorded under the name typed by the user, note "일괄 실행 자동 승인"; only PASS
gates are approved and the batch stops at the first FAIL / error / blocker."""
from pathlib import Path
APP = Path(__file__).resolve().parents[2]
def patch(rel, reps):
p = APP / rel
s = p.read_text(encoding="utf-8")
for a, b in reps:
assert s.count(a) == 1, (rel, a[:70])
s = s.replace(a, b)
p.write_text(s, encoding="utf-8")
patch("web/app.py", [
("""ACTIVE = {} # job id -> thread of this server process""",
"""ACTIVE = {} # job id -> thread of this server process
BATCH = {} # pier name -> batch thread of this server process
AUTO_NOTE = "일괄 실행 자동 승인\""""),
(""" return redirect(request.form.get("next") or url_for("pier", name=name))
# ---------------- gates""",
""" return redirect(request.form.get("next") or url_for("pier", name=name))
def unconfirmed(p):
\"\"\"Required sections (all gates) that exist and are not confirmed, without duplicates.\"\"\"
out = []
for g in gate_ids():
for r in p.gate_requirements(g):
if r["exists"] and not r["confirmed"] and (r["file"], r["key"]) not in out:
out.append((r["file"], r["key"]))
return out
@app.post("/pier/<name>/confirm_all")
def confirm_all(name):
p = pier_or_404(name)
by = request.form.get("by", "").strip()
if not by:
flash("확정자 이름을 입력하세요.", "fail")
return redirect(url_for("pier", name=name))
need = unconfirmed(p)
for f, k in need:
p.confirm(f, k, by)
flash(f"필요한 묶음 {len(need)}개를 일괄 확정했습니다 ({by}).", "ok")
return redirect(url_for("pier", name=name))
# ---------------- gates"""),
("""@app.post("/pier/<name>/gate/<gate>/run")
def gate_run(name, gate):
p = pier_or_404(name)
if gate not in gate_ids():
abort(404)""",
"""def batch_state(p):
\"\"\"State of the batch run of this pier (실행 중 only while its thread lives in this server process).\"\"\"
b = store.rjson(p.run_dir / "batch.json")
if b and b["state"] == "실행 중":
t = BATCH.get(p.name)
if b.get("pid") != os.getpid() or not (t and t.is_alive()):
b = dict(b, state="중단됨")
return b
def _batch(p, by, gates_todo):
bp = p.run_dir / "batch.json"
log = []
def put(state):
store.wjson(bp, {"state": state, "by": by, "pid": os.getpid(), "at": store.now(), "log": log})
put("실행 중")
for g in gates_todo:
b = gate_blockers(p, g)
if b:
log.append(f"{g} 실행 불가: " + ", ".join(b))
return put("중단")
jid = uuid.uuid4().hex[:12]
store.wjson(p.run_dir / f"{g}_job.json", {"id": jid, "pid": os.getpid(), "state": "대기", "at": store.now(), "log": []})
ACTIVE[jid] = threading.current_thread()
log.append(f"{g} 실행 시작")
put("실행 중")
_gate_job(p, g, jid)
st = p.gate_status(g)
rep, job = st["report"] or {}, st["job"] or {}
if job.get("state") != "완료" or rep.get("job_id") != jid or rep.get("verdict") != "PASS":
log.append(f"{g} {rep.get('verdict') or job.get('state')} — 승인하지 않고 멈춤 (결과를 확인하세요)")
return put("중단")
p.set_approval(g, "승인", by, AUTO_NOTE)
s = rep["summary"]
log.append(f"{g} PASS {s['passed']}/{s['total']} (계획 FAIL {len(rep['planned_fails'])}) → 승인")
put("실행 중")
put("완료")
def anything_running(p):
if (batch_state(p) or {}).get("state") == "실행 중":
return True
return any(job_state(p.gate_status(g)["job"]) in ("대기", "실행 중") for g in gate_ids())
@app.post("/pier/<name>/gates/run_all")
def gates_run_all(name):
p = pier_or_404(name)
by = request.form.get("by", "").strip()
if not by:
flash("판정자 이름을 입력하세요.", "fail")
return redirect(url_for("gates", name=name))
if anything_running(p):
flash("이미 실행 중인 작업이 있습니다.", "fail")
return redirect(url_for("gates", name=name))
todo = [g for g in gate_ids() if not p.gate_status(g)["approved"]]
if not todo:
flash("모든 게이트가 이미 승인되어 있습니다.", "ok")
return redirect(url_for("gates", name=name))
b = gate_blockers(p, todo[0])
if b:
flash(f"{todo[0]} 실행 불가: " + ", ".join(b), "fail")
return redirect(url_for("gates", name=name))
store.wjson(p.run_dir / "batch.json", {"state": "실행 중", "by": by, "pid": os.getpid(), "at": store.now(),
"log": ["일괄 실행 대기"]})
t = threading.Thread(target=_batch, args=(p, by, todo), daemon=True)
BATCH[p.name] = t
t.start()
flash(f"{todo[0]}~{todo[-1]} 연속 실행을 시작했습니다. PASS인 게이트만 '{by}' 이름으로 자동 승인합니다.", "ok")
return redirect(url_for("gates", name=name))
@app.post("/pier/<name>/gate/<gate>/run")
def gate_run(name, gate):
p = pier_or_404(name)
if gate not in gate_ids():
abort(404)
if (batch_state(p) or {}).get("state") == "실행 중":
flash("일괄 실행 중에는 개별 실행을 할 수 없습니다.", "fail")
return redirect(url_for("gates", name=name))"""),
(""" for r in rows:
r["state"] = job_state(r["job"])
running = any(r["state"] in ("대기", "실행 중") for r in rows)""",
""" for r in rows:
r["state"] = job_state(r["job"])
batch = batch_state(p)
running = any(r["state"] in ("대기", "실행 중") for r in rows) or (batch or {}).get("state") == "실행 중\""""),
])
patch("web/app.py", [
(""" return render_template("gates.html", p=p, rows=rows, running=running)""",
""" return render_template("gates.html", p=p, rows=rows, running=running, batch=batch,
todo=[g for g in gate_ids() if not p.gate_status(g)["approved"]])"""),
(""" return render_template("pier.html", p=p, sections=secs, gates=gates,""",
""" return render_template("pier.html", p=p, sections=secs, gates=gates, need=unconfirmed(p),"""),
])
patch("web/templates/pier.html", [
("""<h2>3. 게이트</h2>""",
"""<form method="post" action="{{ url_for('confirm_all', name=p.name) }}"
onsubmit="return confirm('게이트에 필요한 미확정 묶음 {{ need|length }}개를 모두 확정합니다. 값을 도면·판독서와 대조하셨습니까?');">
{% if need %}확정자 <input type="text" name="by" required>
<button>필요한 묶음 일괄 확정 ({{ need|length }}개)</button>
<span class="muted">게이트 G1~G5에 필요한 묶음 중 미확정인 것만 확정합니다.</span>
{% else %}<span class="ok">게이트에 필요한 묶음이 모두 확정되어 있습니다.</span>{% endif %}
</form>
<h2>3. 게이트</h2>"""),
])
patch("web/templates/gates.html", [
("""<h2>검토보고서 (초안)</h2>""",
"""<h2>연속 실행·자동 승인</h2>
{% if batch %}<p>최근 일괄 실행: <b class="{{ 'ok' if batch.state == '완료' else ('warn' if batch.state in ('실행 중', '중단됨') else 'fail') }}">{{ batch.state }}</b>
· 판정자 {{ batch.by }} · {{ batch.at }}</p>
<ul>{% for m in batch.log %}<li>{{ m }}</li>{% endfor %}</ul>{% endif %}
{% if todo %}
<form method="post" action="{{ url_for('gates_run_all', name=p.name) }}"
onsubmit="return confirm('{{ todo[0] }}~{{ todo[-1] }}를 차례로 실행하고 PASS인 게이트만 입력한 이름으로 자동 승인합니다. 진행할까요?');">
판정자 <input type="text" name="by" required>
<button {% if running %}disabled{% endif %}>{{ todo[0] }}~{{ todo[-1] }} 연속 실행·자동 승인</button>
</form>
<p class="muted">PASS인 게이트만 승인합니다. FAIL·오류·실행 조건 미충족이면 그 게이트에서 멈추고 승인하지 않습니다.
승인 기록의 의견란에 '일괄 실행 자동 승인'이 남습니다. 결과는 아래 게이트별 표에서 확인하세요.</p>
{% else %}<p class="ok">모든 게이트가 승인되어 있습니다.</p>{% endif %}
<h2>검토보고서 (초안)</h2>"""),
])
print("patched")