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>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,19 @@
|
||||
{# Prism badges (stage B, 2026-10-16). Colour is never the only signal: every badge has a mark and a word. #}
|
||||
{% macro st_badge(st) -%}
|
||||
{%- set M = {'g': ['good', '●', '일치'], 'w': ['warn', '▲', '주의'], 'b': ['bad', '■', '불일치'], 'n': ['dash', '◇', '근거 없음'], 'x': ['', '—', '해당 없음'], '?': ['dash', '◇', '앱 확인 전']} -%}
|
||||
{%- set m = M.get(st, M['n']) -%}
|
||||
<span class="b {{ m[0] }}"><span class="m" aria-hidden="true">{{ m[1] }}</span>{{ m[2] }}</span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro grade_badge(g) -%}
|
||||
{%- set M = {'A': ['good', '●', '등급 A'], 'B': ['warn', '▲', '등급 B'], 'C': ['dash', '◇', '등급 C']} -%}
|
||||
{%- set m = M.get(g, M['C']) -%}
|
||||
<span class="b {{ m[0] }}"><span class="m" aria-hidden="true">{{ m[1] }}</span>{{ m[2] }}</span>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro human_badge(s) -%}
|
||||
{%- if s.confirmed -%}<span class="b fixed"><span class="m" aria-hidden="true">●</span>확정</span>
|
||||
{%- elif s.info -%}<span class="b dash" title="게이트가 쓰지 않는 참고 정보입니다">확정 불필요(참고)</span>
|
||||
{%- elif s.stale -%}<span class="b warn"><span class="m" aria-hidden="true">◆</span>변경됨 · 재확정 필요</span>
|
||||
{%- else -%}<span class="b dash">미확정</span>{%- endif -%}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}T형 교각 모델링{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='prism.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='prism-app.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main" style="position:absolute;left:-999rem">본문 바로가기</a>
|
||||
<header class="top">
|
||||
<a class="brand" href="{{ url_for('index') }}"><i aria-hidden="true"></i>T형 교각 모델링 <span class="muted" style="font-weight:400">반자동 MVP</span></a>
|
||||
{% if p %}<span class="sep" aria-hidden="true">›</span><nav aria-label="교각 화면">
|
||||
<a class="pier" href="{{ url_for('pier', name=p.name) }}">{{ p.name }}</a>
|
||||
<a href="{{ url_for('search', name=p.name) }}">도면 문자 검색</a>
|
||||
<a href="{{ url_for('candidates', name=p.name) }}">표 후보값</a>
|
||||
<a href="{{ url_for('geomread_page', name=p.name) }}">도면에서 읽기</a>
|
||||
<a href="{{ url_for('gates', name=p.name) }}">게이트</a></nav>{% endif %}
|
||||
</header>
|
||||
<main class="page" id="main">
|
||||
{% with msgs = get_flashed_messages(with_categories=true) %}{% for c, m in msgs %}
|
||||
<div class="flash {{ c }}" role="status">{{ m }}</div>{% endfor %}{% endwith %}
|
||||
{% if p and p.info.fit_mismatch %}<div class="flash warn" role="alert">▲ 형식 불일치 경고: 이 교각은 선택한 템플릿과 다음 항목이 다릅니다 — {{ p.info.fit_mismatch | join(', ') }}. 같은 템플릿으로 만들 수 없는 형식일 수 있으니 모든 묶음을 특히 주의해서 확인하세요. (이 경고는 닫을 수 없습니다)</div>{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}표 후보값 · {{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>표 후보값</h1>
|
||||
<p class="muted">자동 인식 범위: 교량받침 ELEVATION TABLE, 솔플레이트 상세 TABLE, 철근재료표(SD400·SD500). 운곡교 P1 도면 1세트로만 검증한 규칙입니다.
|
||||
후보값은 반드시 도면과 대조한 뒤 반영하고, 반영 후 해당 묶음을 다시 확정하세요. 그 밖의 값은 문자 검색으로 찾아 직접 입력합니다.</p>
|
||||
<p class="muted">같은 표 값이 <code>expected</code>(검사 기준) 행과 <code>params</code>(모델 생성값) 행으로 따로 나옵니다.
|
||||
둘을 함께 반영하면 표를 잘못 읽었을 때 모델과 검사가 같이 틀려 게이트가 통과하므로, params 행은 도면과 따로 대조한 뒤 선택하세요.</p>
|
||||
|
||||
{% from "_prism.html" import grade_badge %}
|
||||
<!-- stage C 2026-10-19 -->
|
||||
<div class="prism" id="out-of-table">
|
||||
<div class="stack">
|
||||
<h2>표 밖 치수 후보 (앵커 따라가기)</h2>
|
||||
{% if not ameta.anchors %}
|
||||
<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>이 교각에는 앵커가 없습니다. 앵커는 같은 형식의 도면을 이미 읽은 템플릿에서 "각 값을 도면 어디서 읽었는가"를 기억한 것입니다.
|
||||
같은 도면 틀(예: 운곡교 P1~P4)에 속한다면 템플릿을 골라 불러오세요.</div></div>
|
||||
{% if tpl_anchors %}<form method="post" action="{{ url_for('anchors_load', name=p.name) }}" class="row" style="margin:0">
|
||||
<label for="tpl">템플릿 앵커 불러오기</label>
|
||||
<select id="tpl" name="template">{% for t in tpl_anchors %}<option>{{ t }}</option>{% endfor %}</select>
|
||||
<button class="btn" type="submit">불러오기</button></form>{% endif %}
|
||||
{% elif not ameta.texts %}<p class="cap muted">도면 문자 색인이 없습니다. 도면을 먼저 올리세요.</p>
|
||||
{% else %}
|
||||
{% set na = arows|selectattr('grade', 'equalto', 'A')|list|length %}{% set nb = arows|selectattr('grade', 'equalto', 'B')|list|length %}{% set nc = arows|selectattr('grade', 'equalto', 'C')|list|length %}
|
||||
{% set nd = arows|rejectattr('same')|list|length %}
|
||||
<section class="strip">
|
||||
<p class="concl">후보 {{ arows|length }}건 — 현재 값과 다름 {{ nd }}건 · A {{ na }} · B {{ nb }} · C {{ nc }} · 후보를 못 찾음 {{ amiss|length }}건</p>
|
||||
<p class="cap">도면 틀 일치율 {{ '%d' % ((ameta.coverage or 0) * 100) }} % (앵커를 만든 도면과 이 도면에 공통인 문자의 비율). 70 % 미만이면 틀이 다른 도면으로 보고 모든 후보를 C등급으로 낮춥니다.
|
||||
A = 같은 치수가 여러 곳에서 같게 읽힘 · B = 한 곳 또는 일부만 · C = 근거 약함(목록만, 반영 시 사유 필요). 시험에서 A등급은 틀린 후보가 없었고 B등급은 약 15 %가 틀렸습니다.</p>
|
||||
</section>
|
||||
{% if ameta.self_source %}<div class="banner"><span class="m" aria-hidden="true">▲</span><div>이 앵커는 이 교각 자신의 도면에서 만들어졌습니다. 같은 도면을 다시 읽은 것이라 앱 확인의 근거로 쓰지 않습니다.</div></div>{% endif %}
|
||||
{% if arows %}
|
||||
<form method="post" action="{{ url_for('anchors_apply', name=p.name) }}" class="stack">
|
||||
<div class="tw"><table class="t">
|
||||
<caption>표 밖 치수 후보 — 다른 것부터, 등급 좋은 것부터</caption>
|
||||
<thead><tr><th scope="col">반영</th><th scope="col">대상</th><th class="num" scope="col">현재</th><th class="num" scope="col">후보</th><th scope="col">등급</th><th scope="col">근거</th><th scope="col">도면 위치</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in arows %}
|
||||
<tr{% if not r.same %} class="sel"{% endif %}>
|
||||
<td>{% if not r.same and not r.self_source %}<input type="checkbox" name="field" value="{{ r.field }}" aria-label="{{ r.field }} 반영" style="width:1.25rem;height:1.25rem">{% endif %}</td>
|
||||
<td><b>{{ r.field }}</b></td><td class="num">{{ r.current }}</td><td class="num">{{ r.value }}</td>
|
||||
<td>{{ grade_badge(r.grade) }}{% if r.same %} <span class="cap muted">같음</span>{% endif %}</td>
|
||||
<td class="cap">{{ r.followed }}곳 중 {{ r.agree }}곳 일치{% if r.shared > 1 %} · 같은 값의 필드 {{ r.shared }}종{% endif %}
|
||||
{% if r.table_confirmed %}<br><b>표 값과도 일치</b>{% elif r.table is defined %}<br><b>표 값 {{ r.table }}와 다름 — 표를 따르세요</b>{% endif %}</td>
|
||||
<td><span class="chip">{{ r.dwg }} '{{ r.text }}' ({{ '%.0f' % r.x }}, {{ '%.0f' % r.y }})</span>
|
||||
<a class="cap" href="{{ url_for('view_drawing', name=p.name, dwg=r.dwg, x=r.x, y=r.y) }}" target="_blank">도면 보기</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="row"><span class="cap">빠른 선택(A등급 중 현재 값과 다른 것):</span>
|
||||
<button class="btn sm" type="button" data-sel-a="expected">expected만</button>
|
||||
<button class="btn sm" type="button" data-sel-a="params">params만</button>
|
||||
<button class="btn sm quiet" type="button" data-sel-a="">선택 해제</button>
|
||||
<span class="cap muted">params와 expected에 같은 도면 문자를 함께 반영할 수 없어 한쪽씩 고릅니다.</span></div>
|
||||
<div class="field"><label for="why">반영 사유<span class="req">C등급 후보 반영에는 필수</span></label><input id="why" type="text" name="why" style="max-width:36rem"></div>
|
||||
<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>같은 도면 문자에서 나온 후보를 params와 expected에 함께 반영할 수 없습니다(검사의 독립성). 한쪽만 고르고, 나머지는 도면에서 따로 확인하세요.</div></div>
|
||||
<div class="row"><button class="btn primary" type="submit">선택한 후보 반영</button></div>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
/* sel-a */
|
||||
var rows = document.querySelectorAll('#out-of-table tbody tr');
|
||||
document.querySelectorAll('[data-sel-a]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var file = btn.getAttribute('data-sel-a');
|
||||
rows.forEach(function (tr) {
|
||||
var cb = tr.querySelector('input[type=checkbox][name=field]');
|
||||
if (!cb) return;
|
||||
var isA = tr.textContent.indexOf('등급 A') >= 0;
|
||||
cb.checked = !!file && isA && cb.value.indexOf(file + ':') === 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if chains %}
|
||||
<h3>도면 치수 사슬 검산 (R2)</h3>
|
||||
<p class="cap">{{ chains|length }}개 중 검산(개수 × 간격 = 합계)에 맞지 않는 것 {{ chain_bad|length }}개{% if chain_bad %}:{% endif %}
|
||||
{% for c in chain_bad %}<span class="b bad"><span class="m" aria-hidden="true">■</span>{{ c.dwg }} {{ c.text }} → 계산 {{ c.n * c.pitch }} mm</span> <a class="cap" href="{{ url_for('view_drawing', name=p.name, dwg=c.dwg, x=c.x, y=c.y) }}" target="_blank">도면 보기</a> {% endfor %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>후보값 비교 ({{ rows|length }}건 · 다름 {{ rows|rejectattr('same')|list|length }}건)</h2>
|
||||
{% if rows %}
|
||||
<form method="post" action="{{ url_for('candidates_apply', name=p.name) }}">
|
||||
<div class="wrap"><table>
|
||||
<tr><th>반영</th><th>대상</th><th>현재값</th><th>후보값</th><th>상태</th><th>근거</th></tr>
|
||||
{% for r in rows %}
|
||||
<tr><td>{% if not r.same %}<input type="checkbox" name="row" value="{{ loop.index0 }}">{% endif %}</td>
|
||||
<td><code>{{ r.file }}.{{ r.path|join('.') }}</code></td>
|
||||
<td><code>{{ dumps(r.current) }}</code></td><td><code>{{ dumps(r.candidate) }}</code></td>
|
||||
<td>{% if r.same %}<span class="ok">일치</span>{% else %}<span class="warn">다름</span>{% endif %}</td>
|
||||
<td>{{ r.src }}</td></tr>
|
||||
{% endfor %}
|
||||
</table></div>
|
||||
<p><button>선택한 후보값 반영</button></p>
|
||||
</form>
|
||||
{% else %}<p class="muted">인식된 표가 없습니다.</p>{% endif %}
|
||||
|
||||
<h2>인식한 표 ({{ tables|length }})</h2>
|
||||
{% for t in tables %}
|
||||
<details><summary>{{ t.key }} · {{ t.anchor.dwg }} '{{ t.anchor.text }}' ({{ t.anchor.x }}, {{ t.anchor.y }}) · {{ t.rows|length }}행</summary>
|
||||
<div class="wrap"><table>{% for r in t.rows %}<tr>{% for c in r.cells %}<td>{{ c.text }}</td>{% endfor %}</tr>{% endfor %}</table></div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
|
||||
<h2>EL 표기 ({{ els|length }})</h2>
|
||||
<p class="muted">EL 문자는 위치에 따라 뜻이 달라 자동 반영하지 않습니다.</p>
|
||||
<table><tr><th>도면</th><th>레이어</th><th>X</th><th>Y</th><th>문자</th><th>mm</th></tr>
|
||||
{% for e in els %}<tr><td>{{ e.dwg }}</td><td>{{ e.layer }}</td><td>{{ e.x }}</td><td>{{ e.y }}</td><td>{{ e.text }}</td><td>{{ e.value_mm }}</td></tr>{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_prism.html" import st_badge %}
|
||||
{% block title %}일치 묶음 일괄 확정{% endblock %}
|
||||
{% block body %}
|
||||
<div class="prism">
|
||||
<div class="stack">
|
||||
<div class="stack" style="gap:var(--prism-space-100)">
|
||||
<span class="eyebrow">입력 묶음 · 일괄 확정</span>
|
||||
<h1 style="font-size:var(--prism-type-title);color:var(--prism-text-heading);margin:0">앱 확인 '일치' 묶음 일괄 확정</h1>
|
||||
</div>
|
||||
{% if targets %}
|
||||
<section class="strip">
|
||||
<p class="concl">일치 {{ targets|length }}개를 확정합니다. 주의·불일치·근거 없음·미확인 {{ rest|length }}개는 대상이 아닙니다.</p>
|
||||
<p class="cap">근거표를 <b>끝까지 내려</b> 읽어야 확정할 수 있습니다. 확정 기록에는 입력한 이름만 남고, 앱은 확정하지 않습니다.</p>
|
||||
</section>
|
||||
<h3>근거표 <span class="cap muted">끝까지 내려 확인합니다</span></h3>
|
||||
<div class="scroll" id="sc" tabindex="0" role="region" aria-label="일치 묶음 근거표" style="max-height:20rem">
|
||||
<table class="t">
|
||||
<caption style="position:absolute;left:-999rem">일치 묶음 근거</caption>
|
||||
<thead><tr><th scope="col">묶음</th><th scope="col">근거</th><th scope="col">출처·위치</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in targets %}
|
||||
{% set c = (t.chk.checks | selectattr('st', 'equalto', 'g') | selectattr('strength', 'in', ['S', 'F', 'C']) | list) %}
|
||||
<tr><td><b>{{ t.file }}.{{ t.key }}</b></td><td>{% if c %}{{ c[0].id }} {{ c[0].text }}{% else %}{{ t.chk.summary }}{% endif %}</td>
|
||||
<td>{% if c and c[0].pos %}<span class="chip">{{ c[0].pos }}</span>{% endif %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<details class="card sunken">
|
||||
<summary style="min-height:2.75rem;display:flex;align-items:center;cursor:pointer;font-weight:700">제외된 묶음 {{ rest|length }}개 — 누르면 사유를 봅니다</summary>
|
||||
<table class="t"><thead><tr><th scope="col">묶음</th><th scope="col">앱 확인</th><th scope="col">사유</th></tr></thead><tbody>
|
||||
{% for r in rest %}
|
||||
<tr><td><a href="{{ url_for('evidence', name=p.name, which=r.file, key=r.key) }}">{{ r.file }}.{{ r.key }}</a></td><td>{{ st_badge(r.chk.st) }}</td><td>{{ r.chk.summary }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
</details>
|
||||
<form method="post" id="mf" class="stack">
|
||||
<div class="field"><label for="mby">확정자<span class="req">필수</span></label>
|
||||
<input id="mby" type="text" name="by" required style="max-width:20rem" disabled>
|
||||
<span class="help" id="hint">근거표를 끝까지 내리면 입력할 수 있습니다.</span></div>
|
||||
<div class="row end"><a class="btn" href="{{ url_for('pier', name=p.name) }}">취소</a>
|
||||
<button class="btn primary" id="mok" type="submit" disabled>일치 {{ targets|length }}개 확정</button></div>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
var sc = document.getElementById('sc'), by = document.getElementById('mby'), ok = document.getElementById('mok'), hint = document.getElementById('hint'), sent = false;
|
||||
function done() {
|
||||
if (sent) return; sent = true;
|
||||
fetch({{ url_for('confirm_matched_read', name=p.name)|tojson }}, {method: 'POST'}).then(function () {
|
||||
by.disabled = false; ok.disabled = false; hint.textContent = '근거표를 끝까지 읽었습니다. 이름을 입력하고 확정하세요.'; by.focus();
|
||||
});
|
||||
}
|
||||
function chk() { if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) done(); }
|
||||
sc.addEventListener('scroll', chk); chk();
|
||||
})();
|
||||
</script>
|
||||
{% else %}
|
||||
<section class="strip"><p class="concl">확정할 일치 묶음이 없습니다.</p>
|
||||
<p class="cap">모든 일치 묶음이 확정되었거나, 앱 확인이 아직 '일치'인 묶음이 없습니다. 주의·불일치·근거 없음 묶음은 근거 보기에서 사유와 함께 하나씩 확정합니다.</p></section>
|
||||
{% if rest %}<div class="tw"><table class="t"><thead><tr><th scope="col">묶음</th><th scope="col">앱 확인</th><th scope="col">사유</th></tr></thead><tbody>
|
||||
{% for r in rest %}<tr><td><a href="{{ url_for('evidence', name=p.name, which=r.file, key=r.key) }}">{{ r.file }}.{{ r.key }}</a></td><td>{{ st_badge(r.chk.st) }}</td><td>{{ r.chk.summary }}</td></tr>{% endfor %}
|
||||
</tbody></table></div>{% endif %}
|
||||
<div class="row"><a class="btn" href="{{ url_for('pier', name=p.name) }}">묶음 표로 돌아가기</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_prism.html" import st_badge, human_badge %}
|
||||
{% block title %}근거 보기 {{ which }}.{{ key }}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="prism">
|
||||
<div class="stack">
|
||||
<div class="stack" style="gap:var(--prism-space-100)">
|
||||
<span class="eyebrow">입력 묶음 · 근거 보기</span>
|
||||
<h1 style="font-size:var(--prism-type-title);color:var(--prism-text-heading);margin:0">{{ which }}.{{ key }}</h1>
|
||||
</div>
|
||||
<section class="strip" aria-label="결론">
|
||||
<p class="concl">{{ st_badge(chk.st) }} {{ chk.summary }}</p>
|
||||
{% if chk.stale %}<p class="cap">▲ 값이 바뀌어 이 결과는 낡았습니다. [전체 확인]으로 다시 확인하세요.</p>{% endif %}
|
||||
<dl class="kv">
|
||||
<dt>변수 등급</dt><dd>{% if st.info %}참고{% else %}{{ meta.grade }} {{ grades[meta.grade] }}{% endif %} <span class="muted">— {{ meta.note }}</span></dd>
|
||||
<dt>값 출처(일반)</dt><dd>{{ meta.source|join(' · ') }}{% if meta.origin %} · <span class="b warn"><span class="m" aria-hidden="true">▲</span>템플릿 원본 그대로</span>{% endif %}</dd>
|
||||
<dt>앱 확인</dt><dd>{{ st_badge(chk.st) }}</dd>
|
||||
<dt>사람 확정</dt><dd>{{ human_badge(st) }} {% if st.confirmed %}<span class="muted">{{ st.by }} · {{ st.at }}{% if st.app_st %} · 확정 당시 앱 확인 {{ ST.get(st.app_st, st.app_st) }}{% endif %}</span>{% endif %}
|
||||
{% if st.why %}<br><span class="muted">사유: {{ st.why }}</span>{% endif %}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<div class="tw"><table class="t">
|
||||
<caption>앱 확인 8종 (해당하는 것만 수행)</caption>
|
||||
<thead><tr><th scope="col">검사</th><th scope="col">결과</th><th scope="col">무엇을 무엇과 비교했는가</th><th scope="col">출처·위치</th></tr></thead>
|
||||
<tbody>
|
||||
{% for c in chk.checks %}
|
||||
<tr><td><b>{{ c.id }}</b> {{ c.name }}</td><td>{{ st_badge(c.st) }}</td><td>{{ c.text }}</td><td>{% if c.pos %}<span class="chip">{{ c.pos }}</span>{% endif %}</td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4">앱 확인 전입니다. [전체 확인]을 누르세요.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<div class="banner tip"><span class="m" aria-hidden="true">◆</span>
|
||||
<div>'일치'는 "독립 근거(도면 표·재료표 수량·템플릿)와 값이 같다"는 뜻이지 "도면이 맞다"는 뜻이 아닙니다. 앱은 확정하지 않으며, 근거를 읽은 사람이 확정합니다.
|
||||
철근 위치·좌표와 표 밖 치수(EL·치수 사슬)는 아직 앱이 대조하지 못합니다(C단계 예정).</div></div>
|
||||
|
||||
{% if not st.info %}
|
||||
<section class="card" aria-label="확정">
|
||||
<h3>확정</h3>
|
||||
<form method="post" action="{{ url_for('section_confirm', name=p.name, which=which, key=key) }}" class="stack">
|
||||
<input type="hidden" name="next" value="{{ url_for('evidence', name=p.name, which=which, key=key) }}">
|
||||
<div class="field"><label for="by">확정자<span class="req">필수</span></label>
|
||||
<input id="by" type="text" name="by" required style="max-width:20rem"></div>
|
||||
<div class="field"><label for="why">사유<span class="req">{% if chk.st in ('w', 'b', 'n') %}앱 확인이 '일치'가 아니므로 필수{% else %}선택{% endif %}</span></label>
|
||||
<textarea id="why" name="why" rows="3" {% if chk.st in ('w', 'b', 'n') %}required{% endif %}></textarea>
|
||||
<span class="help">무엇을 어떻게 확인했는지 적습니다(예: 도면 D1 정면도 EL 문자와 직접 대조).</span></div>
|
||||
<div class="row"><button class="btn primary" type="submit">저장된 값으로 확정</button>
|
||||
<a class="btn" href="{{ url_for('section', name=p.name, which=which, key=key) }}">값 편집</a>
|
||||
<a class="btn quiet" href="{{ url_for('pier', name=p.name) }}">묶음 표로 돌아가기</a></div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}게이트 · {{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
{% if running %}<meta http-equiv="refresh" content="5">{% endif %}
|
||||
<h1>게이트 G1 → G5</h1>
|
||||
<p class="muted">게이트마다 G1부터 그 단계까지 새로 만들고, 저장한 파일을 다시 열어 검사합니다(재로드).
|
||||
PASS 조건: 계획 FAIL 외 FAIL 0 · 수평/수직 음성 시험 검출 · 재로드 객체 수 일치 · 앞 게이트 객체 불변.
|
||||
다음 게이트는 앞 게이트가 승인되어야 실행할 수 있습니다.{% if running %} <b>실행 중 — 5초마다 새로 고칩니다.</b>{% endif %}</p>
|
||||
|
||||
{% if out_of_scope %}<p class="muted">이 교각의 범위 밖 게이트: {{ out_of_scope|join(', ') }} (실행·승인 대상 아님)</p>{% endif %}
|
||||
<h2>일괄 승인 (필요하면 실행)</h2>
|
||||
{% if batch %}<p>최근 일괄 실행: <b class="{{ 'ok' if batch.state == '완료' else ('warn' if batch.state in ('실행 중', '중단됨') else 'fail') }}">{{ batch.state }}</b>
|
||||
· 판정자 {{ batch.by }} · {% if batch.started %}{{ batch.started }} 시작 · {% endif %}{{ batch.at }}{% if batch.state != '실행 중' %} 끝{% endif %}</p>
|
||||
{% if aborted %}<div class="flash fail" role="alert" id="batch-aborted">
|
||||
<b>이 일괄 실행은 끝까지 가지 못했습니다.</b>
|
||||
{% if batch.reason == 'error' %}모델을 만들거나 검사하는 도중 <b>오류</b>가 나서 멈췄습니다{% if batch.error %}: <code>{{ batch.error }}</code>{% endif %}.
|
||||
{% elif batch.reason == 'fail' %}<b>{{ batch.gate }}</b>가 FAIL이라 승인하지 않고 멈췄습니다.
|
||||
{% elif batch.reason == 'blocked' %}실행 조건이 맞지 않아 시작하지 못했습니다.
|
||||
{% elif batch.reason == 'killed' %}서버가 멈추거나 다시 켜져서 끝나지 못했습니다.
|
||||
{% else %}이유가 기록되지 않았습니다.{% endif %}
|
||||
<ul>
|
||||
<li>아래 게이트별 표에서 <b>"이전 실행 결과"</b>로 표시된 것은 이번 실행에서 갱신되지 않은 옛 결과입니다. 그 결과나 승인 기록을 이번 입력의 결과로 읽지 마세요.</li>
|
||||
<li>중단되기 전에 계산된 <b>임시 결과</b>가 있으면 해당 게이트 표 위에 따로 표시합니다. 승인과 보고서에는 쓰이지 않습니다.</li>
|
||||
<li>원인을 해결한 뒤 일괄 승인을 다시 실행하세요.</li>
|
||||
</ul></div>{% endif %}
|
||||
<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 결과가 있는 게이트는 바로 승인하고, 결과가 없으면 실행한 뒤 PASS일 때만 승인합니다. 진행할까요?');">
|
||||
판정자 <input type="text" name="by" required>
|
||||
<button {% if running %}disabled{% endif %}>{{ todo[0] if todo|length == 1 else todo[0] ~ '~' ~ todo[-1] }} 일괄 승인</button>
|
||||
</form>
|
||||
<p class="muted">최신 PASS 결과가 있는 게이트는 다시 실행하지 않고 바로 승인합니다. 결과가 없거나 입력이 바뀐 게이트는 실행한 뒤 PASS일 때만 승인합니다. FAIL·오류·실행 조건 미충족이면 그 게이트에서 멈추고 승인하지 않습니다.
|
||||
승인 기록의 의견란에 '일괄 실행 자동 승인'이 남습니다. 결과는 아래 게이트별 표에서 확인하세요.</p>
|
||||
{% else %}<p class="ok">모든 게이트가 승인되어 있습니다.</p>{% endif %}
|
||||
|
||||
<h2>검토보고서 (초안)</h2>
|
||||
<form method="post" action="{{ url_for('report_make', name=p.name) }}">
|
||||
작성자 <input type="text" name="by"> <button {% if running %}disabled{% endif %}>보고서 초안 생성 (MD + PDF)</button>
|
||||
<a href="{{ url_for('report_get', name=p.name, kind='md') }}">MD 받기</a> · <a href="{{ url_for('report_get', name=p.name, kind='pdf') }}">PDF 받기</a>
|
||||
</form>
|
||||
<p class="muted">게이트 결과·도면 표·승인 기록으로 자동 작성합니다. 승인되지 않은 게이트가 있으면 '미완료'로 표시됩니다. 판정 확정과 3~5장 서술은 담당자가 합니다.</p>
|
||||
|
||||
{% for g in rows %}
|
||||
<h2>{{ g.id }} {{ g.name }} —
|
||||
{% if g.report %}{% if g.stale %}<span class="warn">입력 변경 · 재실행 필요</span>{% elif g.report.verdict == 'PASS' %}<span class="ok">PASS</span>{% else %}<span class="fail">FAIL</span>{% endif %}{% else %}<span class="muted">미실행</span>{% endif %}
|
||||
{% if g.approved %}· <span class="ok">승인</span>{% elif g.approval_stale %}· <span class="warn">승인 기록은 이전 입력 것(무효)</span>{% elif g.approval %}· {{ g.approval.decision }}{% endif %}</h2>
|
||||
{% if g.report %}<p class="muted">결과 시각 {{ g.report_at or '-' }}{% if g.report.mode == 'batch' %} · 일괄 실행{% endif %}
|
||||
{% if g.older %}<b class="warn">이전 실행 결과 — 이번 일괄 실행에서 갱신되지 않았습니다</b>{% endif %}</p>{% endif %}
|
||||
{% if g.show_partial %}{% set pr = g.partial %}<div class="flash warn" id="partial-{{ g.id }}"><b>중단된 실행의 임시 결과</b> ({{ pr.at }}) — 검사 {{ pr.passed }} / {{ pr.total }} PASS,
|
||||
FAIL {{ pr.fails|length }}건{% if pr.fails %}: {{ pr.fails[:6]|join(' · ') }}{% if pr.fails|length > 6 %} 외 {{ pr.fails|length - 6 }}건{% endif %}{% endif %}.
|
||||
계획된 예외는 구분하지 않은 검사기 원본 결과이며 승인·보고서에는 쓰이지 않습니다.</div>{% endif %}
|
||||
|
||||
{% if g.state in ('대기', '실행 중') %}<p class="warn">{{ g.state }}: {{ g.job.log[-1] if g.job.log else '' }}</p>
|
||||
{% elif g.state == '중단됨' %}<p class="warn">중단됨: 이전 실행이 끝나지 않은 채 서버가 멈췄습니다. 다시 실행하세요.</p>
|
||||
{% elif g.state == '오류' %}<p class="fail">오류</p><pre>{{ g.job.error }}</pre>{% endif %}
|
||||
|
||||
{% if g.blockers %}<p>실행 조건 미충족: {% for b in g.blockers %}<span class="fail">{{ b }}</span>{% if not loop.last %}, {% endif %}{% endfor %}</p>
|
||||
{% else %}<form method="post" action="{{ url_for('gate_run', name=p.name, gate=g.id) }}"><button {% if running %}disabled{% endif %}>{{ g.id }} 실행</button></form>{% endif %}
|
||||
|
||||
{% if g.report %}{% set r = g.report %}
|
||||
<table>
|
||||
<tr><th>검사</th><td>{{ r.summary.passed }} / {{ r.summary.total }} PASS ({{ r.checker }})</td></tr>
|
||||
<tr><th>미계획 FAIL</th><td>{% if r.unplanned_fails %}<span class="fail">FAIL {{ r.unplanned_fails|length }}건</span><ul>{% for f in r.unplanned_fails %}<li>{{ f }}</li>{% endfor %}</ul>{% else %}<span class="ok">0건</span>{% endif %}</td></tr>
|
||||
<tr><th>계획 FAIL (승인된 예외)</th><td>{% if r.planned_fails %}<span class="warn">계획 FAIL {{ r.planned_fails|length }}건</span><ul>{% for f in r.planned_fails %}<li>{{ f.item }} — {{ f.reason }}</li>{% endfor %}</ul>{% else %}0건{% endif %}</td></tr>
|
||||
<tr><th>음성 시험</th><td>{% for k, v in r.negative.items() %}{{ '수평' if k == 'negH' else '수직' }} {{ v.move }} →
|
||||
{% if v.detected %}<span class="ok">검출</span> ({{ v.new_fails|join(', ') }}){% else %}<span class="fail">미검출</span>{% endif %}<br>{% endfor %}</td></tr>
|
||||
<tr><th>재로드</th><td>{% if r.reload.ok is none %}<span class="muted">생략</span> {{ r.reload.skipped }}{% elif r.reload.ok %}<span class="ok">일치</span> 생성 {{ r.reload.built }} / 다시 연 파일 {{ r.reload.reopened }}{% else %}<span class="fail">불일치</span> 생성 {{ r.reload.built }} / 다시 연 파일 {{ r.reload.reopened }}{% endif %}</td></tr>
|
||||
<tr><th>앞 게이트 객체 불변</th><td>{% if r.unchanged.ok is none %}<span class="muted">생략</span> {{ r.unchanged.skipped }}{% elif r.unchanged.ok %}<span class="ok">불변</span> {{ r.unchanged.compared }}개 비교{% if r.unchanged.against %} (기준 {{ r.unchanged.against }}){% endif %}{% else %}<span class="fail">변경</span> {{ r.unchanged.compared }}개 비교{% endif %}</td></tr>
|
||||
<tr><th>모델</th><td>{% if r.model %}<a href="{{ url_for('download', name=p.name, gate=g.id) }}">{{ p.name }}_{{ g.id }}.3dm 내려받기</a>{% else %}<span class="muted">일괄 실행 — 중간 모델 저장 안 함 (G5 모델에 포함)</span>{% endif %} · {{ r.secs }}초{% if r.mode == 'batch' %} (일괄 세션 {{ r.session_secs }}초){% endif %}</td></tr>
|
||||
</table>
|
||||
{% if not g.stale %}
|
||||
<form method="post" action="{{ url_for('gate_decide', name=p.name, gate=g.id) }}">
|
||||
판정자 <input type="text" name="by" required> 의견 <input type="text" name="note" size="40" placeholder="반려 시 필수">
|
||||
<button name="decision" value="승인" {% if r.verdict != 'PASS' %}disabled{% endif %}>승인</button>
|
||||
<button name="decision" value="반려">반려</button>
|
||||
</form>{% endif %}
|
||||
{% if g.approval %}<p class="muted">최근 판정: {{ g.approval.decision }} · {{ g.approval.by }} · {{ g.approval.at }}{% if g.approval.note %} · {{ g.approval.note }}{% endif %}</p>{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}도면에서 읽기 · {{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>도면에서 읽기</h1>
|
||||
<p class="muted">한 화면에서 <b>템플릿 원본 값을 모두 도면 값으로</b> 바꿉니다. 운곡교 P1 도면 틀(P1~P4)의 첫 번째 도면(D1)과 철근재료표에서 코핑·기초·말뚝·기둥·받침·철근 값을 읽어 <code>params</code>(모델 생성값)와 <code>expected</code>(검사 기준)의 현재 값과 비교합니다.
|
||||
읽기 규칙은 모두 완성된 교각(P1)에서 같은 값이 재현되는지 먼저 확인한 것입니다(<code>tools/test_geomread.py</code>). 이 도구는 아무것도 자동으로 바꾸지 않고, 고른 줄만 반영합니다. 반영한 묶음은 다시 확정해야 합니다.</p>
|
||||
{% if running %}<p class="warn">게이트 실행 중에는 값을 바꿀 수 없습니다.</p>{% endif %}
|
||||
{% if state != 'ok' %}
|
||||
<p class="fail">{{ msg }}</p>
|
||||
{% else %}
|
||||
<p class="muted">{{ msg }}{% if msg %} · {% endif %}현재 값과 다른 줄 {{ n }}건</p>
|
||||
<div class="flash warn">params와 expected를 같은 도면 읽기에서 채우면 둘이 서로 같은 것은 <b>독립 근거가 아닙니다</b>(앱 확인 C9). 받침 표·솔플레이트 표·재료표 값(아래 "표 값" 구역)도 이 화면에 함께 나오며, 같은 값을 <a href="{{ url_for('candidates', name=p.name) }}">표 후보값</a> 화면에서 따로 골라 반영할 수도 있습니다.
|
||||
추론 줄(예: 좌측 스터럽 윗면)은 기본으로 선택되어 있지 않습니다.</div>
|
||||
{% if notes %}<h2>확인이 필요한 점</h2><ul>{% for t in notes %}<li>{{ t }}</li>{% endfor %}</ul>{% endif %}
|
||||
{% if not n %}<p class="ok">읽은 값이 모두 현재 값과 같습니다(1.5 mm 반올림 안).</p>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('geomread_apply', name=p.name) }}">
|
||||
{% for g, items in groups.items() %}
|
||||
<h2>{{ g }} ({{ items|length }}건)</h2>
|
||||
<div class="wrap"><table>
|
||||
<thead><tr><th>반영</th><th>대상</th><th>현재</th><th>읽은 값</th><th>근거</th></tr></thead>
|
||||
<tbody>{% for i, c in items %}
|
||||
<tr><td><input type="checkbox" name="row" value="{{ i }}" {% if not c.inferred %}checked{% endif %} aria-label="{{ c.file }}:{{ c.path }} 반영"></td>
|
||||
<td><code>{{ c.file }}:{{ c.path }}</code></td>
|
||||
<td>{{ brief(c.old)|truncate(60) }}</td><td><b>{{ brief(c.new)|truncate(60) }}</b></td>
|
||||
<td>{{ c.why }}{% if c.inferred %} <span class="warn">추론</span>{% endif %}</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
{% endfor %}
|
||||
<p>반영자 <input type="text" name="by" required> <button {% if running %}disabled{% endif %}>선택한 줄 반영</button></p>
|
||||
<p class="muted">반영 전에 <code>params.json</code>·<code>expected.json</code>·<code>confirm.json</code>을 <code>backup/before_geomread_…</code>에 보관하고, 반영 기록은 <code>read_log.json</code>에 남깁니다.</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if log %}<h2>최근 반영 기록</h2><ul>{% for e in log %}<li>{{ e.at }} · {{ e.by or '-' }} · {{ e.count }}건 · {{ e.backup }}</li>{% endfor %}</ul>{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<h1>교각 목록</h1>
|
||||
<table>
|
||||
<tr><th>교각</th><th>형식</th><th>만든 날</th><th>템플릿</th><th>삭제</th></tr>
|
||||
{% for p in piers %}
|
||||
<tr><td><a href="{{ url_for('pier', name=p.name) }}">{{ p.name }}</a></td><td>{{ p.info.type }}</td>
|
||||
<td>{{ p.info.created }}</td><td>{{ p.info.template or '-' }}</td>
|
||||
<td><form method="post" action="{{ url_for('pier_delete', name=p.name) }}" style="margin:0"
|
||||
onsubmit="return confirm('교각 \'{{ p.name }}\'을(를) 삭제합니다. 도면·입력·결과가 모두 휴지통(data/_휴지통)으로 옮겨집니다. 계속할까요?');">
|
||||
<input type="text" name="confirm_name" placeholder="교각 이름 입력" size="12" required>
|
||||
<button>삭제</button></form></td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p class="muted">삭제하려면 해당 줄의 칸에 교각 이름을 똑같이 입력하고 [삭제]를 누르세요. 삭제한 교각은 data/_휴지통 폴더로 옮겨지며, 폴더를 다시 옮기면 복구됩니다. 게이트 실행 중인 교각은 삭제할 수 없습니다.</p>
|
||||
|
||||
<h2>도면으로 템플릿 추천</h2>
|
||||
<form method="post" action="{{ url_for('recommend_upload') }}" enctype="multipart/form-data">
|
||||
교각 도면(DWG, 여러 장) <input type="file" name="dwg" accept=".dwg" multiple required>
|
||||
<button>도면 읽고 추천 받기</button>
|
||||
</form>
|
||||
<p class="muted">도면을 먼저 올리면 가장 비슷한 템플릿을 순위로 보여 줍니다. 추천은 제안이며, 아래 6문항을 사람이 확인한 뒤 교각을 만듭니다. 도면을 올리지 않고 만들려면 아래 '새 교각'을 쓰세요.</p>
|
||||
|
||||
<h2>새 교각</h2>
|
||||
<form method="post" action="{{ url_for('pier_new') }}">
|
||||
이름 <input type="text" name="name" required>
|
||||
형식 <select name="type">{% for t in types %}<option>{{ t }}</option>{% endfor %}</select>
|
||||
템플릿 <select name="template">
|
||||
{% if templates %}<optgroup label="보호 템플릿">{% for t in templates %}<option value="{{ tpl }}{{ t.name }}">{{ t.name }}</option>{% endfor %}</optgroup>{% endif %}
|
||||
<optgroup label="기존 교각 복사">{% for p in piers %}<option value="{{ p.name }}">{{ p.name }}</option>{% endfor %}</optgroup>
|
||||
</select>
|
||||
<button>만들기</button>
|
||||
</form>
|
||||
<p class="muted">1차 대상은 T형 교각뿐입니다. 다른 형식은 만들 수 없습니다.
|
||||
템플릿의 매개변수·기대값이 복사되므로, 새 도면 값으로 모든 묶음을 확인·수정한 뒤 확정해야 게이트를 실행할 수 있습니다.</p>
|
||||
|
||||
{% if templates %}
|
||||
<h2>보호 템플릿</h2>
|
||||
<table>
|
||||
<tr><th>템플릿</th><th>형식 설명</th><th>모델링 게이트</th><th>원본 교각</th><th>저장일</th></tr>
|
||||
{% for t in templates %}
|
||||
<tr><td>{{ t.name }}</td><td>{{ t.desc }}</td><td>{{ t.scope | join(', ') }}</td><td>{{ t.source }}</td><td>{{ t.created }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p class="muted">보호 템플릿은 data/_템플릿 폴더에 고정 사본으로 저장되어, 원본 교각을 고치거나 삭제해도 바뀌지 않습니다.
|
||||
앱 화면에서는 고칠 수 없습니다. 형식이 같은 교각이면 보호 템플릿에서 시작하는 것을 권장합니다.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>{{ p.name }} <span class="muted">({{ p.info.type }})</span></h1>
|
||||
|
||||
<h2>1. 도면</h2>
|
||||
{% if audit %}
|
||||
<table><tr><th>구분</th><th>DWG</th><th>DXF</th><th>DWG SHA-256</th></tr>
|
||||
{% for a in audit %}<tr><td>{{ a.label }}</td><td>{{ a.source }}</td><td>{{ a.dxf }}</td><td><code>{{ a.source_sha256[:16] }}…</code></td></tr>{% endfor %}
|
||||
</table>
|
||||
<p>색인 문자 {{ ntexts }}개 · <a href="{{ url_for('search', name=p.name) }}">문자 검색</a> · <a href="{{ url_for('candidates', name=p.name) }}">표 후보값</a> · <a href="{{ url_for('geomread_page', name=p.name) }}">도면에서 읽기</a></p>
|
||||
{% else %}<p class="muted">올린 도면이 없습니다.</p>{% endif %}
|
||||
<form method="post" action="{{ url_for('upload', name=p.name) }}" enctype="multipart/form-data">
|
||||
<input type="file" name="dwg" accept=".dwg" multiple> <button>올리기·변환</button>
|
||||
<span class="muted">파일 이름 순서대로 D1, D2, …로 부릅니다. 다시 올리면 색인을 새로 만듭니다.</span>
|
||||
</form>
|
||||
|
||||
<h2>2. 입력 묶음 확인·확정</h2>
|
||||
<!-- stage A 2026-10-16 / stage B 2026-10-16 -->
|
||||
{% from "_prism.html" import st_badge, human_badge %}
|
||||
<div class="prism" id="bundles">
|
||||
<div class="stack">
|
||||
<section class="strip" aria-label="요약">
|
||||
<p class="concl">필요 {{ nreq }}개 중 앱 확인 일치 {{ cnt.g }} · 주의 {{ cnt.w }} · 불일치 {{ cnt.b }} · 근거 없음 {{ cnt.n }} · 해당 없음 {{ cnt.x }} · 사람 확정 {{ human }}</p>
|
||||
<div class="row">
|
||||
<form method="post" action="{{ url_for('autocheck_run', name=p.name) }}" style="margin:0"><button class="btn" type="submit">전체 확인</button></form>
|
||||
<a class="btn" href="{{ url_for('geomread_page', name=p.name) }}">도면에서 읽기</a>
|
||||
<a class="btn primary" href="{{ url_for('review', name=p.name) }}">검토 요약으로 확정</a>
|
||||
{% if nmatch %}<a class="btn" href="{{ url_for('confirm_matched', name=p.name) }}">일치 묶음만 확정 ({{ nmatch }}개)</a>{% endif %}
|
||||
</div>
|
||||
<p class="cap">앱 확인은 도면 표·재료표 수량·고정 항목을 앱이 대조한 결과이며 <b>앱은 확정하지 않습니다</b>. '일치'는 "독립 근거와 값이 같다"는 뜻이지 "도면이 맞다"는 뜻이 아닙니다.
|
||||
<b>변수 등급</b> V1 값 · V2 목록 · V3 생성(화면 편집 차단) · F 고정(바꾸면 사유 필요).</p>
|
||||
</section>
|
||||
{% if chain_bad %}
|
||||
<div class="banner dont" role="alert"><span class="m" aria-hidden="true">■</span><div>도면 치수 사슬 {{ chain_bad|length }}건이 검산에 맞지 않습니다(개수 × 간격 ≠ 합계) — 도면 자체의 오류일 수 있습니다:
|
||||
{% for c in chain_bad %}<span class="chip">{{ c.dwg }} {{ c.text }} (계산 {{ c.n * c.pitch }} mm)</span> {% endfor %}
|
||||
<a href="{{ url_for('candidates', name=p.name) }}">표 후보값 화면에서 보기</a></div></div>
|
||||
{% endif %}
|
||||
{% if origin_same %}
|
||||
<div class="banner" role="alert"><span class="m" aria-hidden="true">▲</span><div>템플릿 원본 그대로인 필수 묶음 {{ origin_same|length }}개 — 템플릿을 복사한 뒤 한 번도 바뀌지 않았습니다. 새 도면과 대조(후보 반영·값 수정)하기 전에 확정하면 원본 교각의 모델이 PASS로 나올 수 있습니다:
|
||||
{% for s in origin_same %}<a href="{{ url_for('evidence', name=p.name, which=s.file, key=s.key) }}">{{ s.file }}.{{ s.key }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</div></div>
|
||||
{% elif not has_origin %}<p class="cap muted">이 교각은 템플릿 복사 기록이 없어 '템플릿 원본 그대로' 여부를 표시하지 않습니다.</p>{% endif %}
|
||||
<div class="tw"><table class="t">
|
||||
<caption>입력 묶음 {{ sections|length }}개 — 앱 확인이 나쁜 것부터</caption>
|
||||
<thead><tr><th scope="col">묶음</th><th scope="col">변수 등급</th><th scope="col">값 출처</th><th scope="col">앱 확인</th><th scope="col">사람 확정</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in sections %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('section', name=p.name, which=s.file, key=s.key) }}">{{ s.file }}.{{ s.key }}</a>{% if not s.required %} <span class="cap muted">(필수 아님)</span>{% endif %}</td>
|
||||
<td>{% if s.info %}<span class="muted">참고</span>{% else %}<span title="{{ s.meta.note }}">{{ s.meta.grade }} {{ grades[s.meta.grade] }}</span>{% endif %}</td>
|
||||
<td>{% if s.meta.origin and s.required %}<span class="b warn"><span class="m" aria-hidden="true">▲</span>템플릿 원본 그대로</span>{% elif s.meta.origin %}<span class="muted">템플릿 원본 그대로</span>{% else %}{{ s.meta.source|join(' · ') }}{% endif %}</td>
|
||||
<td>{{ st_badge(s.chk.st) }} <span class="cap">{{ s.chk.summary }}</span> <a class="cap" href="{{ url_for('evidence', name=p.name, which=s.file, key=s.key) }}">근거 보기</a></td>
|
||||
<td>{{ human_badge(s) }}{% if s.by %} <span class="cap muted">{{ s.by }} · {{ s.at }}</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if need %}<p class="muted">게이트에 필요한 묶음 {{ need|length }}개가 아직 확정되지 않았습니다. 앱 확인이 '일치'인 묶음은 위의 [일치 묶음 일괄 확정]으로, 나머지는 '근거 보기'에서 사유와 함께 하나씩 확정합니다.</p>
|
||||
{% else %}<p class="ok">게이트에 필요한 묶음이 모두 확정되어 있습니다.</p>{% endif %}
|
||||
|
||||
<h2>3. 게이트</h2>
|
||||
{% if out_of_scope %}<p class="muted">범위 밖: {{ out_of_scope|join(', ') }} — 이 교각에서는 모델링하지 않습니다.</p>{% endif %}
|
||||
<table><tr><th>게이트</th><th>내용</th><th>판정</th><th>승인</th></tr>
|
||||
{% for g in gates %}
|
||||
<tr><td>{{ g.id }}</td><td>{{ g.name }}</td>
|
||||
<td>{% if g.report %}{% if g.stale %}<span class="warn">입력 변경 · 재실행 필요</span>{% elif g.report.verdict == 'PASS' %}<span class="ok">PASS</span>{% else %}<span class="fail">FAIL</span>{% endif %}{% else %}<span class="muted">미실행</span>{% endif %}</td>
|
||||
<td>{% if g.approved %}<span class="ok">승인</span>{% elif g.approval %}{{ g.approval.decision }}{% else %}<span class="muted">-</span>{% endif %}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if running %}<p class="warn">실행 중입니다{% if batch and batch.log %} — {{ batch.log[-1] }}{% endif %}.
|
||||
<a href="{{ url_for('gates', name=p.name) }}">게이트 화면에서 진행 보기</a></p>
|
||||
{% elif todo %}
|
||||
<form method="post" action="{{ url_for('gates_run_all', name=p.name) }}"
|
||||
onsubmit="return confirm('{{ todo[0] }}~{{ todo[-1] }}를 차례로 승인합니다. 최신 PASS 결과가 있는 게이트는 바로 승인하고, 결과가 없으면 실행한 뒤 PASS일 때만 승인합니다. 진행할까요?');">
|
||||
판정자 <input type="text" name="by" required>
|
||||
<button {% if need %}disabled title="먼저 묶음을 확정하세요"{% endif %}>{{ todo[0] if todo|length == 1 else todo[0] ~ '~' ~ todo[-1] }} 일괄 승인</button>
|
||||
<span class="muted">{% if need %}먼저 위의 묶음을 확정하세요. {% endif %}최신 PASS 결과가 있으면 바로 승인하고, 없으면 실행한 뒤 PASS일 때만 승인합니다. FAIL·오류가 나면 멈춥니다. 진행은 게이트 화면에서 보입니다.</span>
|
||||
</form>
|
||||
{% else %}<p class="ok">모든 게이트가 승인되어 있습니다.</p>{% endif %}
|
||||
{% if batch and not running %}<p class="muted">최근 일괄 실행: {{ batch.state }} · {{ batch.by }} · {{ batch.at }}{% if batch.log %} — {{ batch.log[-1] }}{% endif %}</p>{% endif %}
|
||||
<p><a href="{{ url_for('gates', name=p.name) }}">게이트 화면으로</a> (게이트별 결과·개별 실행·보고서)</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<h1>도면으로 템플릿 추천</h1>
|
||||
<p class="muted">도면 {{ nfiles }}장, 문자 {{ ntexts }}개를 읽었습니다. 아래 추정은 <b>제안</b>이며 확정이 아닙니다. 맞지 않으면 아래 6문항에서 바꾸세요.</p>
|
||||
|
||||
<h2>1. 추천 순위</h2>
|
||||
{% set asked = rank[0].asked if rank else 0 %}
|
||||
{% if rank and rank[0].matched < asked %}
|
||||
<p class="warn">▲ 판단 불가: 추정한 {{ asked }}문항이 모두 맞는 템플릿이 없습니다. 가장 가까운 템플릿과 다른 문항을 확인하고, 필요하면 생성기·검사기를 추가해야 합니다.</p>
|
||||
{% endif %}
|
||||
<table>
|
||||
<tr><th>순위</th><th>템플릿</th><th>일치</th>{% for q, label in questions %}<th>{{ label }}</th>{% endfor %}</tr>
|
||||
{% for r in rank %}
|
||||
<tr><td>{{ loop.index }}</td><td>{{ r.name }}</td><td>{{ r.matched }} / {{ r.asked }}</td>
|
||||
{% for q, label in questions %}<td>{{ r.profile[q] }}{% if q in r.hits %} {% if r.hits[q] %}<span class="ok">● 일치</span>{% else %}<span class="fail">■ 다름</span>{% endif %}{% endif %}</td>{% endfor %}</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2>2. 추정 근거</h2>
|
||||
<table>
|
||||
<tr><th>문항</th><th>추정</th><th>근거(도면 위치)</th><th>신뢰</th></tr>
|
||||
{% for q, label in questions %}{% set e = est[q] %}
|
||||
<tr><td>{{ label }}</td><td>{% if e[0] is none %}<span class="warn">▲ 판단 불가</span>{% else %}{{ e[0] }}{% endif %}</td><td>{{ e[1] }}</td><td>{{ e[2] or '-' }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p class="muted">코핑 하면 형상은 정면도의 큰 R 문자(10 m 이상 = 곡선)만 보며, 작은 R(평면 모서리 등)은 제외합니다. 기초 형식은 '말뚝' 문자 유무로 정하므로, 도면에 말뚝 문구가 없는데 말뚝이 있는 경우는 틀립니다. 평면의 사각·직각과 각도는 문항이 아니라 값이라 여기서 묻지 않습니다.</p>
|
||||
|
||||
<h2>3. 6문항 확인 후 교각 만들기</h2>
|
||||
<form method="post" action="{{ url_for('recommend_create', sid=sid) }}">
|
||||
<table>
|
||||
{% for q, label in questions %}
|
||||
<tr><th>{{ label }}</th><td><input type="text" name="ans_{{ q }}" value="{{ '' if est[q][0] is none else est[q][0] }}" required></td></tr>
|
||||
{% endfor %}
|
||||
<tr><th>철근 형상 종류가 템플릿의 재료표 형식 열과 같은가</th><td><select name="ans_rebar_shape"><option>미확인</option><option>예</option><option>아니오</option></select></td></tr>
|
||||
<tr><th>철근 생성 방식(규칙 값 / 철근별 중심선)이 같은가</th><td><select name="ans_rebar_method"><option>미확인</option><option>예</option><option>아니오</option></select></td></tr>
|
||||
</table>
|
||||
<h3>각도 값 확인 <span class="muted">(템플릿을 가르지 않는 값. 도면에서 읽어 입력하며 비워도 됩니다)</span></h3>
|
||||
<table>
|
||||
<tr><th>코핑 평면 skew (°)</th><td><input type="text" name="ans_skew_deg" size="8"></td>
|
||||
<th>받침 회전 (°)</th><td><input type="text" name="ans_bearing_rot_deg" size="8"></td>
|
||||
<th>기초 회전 (°)</th><td><input type="text" name="ans_footing_rot_deg" size="8"></td></tr>
|
||||
</table>
|
||||
<h3>결과 판정</h3>
|
||||
<p id="verdict" class="muted" role="status">템플릿과 답을 고르면 판정이 나옵니다.</p>
|
||||
<p>이름 <input type="text" name="name" required>
|
||||
형식 <select name="type"><option>T형</option></select>
|
||||
템플릿 <select name="template">{% for r in rank %}<option value="{{ tpl }}{{ r.name }}">{{ loop.index }}위 {{ r.name }}</option>{% endfor %}</select>
|
||||
<button id="mk">교각 만들기</button></p>
|
||||
</form>
|
||||
<p class="muted">[교각 만들기]를 누를 때 교각 폴더가 만들어지고 올린 도면이 그 안으로 옮겨집니다. 6문항 답은 교각 정보(pier.json)에 저장됩니다.
|
||||
'다름'이 있는데 진행하면 형식이 다른 도면을 같은 템플릿에 넣는 것이므로, 만든 뒤 모든 묶음을 특히 주의해서 확인하세요.</p>
|
||||
<script>
|
||||
(function () {
|
||||
var PROF = {{ profiles | tojson }}, QS = {{ questions | map(attribute=0) | list | tojson }}, LAB = {{ questions | map(attribute=1) | list | tojson }};
|
||||
var f = document.querySelector('form'), v = document.getElementById('verdict'), mk = document.getElementById('mk');
|
||||
function judge() {
|
||||
var t = f.template.value.replace('{{ tpl }}', ''), pr = PROF[t], bad = [];
|
||||
QS.forEach(function (q, i) { if (String(pr[q]) !== f['ans_' + q].value.trim()) bad.push(LAB[i]); });
|
||||
if (f.ans_rebar_shape.value === '아니오') bad.push('철근 형상 종류');
|
||||
if (f.ans_rebar_method.value === '아니오') bad.push('철근 생성 방식');
|
||||
var unk = (f.ans_rebar_shape.value === '미확인' ? 1 : 0) + (f.ans_rebar_method.value === '미확인' ? 1 : 0);
|
||||
if (bad.length) { v.className = 'fail'; v.textContent = '■ 같은 템플릿으로 만들 수 없음 — 다른 문항: ' + bad.join(', ') + '. 가장 가까운 템플릿에 생성기·검사기를 추가해야 합니다. 진행하면 형식 불일치 경고가 교각에 계속 표시됩니다.'; mk.textContent = '그래도 만들기'; }
|
||||
else { v.className = 'ok'; v.textContent = '● 이 템플릿으로 만들 수 있음' + (unk ? ' (철근 문항 ' + unk + '개는 미확인)' : ''); mk.textContent = '교각 만들기'; }
|
||||
}
|
||||
f.addEventListener('input', judge); f.addEventListener('change', judge); judge();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,108 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_prism.html" import st_badge %}
|
||||
{% block title %}검토 요약 확정 · {{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="prism">
|
||||
<div class="stack">
|
||||
<div class="stack" style="gap:var(--prism-space-100)">
|
||||
<span class="eyebrow">입력 묶음 · 검토 요약 확정</span>
|
||||
<h1 style="font-size:var(--prism-type-title);color:var(--prism-text-heading);margin:0">{{ p.name }} — 한 화면에서 검토하고 한 번에 확정</h1>
|
||||
</div>
|
||||
{% if not items %}
|
||||
<section class="strip"><p class="concl">확정이 필요한 묶음이 없습니다.</p>
|
||||
<p class="cap">필요한 묶음이 모두 확정되어 있습니다. 값을 고치면 해당 묶음만 다시 여기에 나타납니다.</p></section>
|
||||
<div class="row"><a class="btn" href="{{ url_for('pier', name=p.name) }}">교각 화면으로</a></div>
|
||||
{% else %}
|
||||
<section class="strip">
|
||||
<p class="concl">확정이 필요한 묶음 {{ items|length }}개 — 일치 {{ cnt.A }} · 주의·근거 없음 {{ cnt.B }} · 결정 기록 {{ cnt.D }} · 불일치 {{ cnt.C }}</p>
|
||||
<p class="cap">앱이 <b>무엇이 바뀌었는지</b>, <b>앱이 무엇을 확인했는지</b>, <b>앱이 확인하지 못한 것</b>을 정리했습니다. 확정은 사람의 기록이므로 <b>도면과 대조한 묶음에 직접 체크</b>해야 하고,
|
||||
체크하지 않은 묶음은 미확정으로 남습니다. 사유는 앱이 아는 사실(앱 확인·변경·출처)과 아래 의견으로 자동 기록됩니다.</p>
|
||||
</section>
|
||||
<form method="post" id="rf" class="stack">
|
||||
|
||||
{% if by_group.C %}
|
||||
<h2>불일치 {{ cnt.C }}개 — 확정할 수 없습니다</h2>
|
||||
<div class="banner dont" role="alert"><span class="m" aria-hidden="true">■</span><div>값을 고치거나 도면과 다른 이유를 해결한 뒤 다시 확인하세요(묶음을 열어 고치면 이 화면에 다시 나타납니다).</div></div>
|
||||
<div class="tw"><table class="t"><thead><tr><th scope="col">묶음</th><th scope="col">앱 확인</th><th scope="col">불일치 내용</th></tr></thead><tbody>
|
||||
{% for i in by_group.C %}<tr><td><a href="{{ url_for('evidence', name=p.name, which=i.file, key=i.key) }}">{{ i.id }}</a></td><td>{{ st_badge(i.st) }}</td><td>{{ i.summary }}</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
{% endif %}
|
||||
|
||||
{% if by_group.B %}
|
||||
<h2>주의·근거 없음 {{ cnt.B }}개 — 도면과 대조한 것에 체크</h2>
|
||||
<div class="row"><button class="btn" type="button" id="all-on" data-flagged="{{ by_group.B|selectattr('flags')|list|length + by_group.D|selectattr('flags')|list|length }}">주의·근거 없음·결정 기록 모두 도면과 대조함으로 체크</button>
|
||||
<button class="btn quiet" type="button" id="all-off">모두 해제</button></div>
|
||||
<p class="cap">앱이 독립적으로 대조하지 못했거나 일부만 확인한 묶음입니다. 아래 "앱이 확인하지 못한 것"을 도면과 직접 대조한 뒤 체크하세요. 체크 하나가 그 묶음의 확정 기록입니다.</p>
|
||||
<div class="tw"><table class="t"><thead><tr><th scope="col">대조함</th><th scope="col">묶음</th><th scope="col">바뀐 것</th><th scope="col">앱이 확인하지 못한 것</th><th scope="col">값의 출처</th></tr></thead><tbody>
|
||||
{% for i in by_group.B %}
|
||||
<tr><td><input type="checkbox" name="ack" value="{{ i.id }}" aria-label="{{ i.id }} 도면과 대조함" style="width:1.4rem;height:1.4rem"></td>
|
||||
<td><b><a href="{{ url_for('evidence', name=p.name, which=i.file, key=i.key) }}">{{ i.id }}</a></b><br>{{ st_badge(i.st) }}{% if i.stale %} <span class="cap muted">변경됨(이전 확정 {{ i.prev[0] }})</span>{% endif %}</td>
|
||||
<td>{{ i.change.text }}{% for fl in i.flags %}<br><span class="b warn"><span class="m" aria-hidden="true">▲</span>{{ fl }}</span>{% endfor %}</td>
|
||||
<td>{% if i.unverified %}<ul style="margin:0;padding-left:1.1rem">{% for u in i.unverified[:3] %}<li>{{ u }}</li>{% endfor %}</ul>{% else %}{{ i.summary }}{% endif %}</td>
|
||||
<td>{% if i.prov %}{% for pv in i.prov %}<span class="chip">{{ pv }}</span> {% endfor %}{% else %}<span class="cap muted">기록 없음</span>{% endif %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody></table></div>
|
||||
{% endif %}
|
||||
|
||||
{% if by_group.D %}
|
||||
<h2>사람 결정 기록 {{ cnt.D }}개 — 내용을 읽고 체크</h2>
|
||||
<div class="tw"><table class="t"><thead><tr><th scope="col">읽음</th><th scope="col">묶음</th><th scope="col">내용</th></tr></thead><tbody>
|
||||
{% for i in by_group.D %}
|
||||
<tr><td><input type="checkbox" name="ack" value="{{ i.id }}" aria-label="{{ i.id }} 읽었음" style="width:1.4rem;height:1.4rem"></td>
|
||||
<td><b>{{ i.id }}</b><br>{{ st_badge(i.st) }}</td>
|
||||
<td>{% if i.detail %}<ul style="margin:0;padding-left:1.1rem">{% for d in i.detail %}<li>{{ d }}</li>{% endfor %}</ul>
|
||||
<p class="cap muted">계획된 FAIL(승인된 예외)은 도면과 재료표의 알려진 차이를 받아들이는 사람의 결정입니다. 설계자 확인 전 임시 등록은 사유에 그렇게 적혀 있어야 합니다.</p>
|
||||
{% else %}{{ i.change.text }}{% endif %}{% for fl in i.flags %}<br><span class="b warn"><span class="m" aria-hidden="true">▲</span>{{ fl }}</span>{% endfor %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody></table></div>
|
||||
{% endif %}
|
||||
|
||||
{% if by_group.A %}
|
||||
<h2>앱 확인 일치 {{ cnt.A }}개 — 근거를 훑어보세요</h2>
|
||||
<div class="tw"><table class="t"><thead><tr><th scope="col">묶음</th><th scope="col">바뀐 것</th><th scope="col">앱이 확인한 것</th></tr></thead><tbody>
|
||||
{% for i in by_group.A %}
|
||||
{% set c = (i.checks | selectattr('st', 'equalto', 'g') | selectattr('strength', 'in', ['S', 'F', 'C']) | list) %}
|
||||
<tr><td><b><a href="{{ url_for('evidence', name=p.name, which=i.file, key=i.key) }}">{{ i.id }}</a></b></td><td>{{ i.change.text }}</td>
|
||||
<td>{% if c %}{{ c[0].id }} {{ c[0].text }}{% if c[0].pos %} <span class="chip">{{ c[0].pos }}</span>{% endif %}{% else %}{{ i.summary }}{% endif %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody></table></div>
|
||||
<label class="row" style="gap:.5rem"><input type="checkbox" name="ack_matched" value="1" style="width:1.4rem;height:1.4rem"> 일치 {{ cnt.A }}개의 근거를 읽었습니다(앱이 독립 근거와 같다고 확인한 묶음입니다)</label>
|
||||
{% endif %}
|
||||
|
||||
<h2>확정</h2>
|
||||
<div class="field"><label for="rby">확정자<span class="req">필수</span></label>
|
||||
<input id="rby" type="text" name="by" required style="max-width:20rem" disabled>
|
||||
<span class="help" id="rhint">목록을 끝까지 내리면 입력할 수 있습니다.</span></div>
|
||||
<div class="field"><label for="rnote">공통 의견 (선택)</label>
|
||||
<input id="rnote" type="text" name="note" style="max-width:40rem" placeholder="예: 운곡교 P5 도면(2026-10 개정)과 대조" disabled>
|
||||
<span class="help">모든 확정 기록의 사유 끝에 붙습니다. 앱이 쓰는 사유(앱 확인·변경·출처)는 따로 자동으로 남습니다.</span></div>
|
||||
<div class="row end"><a class="btn" href="{{ url_for('pier', name=p.name) }}">취소</a>
|
||||
<button class="btn primary" id="rok" type="submit" disabled>선택한 묶음 확정</button></div>
|
||||
<div id="rend" aria-hidden="true" style="height:1px"></div>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
var on = document.getElementById('all-on'), off = document.getElementById('all-off');
|
||||
function boxes() { return document.querySelectorAll('input[name=ack]'); }
|
||||
if (on) on.addEventListener('click', function () {
|
||||
var n = parseInt(on.getAttribute('data-flagged') || '0', 10);
|
||||
if (n && !confirm('주의 표시가 붙은 묶음 ' + n + '개(템플릿 원본 그대로이거나 설계자 확인 전 임시 등록)가 포함됩니다. 모두 도면과 대조했습니까?')) return;
|
||||
boxes().forEach(function (b) { b.checked = true; });
|
||||
});
|
||||
if (off) off.addEventListener('click', function () { boxes().forEach(function (b) { b.checked = false; }); });
|
||||
})();
|
||||
(function () {
|
||||
var end = document.getElementById('rend'), by = document.getElementById('rby'), note = document.getElementById('rnote'), ok = document.getElementById('rok'), hint = document.getElementById('rhint'), sent = false;
|
||||
function done() {
|
||||
if (sent) return; sent = true;
|
||||
fetch({{ url_for('review_read', name=p.name)|tojson }}, {method: 'POST'}).then(function () {
|
||||
by.disabled = false; note.disabled = false; ok.disabled = false; hint.textContent = '목록을 끝까지 읽었습니다. 이름을 입력하고 확정하세요.'; by.focus();
|
||||
});
|
||||
}
|
||||
if ('IntersectionObserver' in window) { new IntersectionObserver(function (e) { if (e[0].isIntersecting) done(); }).observe(end); } else { done(); }
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}문자 검색 · {{ p.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>도면 문자 검색</h1>
|
||||
{% if not audit %}<p class="fail">도면이 없습니다. 교각 화면에서 DWG를 먼저 올리세요.</p>{% endif %}
|
||||
<form>
|
||||
<input type="text" name="q" value="{{ q }}" size="40" placeholder="예: EL. 26.208, 9.350, @250">
|
||||
<label><input type="checkbox" name="regex" value="1" {% if regex %}checked{% endif %}> 정규식</label>
|
||||
<button>검색</button>
|
||||
</form>
|
||||
{% if err %}<p class="fail">{{ err }}</p>{% endif %}
|
||||
{% if q %}<p>{{ hits|length }}건{% if hits|length >= 200 %} (최대 200건만 표시){% endif %}</p>
|
||||
<div class="wrap"><table>
|
||||
<tr><th>도면</th><th>레이어</th><th>X</th><th>Y</th><th>글자 높이</th><th>문자</th></tr>
|
||||
{% for t in hits %}<tr><td>{{ t.dwg }}</td><td>{{ t.layer }}</td><td>{{ t.x }}</td><td>{{ t.y }}</td><td>{{ t.h }}</td><td>{{ t.text }}</td></tr>{% endfor %}
|
||||
</table></div>{% endif %}
|
||||
<h2>핸들 조회</h2>
|
||||
<p class="muted">CAD에서 확인한 객체 핸들(예: 21A76)로 선·폴리선·호·원·문자·블록의 좌표·길이·반지름을 봅니다. 값은 자동 반영하지 않습니다.</p>
|
||||
<form>
|
||||
<select name="dwg">{% for a in audit %}<option value="{{ a.label }}" {% if a.label == hdwg %}selected{% endif %}>{{ a.label }} · {{ a.source }}</option>{% endfor %}</select>
|
||||
<input type="text" name="h" value="{{ h }}" size="10" placeholder="핸들">
|
||||
<button>조회</button>
|
||||
</form>
|
||||
{% if ent %}<p>{{ hdwg }} · <code>{{ ent.handle }}</code> · {{ ent.type }} · 레이어 {{ ent.layer }}</p>
|
||||
<div class="wrap"><table>{% for k, v in ent['values'].items() %}<tr><th>{{ k }}</th><td><code>{{ dumps(v) }}</code></td></tr>{% endfor %}</table></div>{% endif %}
|
||||
<p class="muted">좌표는 도면(DXF) 좌표이며 모델 좌표가 아닙니다. 이 도면 세트에서는 치수가 치수 객체가 아니라 문자로 그려져 있어 문자 검색으로 찾습니다.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ which }}.{{ key }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>{{ which }}.{{ key }}
|
||||
{% if st.confirmed %}<span class="ok">확정</span> <span class="muted">{{ st.by }} · {{ st.at }}</span>
|
||||
{% elif st.info %}<span class="muted">확정 불필요(참고)</span>
|
||||
{% elif st.stale %}<span class="warn">변경됨 · 재확정 필요</span>{% else %}<span class="muted">미확정</span>{% endif %}</h1>
|
||||
<!-- stage A 2026-10-16 -->
|
||||
<p><b>{{ meta.grade }} {{ grades[meta.grade] }}</b> · 값 출처 {{ meta.source|join(' · ') }}{% if meta.origin %} · <span class="warn">▲ 템플릿 원본 그대로</span>{% endif %}
|
||||
<span class="muted"> — {{ meta.note }}{% if meta.fixed_fields %} (고정 항목: {{ meta.fixed_fields|join(', ') }}){% endif %}</span></p>
|
||||
{% if meta.grade == 'V3' %}<p class="fail">■ 생성 변수(V3)는 화면에서 직접 고칠 수 없습니다. 판독 스크립트를 다시 실행해 목록을 만든 뒤 반영하세요. 저장 단추는 거부됩니다.</p>
|
||||
{% elif meta.grade == 'F' or meta.fixed_fields %}<p class="warn">▲ 이 묶음에는 고정(F) 항목이 있습니다. 바꾸면 같은 템플릿이 아닐 수 있으므로 <b>바꾸는 사유</b>를 입력해야 저장되고, 사유는 기록됩니다.</p>{% endif %}
|
||||
<p class="muted">{% if which == 'expected' %}검사 기대값입니다. 생성용 params와 따로, 도면 치수 문자·표에서 직접 옮긴 값이어야 합니다(독립성).{% else %}모델 생성용 매개변수입니다. 단위 mm, Z = EL × 1000, 교각 중심 원점(X 교축직각, Y 교축).{% endif %}
|
||||
값은 JSON으로 편집합니다. 최상위 형식(객체·목록·숫자 등)은 바꿀 수 없습니다.</p>
|
||||
<p class="muted">위치 목록은 치수 사슬로 입력할 수 있습니다: <code>{"chain": "-4575, 200, 2@125, 30@250"}</code>
|
||||
— 첫 값은 시작 위치, 나머지는 증분이며 <code>n@p</code>는 간격 p를 n번 반복합니다. 저장하면 숫자 목록으로 펼쳐집니다.</p>
|
||||
<form method="post">
|
||||
<textarea name="value" spellcheck="false">{{ text }}</textarea>
|
||||
{% if meta.grade == 'F' or meta.fixed_fields %}<p>고정 항목을 바꾸는 사유 <input type="text" name="reason" size="60"></p>{% endif %}
|
||||
<p><button>저장</button></p>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('section_confirm', name=p.name, which=which, key=key) }}">
|
||||
확정자 <input type="text" name="by" required>
|
||||
<input type="hidden" name="next" value="{{ url_for('section', name=p.name, which=which, key=key) }}">
|
||||
사유 <input type="text" name="why" size="50" placeholder="앱 확인이 '일치'가 아니면 필수">
|
||||
<button>저장된 값으로 확정</button> <a href="{{ url_for('evidence', name=p.name, which=which, key=key) }}">근거 보기</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user