- 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>
432 lines
41 KiB
HTML
432 lines
41 KiB
HTML
<!doctype html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PierProof 시안 — 입력 묶음·새 교각 판별</title>
|
|
<link rel="stylesheet" href="pier.css">
|
|
<style>
|
|
@font-face { font-family: "Pretendard"; src: url("fonts/PretendardVariable.woff2") format("woff2-variations"); font-weight: 45 920; font-display: swap; }
|
|
body { margin: 0; }
|
|
.toast { position: fixed; right: var(--prism-space-300); bottom: var(--prism-space-300); z-index: 700; max-width: 28rem; background: var(--prism-bg-card);
|
|
border: var(--prism-stroke-regular) solid var(--prism-comp-line-strong); border-radius: var(--prism-radius-md); padding: var(--prism-space-150) var(--prism-space-200); }
|
|
.toast[hidden] { display: none; }
|
|
.scrim { position: fixed; }
|
|
.demo { background: var(--prism-text-heading); color: var(--prism-comp-on-emphasis); font-size: var(--prism-type-caption); padding: var(--prism-space-050) var(--prism-space-300); }
|
|
.rail a.step { cursor: pointer; }
|
|
a[data-r] { cursor: pointer; }
|
|
td .btn.sm { min-height: var(--prism-comp-hit-min); }
|
|
.pbar.run > i { transition: width var(--prism-motion-fast) linear; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a class="skip" href="#main">본문 바로가기</a>
|
|
<div class="demo">시안 — 동작 흐름 확인용. 데이터는 예시이며 실제 앱 결과가 아니다. 새로고침하면 처음 상태로 돌아간다.</div>
|
|
<div class="app" id="app">
|
|
<header class="top">
|
|
<div class="brand"><i></i>PierProof</div>
|
|
<span class="where" id="where"></span>
|
|
<div class="meta"><span>권한 담당자</span><span>저장 14:32</span><span>환경 로컬</span></div>
|
|
</header>
|
|
<div class="body">
|
|
<nav class="rail" aria-label="교각 작업 단계">
|
|
<span class="cap">교각</span>
|
|
<span class="pier">옥성교P1</span>
|
|
<ol>
|
|
<li><a class="step done" tabindex="0"><span class="n" aria-hidden="true">✓</span>1 도면</a></li>
|
|
<li><a class="step" id="nav-bundles" href="#/bundles"><span class="n">2</span>2 입력 묶음</a></li>
|
|
<li><a class="step" tabindex="0"><span class="n">3</span>3 게이트</a></li>
|
|
<li><a class="step" tabindex="0"><span class="n">4</span>4 보고서</a></li>
|
|
</ol>
|
|
<hr>
|
|
<div class="sub"><a href="#/candidates" id="nav-cand">표 후보값</a></div>
|
|
<div class="sub"><a href="#/new" id="nav-new">새 교각 (S02)</a></div>
|
|
</nav>
|
|
<main class="main" id="main" tabindex="-1"></main>
|
|
</div>
|
|
</div>
|
|
<div id="modalHost"></div>
|
|
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
/* ---------- data (sample) ---------- */
|
|
var GRADE = { V1: ['', 'V1 값'], V2: ['', 'V2 목록'], V3: ['dash', 'V3 생성'], F: ['fixed', 'F 고정'] };
|
|
var ST = { g: ['good', '●', '일치', 3], w: ['warn', '▲', '주의', 2], b: ['bad', '■', '불일치', 0], n: ['dash', '◇', '근거 없음', 1] };
|
|
var RAW = [
|
|
['params · lean_concrete', 'V1', '표 밖 치수', 'g'], ['params · footing', 'V1', '표 밖 치수', 'g'], ['params · column', 'V1', '표 밖 치수', 'g'],
|
|
['params · materials', 'V1', '표', 'g'], ['params · coping', 'V2', '표 밖 치수', 'w'], ['params · bearing_seats', 'V1', '표', 'g'],
|
|
['params · sole_plates', 'V1', '표', 'g'], ['params · rebar_marks', 'V1', '표', 'g'], ['params · column_rebar', 'V2', '표 밖 치수', 'g'],
|
|
['params · footing_rebar', 'V3', '판독 스크립트', 'g'], ['params · coping_rebar', 'V3', '판독 스크립트', 'w'], ['params · display', 'V1', '', 'n', true],
|
|
['expected · G1', 'V1', '표 밖 치수', 'g'], ['expected · G1.pedestal', 'V1', '표', 'g'], ['expected · G2', 'V2', '표 밖 치수', 'g'],
|
|
['expected · G3', 'V2', '표 밖 치수', 'g'], ['expected · G4', 'V2', '표 밖 치수', 'b'], ['expected · G5', 'V1', '표', 'g'],
|
|
['expected · planned_fail', 'V1', '사람 입력', 'n'], ['expected · rebar_palette', 'V1', '', 'n', true], ['expected · checkers', 'F', '템플릿 고정', 'g'],
|
|
['expected · schedule', 'V1', '표', 'g'], ['expected · dia', 'V1', '표', 'g'], ['expected · fixed_length', 'V2', '표 밖 치수', 'w'],
|
|
['expected · loops', 'V2', '표 밖 치수', 'w'], ['expected · seats', 'V2', '표 밖 치수', 'g'], ['expected · c18', 'V2', '표 밖 치수', 'g']
|
|
];
|
|
var rows = RAW.map(function (r, i) {
|
|
return { i: i, key: r[0], g: r[1], src: r[2], st: r[3], orig: !!r[4], human: null, stale: false };
|
|
});
|
|
var SRC_TEXT = { '표': '도면 표(D1)', '표 밖 치수': '구조도 정면·단면의 치수 문자(D1)', '판독 스크립트': '판독 스크립트 실행 기록', '사람 입력': '담당자 입력 기록', '템플릿 고정': '템플릿 고정값' };
|
|
var CAND = [
|
|
{ id: 1, bundle: 'params · coping', file: 'params', target: 'coping.z_tip', cur: 31232, cand: 31231.5, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'B', pos: 'D1 (225817, 1802)', pair: 'ztip' },
|
|
{ id: 2, bundle: 'expected · G1', file: 'expected', target: 'G1.coping.z_tip', cur: 31232, cand: 31231.5, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'B', pos: 'D1 (225817, 1802)', pair: 'ztip' },
|
|
{ id: 3, bundle: 'params · coping', file: 'params', target: 'coping.z_top_right', cur: 32936, cand: 32936, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'A', pos: 'D1 (225817, 4366)' },
|
|
{ id: 4, bundle: 'params · footing', file: 'params', target: 'footing.height', cur: 2500, cand: 2450, kind: '표 밖 치수', rule: 'R2 사슬', trust: 'A', pos: 'D1 (231004, 902)' },
|
|
{ id: 5, bundle: 'params · bearing_seats', file: 'params', target: 'bearing_seats[0].h1', cur: 209, cand: 209, kind: '표', rule: 'ELEVATION TABLE', trust: 'T', pos: 'D1 표 행 1' },
|
|
{ id: 6, bundle: 'params · sole_plates', file: 'params', target: 'sole_plates[3].S', cur: -0.46, cand: -0.52, kind: '표', rule: 'SOLE PLATE TABLE', trust: 'T', pos: 'D1 표 행 4' },
|
|
{ id: 7, bundle: 'params · column_rebar', file: 'params', target: 'column_rebar.hoops[2]', cur: '4@300', cand: '4@300', kind: '표 밖 치수', rule: 'R2 사슬', trust: 'A', pos: 'D2 (224410, 7120)' },
|
|
{ id: 8, bundle: 'params · coping_rebar', file: 'params', target: 'coping_rebar.stirrups.count', cur: 104, cand: 106, kind: '표 밖 치수', rule: 'R6 수량 대조', trust: 'C', pos: 'D1 점 94곳' }
|
|
];
|
|
var TRUST = { A: ['', '등급 A'], B: ['warn', '등급 B'], C: ['dash', '등급 C'], T: ['', '표'] };
|
|
|
|
/* ---------- state ---------- */
|
|
var S = { filter: 'all', fGrade: '', fSrc: '', by: '심영표', checking: false, checkedAt: '14:30', sel: {}, lastBulk: '', flash: null };
|
|
|
|
/* ---------- helpers ---------- */
|
|
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
function esc(t) { return String(t).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); }
|
|
function badge(kind, mark, label) { return '<span class="b ' + kind + '">' + (mark ? '<span class="m" aria-hidden="true">' + mark + '</span>' : '') + esc(label) + '</span>'; }
|
|
function stBadge(r) {
|
|
if (r.stale) return badge('warn', '◆', '변경됨');
|
|
var s = ST[r.st]; return badge(s[0], s[1], s[2]);
|
|
}
|
|
function humanBadge(r) { return r.human ? '<span class="b fixed"><span class="m" aria-hidden="true">●</span>확정</span>' : (r.stale && r.staleHuman ? badge('warn', '◆', '변경됨') : '<span class="b dash">미확정</span>'); }
|
|
function fmt(v) { return typeof v === 'number' ? v.toLocaleString('ko-KR') : v; }
|
|
function toast(msg) {
|
|
var t = $('#toast'); t.textContent = msg; t.hidden = false;
|
|
clearTimeout(toast._t); toast._t = setTimeout(function () { t.hidden = true; }, 6000);
|
|
}
|
|
function counts() {
|
|
var c = { g: 0, w: 0, b: 0, n: 0, human: 0 };
|
|
rows.forEach(function (r) { c[r.st]++; if (r.human) c.human++; });
|
|
return c;
|
|
}
|
|
function sorted() { return rows.slice().sort(function (a, b) { return (ST[a.st][3] - ST[b.st][3]) || (a.i - b.i); }); }
|
|
|
|
/* ---------- evidence ---------- */
|
|
function evidence(r) {
|
|
var src = SRC_TEXT[r.src] || '';
|
|
var hex = (0x2c41 + r.i * 37).toString(16).toUpperCase();
|
|
var pos = 'D1 · 핸들 ' + hex;
|
|
var isExp = r.key.indexOf('expected') === 0;
|
|
var ev = [
|
|
{ id: 'C1', name: '출처 있음', st: 'g', text: '이 묶음의 값은 ' + src + '에서 읽었다.', pos: pos },
|
|
{ id: 'C2', name: '도면 재대조', st: 'g', text: '현재 값이 ' + src + '의 후보 값과 같다.', pos: pos },
|
|
{ id: 'C3', name: '템플릿 원본 잔존', st: 'g', text: '원본과 다른 값은 ' + (30 + r.i) + '개이고 같은 값 ' + (r.i % 4) + '개는 모두 도면 근거가 있다.', pos: '—' },
|
|
{ id: 'C4', name: '내부 정합', st: 'g', text: '치수 사슬 합이 전체 치수와 같고 본수가 재료표 수량과 같다.', pos: '재료표' },
|
|
{ id: 'C5', name: 'params와 독립', st: isExp ? 'g' : 'x', text: isExp ? 'expected의 출처가 params의 출처와 다르다.' : '해당 없음. params 묶음이다.', pos: '—' },
|
|
{ id: 'C6', name: '범위·단위', st: 'g', text: 'EL과 길이가 상식 범위 안에 있고 단위가 mm이다.', pos: '—' },
|
|
{ id: 'C7', name: '변수 등급 위반', st: 'g', text: r.g === 'F' ? 'F 고정 항목이 템플릿 원본과 같다.' : 'F 고정 항목이 템플릿 원본과 다르지 않다.', pos: '—' },
|
|
{ id: 'C8', name: '재생성 일치', st: r.g === 'V3' ? 'g' : 'x', text: r.g === 'V3' ? '재생성 목록의 해시가 현재 값과 같다 (독립성 낮음).' : '해당 없음. V3 묶음이 아니다.', pos: '판독 스크립트' }
|
|
];
|
|
function set(id, st, text) { ev.forEach(function (e) { if (e.id === id) { e.st = st; e.text = text; } }); }
|
|
if (r.src === '표') set('C1', 'g', '이 묶음의 값은 도면 표(D1)의 행에서 읽었다.');
|
|
if (r.key === 'expected · G4') set('C4', 'b', '스터럽 곳 수 합 104가 재료표 C8~C13 수량 합 106(39 + 27 + 40)과 다르다.');
|
|
if (r.key === 'params · coping') set('C2', 'w', '단 높이가 표 값과 1.1 mm 어긋난다. 허용 범위 안이다.');
|
|
if (r.key === 'params · coping_rebar') set('C8', 'w', '재생성 목록의 해시가 같다. 독립성이 낮아 도면 직접 대조가 필요하다.');
|
|
if (r.key === 'expected · fixed_length') set('C4', 'w', '길이가 재료표 L과 겹이음만큼 다르다. 허용 범위 안이다.');
|
|
if (r.key === 'expected · loops') set('C2', 'w', '측면 고리 한 단이 점 목록과 3 mm 다르다. 허용 범위 안이다.');
|
|
if (r.st === 'n') {
|
|
set('C1', 'n', r.key === 'expected · planned_fail' ? '사람 결정 기록이다. 도면 출처가 없다.' : '출처를 찾지 못했다. 사람이 도면에서 직접 확인한다.');
|
|
set('C2', 'x', '비교할 후보가 없다.'); set('C4', 'x', '비교할 항목이 없다.'); set('C6', 'x', '검사 대상 수치가 없다.');
|
|
if (r.orig) set('C3', 'n', '현재 값이 템플릿 원본과 같고 새 도면 근거가 없다.');
|
|
}
|
|
return ev;
|
|
}
|
|
var EVST = { g: ['good', '●', '일치'], w: ['warn', '▲', '주의'], b: ['bad', '■', '불일치'], n: ['dash', '◇', '근거 없음'], x: ['', '', '—'] };
|
|
|
|
/* ---------- views ---------- */
|
|
var main = $('#main');
|
|
function setWhere(t) { $('#where').textContent = t; }
|
|
|
|
function viewBundles() {
|
|
setWhere('S04 · 옥성교P1 · 입력 묶음');
|
|
var c = counts();
|
|
var list = sorted().filter(function (r) {
|
|
if (S.filter !== 'all' && r.st !== S.filter) return false;
|
|
if (S.fGrade && r.g !== S.fGrade) return false;
|
|
if (S.fSrc && (r.orig ? '원본값' : r.src) !== S.fSrc) return false;
|
|
return true;
|
|
});
|
|
var origN = rows.filter(function (r) { return r.orig; }).length;
|
|
var html = '<div class="head"><span class="eyebrow">2단계</span><h1>입력 묶음 확인·확정</h1><p class="lead">앱이 묶음을 도면과 대조해 근거를 보인다. 확정은 근거를 읽은 담당자가 한다.</p></div>';
|
|
if (S.flash) { html += '<div class="banner good" role="status"><span class="m" aria-hidden="true">●</span><div>' + esc(S.flash) + '</div></div>'; }
|
|
html += '<section class="strip" aria-label="요약"><p class="concl">필요 27개 중 앱 확인 일치 ' + c.g + ' · 주의 ' + c.w + ' · 불일치 ' + c.b + ' · 근거 없음 ' + c.n + ' · 사람 확정 ' + c.human + '</p><div class="counts">';
|
|
[['all', '전체', '', 27], ['g', '일치', ST.g, c.g], ['w', '주의', ST.w, c.w], ['b', '불일치', ST.b, c.b], ['n', '근거 없음', ST.n, c.n]].forEach(function (f) {
|
|
var lab = f[2] ? badge(f[2][0], f[2][1], f[1]) : '<span>' + f[1] + '</span>';
|
|
html += '<button class="cnt' + (S.filter === f[0] ? ' sel' : '') + '" type="button" data-filter="' + f[0] + '" aria-pressed="' + (S.filter === f[0]) + '">' + lab + '<b class="num">' + f[3] + '</b></button>';
|
|
});
|
|
html += '</div>';
|
|
if (S.checking) html += '<div class="stack" style="gap:0.25rem"><span class="cap" id="runtxt">앱 확인 중 0/27 묶음</span><div class="pbar run" role="progressbar" aria-valuemin="0" aria-valuemax="27" aria-valuenow="0" id="runbar"><i style="width:0%"></i></div></div>';
|
|
else html += '<span class="cap muted">마지막 앱 확인 ' + S.checkedAt + ' · 검사 v0.1 · 재확인 필요 ' + rows.filter(function (r) { return r.stale; }).length + '개</span>';
|
|
html += '</section>';
|
|
html += '<div class="banner"><span class="m" aria-hidden="true">▲</span><div><b>원본값 ' + origN + '개.</b> 템플릿 원본과 같고 새 도면 근거가 없다. 일괄 확정에서 제외되며, 새 도면과 대조해 값이 바뀌거나 근거가 붙어야 풀린다.</div></div>';
|
|
html += '<div class="row"><div class="field"><label for="fg">변수 등급</label><select id="fg"><option value="">전체</option>' +
|
|
['V1', 'V2', 'V3', 'F'].map(function (g) { return '<option value="' + g + '"' + (S.fGrade === g ? ' selected' : '') + '>' + GRADE[g][1] + '</option>'; }).join('') + '</select></div>' +
|
|
'<div class="field"><label for="fs">값 출처</label><select id="fs"><option value="">전체</option>' +
|
|
['표', '표 밖 치수', '판독 스크립트', '원본값', '사람 입력', '템플릿 고정'].map(function (g) { return '<option value="' + g + '"' + (S.fSrc === g ? ' selected' : '') + '>' + g + '</option>'; }).join('') + '</select></div>' +
|
|
'<span class="grow"></span><button class="btn" type="button" id="runAll">전체 확인</button><button class="btn primary" type="button" id="bulk">일치분 확정</button></div>';
|
|
html += '<div class="tw"><table class="t"><caption>입력 묶음 ' + list.length + '개 · 불일치 → 근거 없음 → 주의 → 일치 순</caption><thead><tr><th scope="col">묶음</th><th scope="col">변수 등급</th><th scope="col">값 출처</th><th scope="col">앱 확인</th><th scope="col">사람 확정</th><th scope="col">근거</th></tr></thead><tbody>';
|
|
if (!list.length) html += '<tr><td colspan="6">조건에 맞는 묶음이 없다. 필터를 바꾼다.</td></tr>';
|
|
list.forEach(function (r) {
|
|
html += '<tr><td><a href="#/evidence/' + r.i + '">' + esc(r.key) + '</a></td><td>' + badge(GRADE[r.g][0], '', GRADE[r.g][1]) + '</td><td>' + esc(r.src) +
|
|
(r.orig ? badge('warn', '▲', '원본값') : '') + '</td><td>' + stBadge(r) + (r.stale ? ' <button class="btn quiet sm" type="button" data-recheck="' + r.i + '">확인 실행</button>' : '') +
|
|
'</td><td>' + (r.human ? humanBadge(r) + ' <span class="cap muted">' + esc(r.human.by) + ' ' + esc(r.human.at) + '</span>' : humanBadge(r)) +
|
|
'</td><td><a class="btn quiet sm" href="#/evidence/' + r.i + '">근거 보기</a></td></tr>';
|
|
});
|
|
html += '</tbody></table></div>';
|
|
html += '<details class="card" style="padding:0"><summary style="min-height:2.75rem;display:flex;align-items:center;cursor:pointer;font-weight:700;padding:0 0.75rem">확정 불필요(참고) 5개</summary><div style="padding:0 1rem 1rem" class="muted">_schema · _meta · pier_id_src · hitl_decisions_note · display_notes. 확정 대상이 아니며 앱 확인도 하지 않는다.</div></details>';
|
|
main.innerHTML = html;
|
|
|
|
main.querySelectorAll('[data-filter]').forEach(function (b) { b.onclick = function () { S.filter = b.getAttribute('data-filter'); viewBundles(); $('[data-filter="' + S.filter + '"]').focus(); }; });
|
|
$('#fg').onchange = function (e) { S.fGrade = e.target.value; viewBundles(); $('#fg').focus(); };
|
|
$('#fs').onchange = function (e) { S.fSrc = e.target.value; viewBundles(); $('#fs').focus(); };
|
|
$('#runAll').onclick = runAll;
|
|
$('#bulk').onclick = openBulk;
|
|
main.querySelectorAll('[data-recheck]').forEach(function (b) { b.onclick = function () { var r = rows[+b.getAttribute('data-recheck')]; r.stale = false; r.staleHuman = false; toast(r.key + ' 앱 확인을 다시 실행했다. 결과는 이전과 같다.'); viewBundles(); }; });
|
|
S.flash = null;
|
|
}
|
|
|
|
function runAll() {
|
|
if (S.checking) return;
|
|
S.checking = true; viewBundles();
|
|
var n = 0, bar = $('#runbar'), txt = $('#runtxt');
|
|
var timer = setInterval(function () {
|
|
n += 3; if (n > 27) n = 27;
|
|
bar.firstElementChild.style.width = (n / 27 * 100) + '%'; bar.setAttribute('aria-valuenow', n); txt.textContent = '앱 확인 중 ' + n + '/27 묶음';
|
|
if (n >= 27) {
|
|
clearInterval(timer); S.checking = false; S.checkedAt = '14:36';
|
|
rows.forEach(function (r) { r.stale = false; r.staleHuman = false; });
|
|
toast('앱 확인을 마쳤다. 결과는 요약 띠에 반영되었다.'); viewBundles();
|
|
}
|
|
}, 120);
|
|
}
|
|
|
|
function viewEvidence(idx) {
|
|
var r = rows[idx]; if (!r) { location.hash = '#/bundles'; return; }
|
|
setWhere('S05 · 옥성교P1 · 근거 보기');
|
|
var ev = evidence(r), s = ST[r.st];
|
|
var order = sorted();
|
|
var pos = order.indexOf(r);
|
|
var next = null;
|
|
for (var k = 1; k <= order.length; k++) { var cand = order[(pos + k) % order.length]; if (!cand.human && cand !== r) { next = cand; break; } }
|
|
var html = '<div class="head"><span class="eyebrow">2단계 · 입력 묶음</span><h1>근거 보기</h1></div><div class="split"><aside class="card" style="padding:0" aria-label="묶음 이동"><h3 style="padding:1rem 1rem 0.5rem">묶음 이동</h3><ul class="list">';
|
|
order.forEach(function (o) {
|
|
html += '<li class="' + (o === r ? 'cur' : '') + '"><a href="#/evidence/' + o.i + '"' + (o === r ? ' aria-current="true"' : '') + '>' + esc(o.key) + '</a>' + stBadge(o) + '</li>';
|
|
});
|
|
html += '</ul><div class="row" style="padding:0.75rem 1rem"><a class="btn sm" href="#/bundles">목록으로</a></div></aside><div class="stack">';
|
|
var concl = r.stale ? esc(r.key) + ' — 값이 바뀌었다. 이전 확인과 확정이 무효가 되었다.' :
|
|
esc(r.key) + ' — 앱 확인 ' + s[2] + '. ' + (r.st === 'g' ? '근거를 읽고 확정한다.' : '사람 확정 전에 사유가 필요하다.');
|
|
html += '<section class="strip"><p class="concl">' + concl + '</p><dl class="kv"><dt>변수 등급</dt><dd>' + badge(GRADE[r.g][0], '', GRADE[r.g][1]) + '</dd><dt>앱 확인</dt><dd>' + stBadge(r) + '</dd><dt>사람 확정</dt><dd>' +
|
|
(r.human ? humanBadge(r) + ' ' + esc(r.human.by) + ' · ' + esc(r.human.at) + (r.human.why ? ' · 사유: ' + esc(r.human.why) : '') : '<span class="b dash">미확정</span>') + '</dd><dt>검사</dt><dd>v0.1 · ' + S.checkedAt + '</dd></dl></section>';
|
|
html += '<div class="tw"><table class="t"><caption>앱 확인 근거 C1 ~ C8</caption><thead><tr><th scope="col">검사</th><th scope="col">결과</th><th scope="col">근거</th><th scope="col">도면 위치</th></tr></thead><tbody>';
|
|
ev.forEach(function (e) { var q = EVST[e.st]; html += '<tr><td style="white-space:nowrap"><b>' + e.id + '</b> ' + e.name + '</td><td>' + (e.st === 'x' ? '—' : badge(q[0], q[1], q[2])) + '</td><td style="max-width:36rem">' + esc(e.text) + '</td><td>' + esc(e.pos) + '</td></tr>'; });
|
|
html += '</tbody></table></div><div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>일치는 후보를 만든 규칙과 값이 같다는 뜻이다. 도면이 맞다는 판정이 아니다.</div></div>';
|
|
if (r.st === 'b' || r.st === 'w') {
|
|
html += '<section class="card"><h3>값 비교</h3><table class="t"><caption class="cap">다른 항목만 표시</caption><thead><tr><th scope="col">항목</th><th class="num" scope="col">현재</th><th class="num" scope="col">기준</th><th scope="col">차이</th></tr></thead><tbody>' +
|
|
(r.st === 'b' ? '<tr><td>스터럽 곳 수 합(개)</td><td class="num">104</td><td class="num">106</td><td>' + badge('warn', '▲', '다름') + '</td></tr>' : '<tr><td>표시된 항목</td><td class="num">1.1</td><td class="num">0</td><td>' + badge('warn', '▲', '다름') + '</td></tr>') + '</tbody></table></section>';
|
|
}
|
|
if (!r.human) {
|
|
html += '<section class="card"><h3>확정</h3><div class="field"><label for="by">확정자<span class="req">필수</span></label><input id="by" type="text" value="' + esc(S.by) + '" style="max-width:20rem"></div>' +
|
|
'<div class="field" id="whyf"><label for="why">사유<span class="req">' + (r.st === 'g' ? '선택' : '주의·불일치·근거 없음 확정에는 필수') + '</span></label><textarea id="why" rows="3"></textarea><span class="err" id="err" hidden></span></div>' +
|
|
'<div class="row"><button class="btn primary" type="button" id="conf">확정</button>' + (next ? '<a class="btn" href="#/evidence/' + next.i + '">다음 미확정 묶음</a>' : '') + '</div></section>';
|
|
}
|
|
html += '</div></div>';
|
|
main.innerHTML = html;
|
|
var cb = $('#conf');
|
|
if (cb) cb.onclick = function () {
|
|
var by = $('#by').value.trim(), why = $('#why').value.trim(), err = $('#err'), f = $('#whyf');
|
|
function fail(m) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); f.classList.add('bad'); }
|
|
f.classList.remove('bad'); err.hidden = true;
|
|
if (!by) { fail('확정자 이름이 비어 있다. 확정은 사람의 기록이다. 이름을 입력한다'); $('#by').focus(); return; }
|
|
if (r.st !== 'g' && !why) { fail('사유가 비어 있다. ' + ST[r.st][2] + ' 묶음은 사유가 있어야 확정된다. 사유를 입력한다'); $('#why').focus(); return; }
|
|
S.by = by; r.human = { by: by, at: '14:41', why: why };
|
|
toast(r.key + ' 확정을 기록했다. 확정자 ' + by + '.'); viewEvidence(idx);
|
|
};
|
|
}
|
|
|
|
function viewCandidates() {
|
|
setWhere('S07 · 옥성교P1 · 표 후보값');
|
|
var diffN = CAND.filter(function (c) { return c.cur !== c.cand; }).length;
|
|
var html = '<div class="head"><span class="eyebrow">2단계 · 입력 묶음</span><h1>표 후보값·표 밖 치수</h1><p class="lead">도면 표와 표 밖 치수에서 찾은 후보를 현재 값과 비교한다. 반영은 선택한 행에만 적용되고 자동 반영은 없다.</p></div>';
|
|
html += '<section class="strip"><p class="concl">후보 ' + CAND.length + '건 — 다름 ' + diffN + '건, 근거 없음 3건</p><div class="counts"><span class="b">표 2</span><span class="b">표 밖 치수 6</span><span class="b dash">등급 C 1</span></div></section>';
|
|
html += '<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>params와 expected에 같은 후보를 함께 반영하면 검사의 독립성이 사라진다. 반영 대상 줄은 하나씩 고른다.</div></div>';
|
|
html += '<div class="tw"><table class="t"><caption>후보 비교 — 선택 ' + Object.keys(S.sel).filter(function (k) { return S.sel[k]; }).length + '건</caption><thead><tr><th scope="col">반영</th><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><th scope="col">신뢰</th><th scope="col">도면 위치</th></tr></thead><tbody>';
|
|
CAND.forEach(function (c) {
|
|
var d = c.cur !== c.cand;
|
|
html += '<tr' + (S.sel[c.id] ? ' class="sel"' : '') + '><td><input type="checkbox" data-c="' + c.id + '"' + (S.sel[c.id] ? ' checked' : '') + ' aria-label="' + esc(c.target) + ' 반영" style="width:1.25rem;height:1.25rem;accent-color:var(--prism-text-heading)"></td><td><b>' + esc(c.target) + '</b></td><td>' + c.file + '</td><td class="num">' + fmt(c.cur) + '</td><td class="num">' + fmt(c.cand) + '</td><td>' + c.kind + '</td><td>' + c.rule + '</td><td>' +
|
|
(d ? badge('warn', '▲', '다름') : '같음') + '</td><td>' + badge(TRUST[c.trust][0], c.trust === 'B' ? '▲' : '', TRUST[c.trust][1]) + '</td><td><span class="chip">' + esc(c.pos) + '</span></td></tr>';
|
|
});
|
|
html += '</tbody></table></div><div class="field" id="cwhy" hidden><label for="cw">반영 사유<span class="req">등급 C 반영에는 필수</span></label><input id="cw" type="text" style="max-width:36rem"></div><span class="err" id="cerr" hidden></span>';
|
|
html += '<div class="row"><button class="btn primary" type="button" id="apply">후보 반영</button></div>';
|
|
main.innerHTML = html;
|
|
function needWhy() { return CAND.some(function (c) { return S.sel[c.id] && c.trust === 'C'; }); }
|
|
$('#cwhy').hidden = !needWhy();
|
|
main.querySelectorAll('[data-c]').forEach(function (cb) { cb.onchange = function () { S.sel[cb.getAttribute('data-c')] = cb.checked; var y = window.scrollY; viewCandidates(); window.scrollTo(0, y); $('[data-c="' + cb.getAttribute('data-c') + '"]').focus(); }; });
|
|
$('#apply').onclick = function () {
|
|
var picks = CAND.filter(function (c) { return S.sel[c.id]; }), err = $('#cerr');
|
|
function fail(m) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); }
|
|
err.hidden = true;
|
|
if (!picks.length) { fail('선택한 행이 없다. 반영할 후보가 없다. 행을 선택한다'); return; }
|
|
var pairs = {}, bad = null;
|
|
picks.forEach(function (c) { if (c.pair) { if (pairs[c.pair] && pairs[c.pair] !== c.file) bad = c; pairs[c.pair] = c.file; } });
|
|
if (bad) { fail('같은 후보를 params와 expected에 함께 반영할 수 없다. 두 줄의 값이 같은 근거에서 나오면 검사가 무의미해진다. 한 줄만 선택한다'); return; }
|
|
if (needWhy() && !$('#cw').value.trim()) { fail('반영 사유가 비어 있다. 등급 C 후보는 사유가 있어야 반영된다. 사유를 입력한다'); $('#cw').focus(); return; }
|
|
var touched = {};
|
|
picks.forEach(function (c) { c.cur = c.cand; touched[c.bundle] = true; });
|
|
rows.forEach(function (r) { if (touched[r.key]) { r.stale = true; if (r.human) r.staleHuman = true; r.human = null; } });
|
|
S.sel = {};
|
|
toast(picks.length + '건을 반영했다. 영향받은 묶음 ' + Object.keys(touched).length + '개의 앱 확인과 확정이 무효가 되었다.');
|
|
viewCandidates();
|
|
};
|
|
}
|
|
|
|
/* ---------- bulk confirm (S10) ---------- */
|
|
var lastFocus = null;
|
|
function closeModal() { $('#modalHost').innerHTML = ''; document.removeEventListener('keydown', trap, true); if (lastFocus) lastFocus.focus(); }
|
|
function trap(e) {
|
|
var m = $('#modalHost .modal'); if (!m) return;
|
|
if (e.key === 'Escape') { e.preventDefault(); closeModal(); return; }
|
|
if (e.key !== 'Tab') return;
|
|
var f = m.querySelectorAll('button, [href], input, select, textarea, summary, [tabindex="0"]');
|
|
if (!f.length) return;
|
|
var first = f[0], last = f[f.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
}
|
|
function openBulk() {
|
|
lastFocus = document.activeElement;
|
|
var targets = rows.filter(function (r) { return r.st === 'g' && !r.orig && !r.human && !r.stale; });
|
|
var ex = rows.filter(function (r) { return !(r.st === 'g' && !r.orig) && !r.human || r.stale; });
|
|
var read = false;
|
|
var html = '<div class="scrim"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="mt"><header><h2 id="mt">일치 묶음 확정</h2><button class="btn quiet sm" type="button" id="mx" aria-label="닫기">닫기</button></header>';
|
|
if (!targets.length) {
|
|
html += '<p>확정할 일치 묶음이 없다. 모든 일치 묶음이 확정되었거나 재확인이 필요하다.</p><div class="row end"><button class="btn" type="button" id="mc">닫기</button></div></div></div>';
|
|
$('#modalHost').innerHTML = html; $('#mx').onclick = $('#mc').onclick = closeModal; document.addEventListener('keydown', trap, true); $('#mx').focus(); return;
|
|
}
|
|
html += '<p class="concl" style="font-size:var(--prism-type-body-lg);font-weight:700;color:var(--prism-text-heading)">일치 ' + targets.length + '개를 확정한다. 주의·불일치·근거 없음·변경됨 ' + ex.length + '개는 대상이 아니다.</p>' +
|
|
'<div><h3>근거표 <span class="cap muted">끝까지 내려 확인한다</span></h3><div class="scroll" id="sc" tabindex="0" role="region" aria-label="일치 묶음 근거표"><table class="t"><caption class="cap" style="position:absolute;left:-999rem">일치 묶음 근거</caption><thead><tr><th scope="col">묶음</th><th scope="col">근거</th><th scope="col">출처</th></tr></thead><tbody>';
|
|
targets.forEach(function (r) { html += '<tr><td><b>' + esc(r.key) + '</b></td><td>' + esc(evidence(r)[1].text) + '</td><td>' + esc(r.src) + '</td></tr>'; });
|
|
html += '</tbody></table></div></div><details class="card sunken" style="padding:0 1rem"><summary style="min-height:2.75rem;display:flex;align-items:center;cursor:pointer;font-weight:700">제외된 묶음 ' + ex.length + '개 — 누르면 근거 보기로 이동</summary><table class="t" style="margin-bottom:1rem"><caption class="cap" style="position:absolute;left:-999rem">제외된 묶음</caption><thead><tr><th scope="col">묶음</th><th scope="col">사유</th></tr></thead><tbody>';
|
|
ex.forEach(function (r) { html += '<tr><td><a href="#/evidence/' + r.i + '" data-close="1">' + esc(r.key) + '</a></td><td>' + stBadge(r) + (r.orig ? ' 원본값. 새 도면 근거가 없다' : '') + '</td></tr>'; });
|
|
html += '</tbody></table></details><div class="field"><label for="mby">확정자<span class="req">필수</span></label><input id="mby" type="text" value="' + esc(S.by) + '" style="max-width:20rem"><span class="help">확정 기록에는 입력한 이름만 남는다. 앱은 확정하지 않는다</span></div>' +
|
|
'<div class="err" id="merr" hidden></div><div class="row end"><button class="btn" type="button" id="mc">취소</button><button class="btn primary" type="button" id="mok">확정</button></div></div></div>';
|
|
$('#modalHost').innerHTML = html;
|
|
var sc = $('#sc');
|
|
function chk() { if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) read = true; }
|
|
sc.addEventListener('scroll', chk); chk();
|
|
$('#mx').onclick = $('#mc').onclick = closeModal;
|
|
$('#modalHost').querySelectorAll('[data-close]').forEach(function (a) { a.onclick = function () { closeModal(); }; });
|
|
$('#mok').onclick = function () {
|
|
var err = $('#merr'), by = $('#mby').value.trim();
|
|
function fail(m, el) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); if (el) el.focus(); }
|
|
if (!read) { fail('근거표를 읽지 않았다. 일치 묶음의 근거를 읽어야 확정할 수 있다. 근거표를 끝까지 내린다', sc); return; }
|
|
if (!by) { fail('확정자 이름이 비어 있다. 확정은 사람의 기록이다. 이름을 입력한다', $('#mby')); return; }
|
|
S.by = by;
|
|
targets.forEach(function (r) { r.human = { by: by, at: '14:41', why: '' }; });
|
|
S.flash = targets.length + '개를 ' + by + '이(가) 확정했다. 확정 기록에는 입력한 이름만 남았다.';
|
|
closeModal(); route();
|
|
};
|
|
document.addEventListener('keydown', trap, true); $('#mx').focus();
|
|
}
|
|
|
|
|
|
/* ---------- S02 new pier fit (sample: Ungok P1 drawings uploaded) ---------- */
|
|
var QS = [['foundation', '기초 형식(말뚝 유무)'], ['soffit', '코핑 하면 형상'], ['bearings', '받침 개수']];
|
|
var OPTS = { foundation: ['기초판+말뚝', '기초판만', '말뚝만'], soffit: ['곡선', '직선 경사', '나팔형'], bearings: ['2', '4', '10'] };
|
|
var TPLS = [
|
|
{ n: '운곡교P1형', p: { foundation: '기초판+말뚝', soffit: '곡선', bearings: '10' } },
|
|
{ n: '옥성교P1형', p: { foundation: '기초판+말뚝', soffit: '직선 경사', bearings: '10' } },
|
|
{ n: '새재1교P1형', p: { foundation: '기초판만', soffit: '직선 경사', bearings: '10' } },
|
|
{ n: 'Ramp-A교P1형', p: { foundation: '말뚝만', soffit: '나팔형', bearings: '2' } },
|
|
{ n: '산외교울산P1형', p: { foundation: '기초판만', soffit: '직선 경사', bearings: '4' } }
|
|
];
|
|
var EST = {
|
|
foundation: ['기초판+말뚝', "'말뚝' 문자 7개: D1 CS-TEXT (복합말뚝)", '보통'],
|
|
soffit: ['곡선', "R 12000 mm: D1 CS-DIMT (8257, 16513) 'R=12.000'", '보통'],
|
|
bearings: ['10', 'ELEVATION TABLE 행 10개: D2 (26179, 9342)', '보통']
|
|
};
|
|
var N = { up: false, ans: null, tpl: '운곡교P1형', name: '', rs: '미확인', rm: '미확인', skew: '', brot: '', frot: '', err: '' };
|
|
function nMatch(t) { return QS.filter(function (q) { return t.p[q[0]] === EST[q[0]][0]; }).length; }
|
|
function nBad() {
|
|
var t = TPLS.filter(function (x) { return x.n === N.tpl; })[0], bad = [];
|
|
QS.forEach(function (q) { if (t.p[q[0]] !== N.ans[q[0]]) bad.push(q[1]); });
|
|
if (N.rs === '아니오') bad.push('철근 형상 종류');
|
|
if (N.rm === '아니오') bad.push('철근 생성 방식');
|
|
return bad;
|
|
}
|
|
function viewNew() {
|
|
setWhere('S02 · 새 교각 적합성 판별');
|
|
var html = '<div class="head"><span class="eyebrow">새 교각</span><h1>도면으로 템플릿 추천</h1><p class="lead">도면을 먼저 올리면 앱이 가장 비슷한 템플릿을 추천한다. 추천은 제안이고, 6문항은 사람이 확인한다.</p></div>';
|
|
if (!N.up) {
|
|
html += '<section class="card" style="max-width:38em"><div class="field"><label for="dw">교각 도면(DWG, 여러 장)</label><input id="dw" type="file" accept=".dwg" multiple><span class="help">시안에서는 운곡교 P1 도면 3장을 올린 것으로 본다</span></div><div class="row"><button class="btn primary" type="button" id="up">도면 읽고 추천 받기</button></div></section>';
|
|
main.innerHTML = html;
|
|
$('#up').onclick = function () { N.up = true; N.ans = {}; QS.forEach(function (q) { N.ans[q[0]] = EST[q[0]][0]; }); viewNew(); };
|
|
return;
|
|
}
|
|
var ranked = TPLS.slice().sort(function (a, b) { return nMatch(b) - nMatch(a); });
|
|
html += '<section class="strip"><p class="concl">도면 3장, 문자 2,366개를 읽었다 — 1위 ' + ranked[0].n + ' (3 / 3 일치)</p></section>';
|
|
html += '<h2>1 추천 순위</h2><div class="tw"><table class="t"><caption>추정한 3문항과 일치한 수</caption><thead><tr><th scope="col">순위</th><th scope="col">템플릿</th><th class="num" scope="col">일치</th>' + QS.map(function (q) { return '<th scope="col">' + q[1] + '</th>'; }).join('') + '</tr></thead><tbody>';
|
|
ranked.forEach(function (t, i) {
|
|
html += '<tr><td class="num">' + (i + 1) + '</td><td><b>' + t.n + '</b></td><td class="num">' + nMatch(t) + ' / 3</td>' + QS.map(function (q) {
|
|
var ok = t.p[q[0]] === EST[q[0]][0];
|
|
return '<td>' + esc(t.p[q[0]]) + ' ' + (ok ? badge('good', '●', '일치') : badge('bad', '■', '다름')) + '</td>';
|
|
}).join('') + '</tr>';
|
|
});
|
|
html += '</tbody></table></div>';
|
|
html += '<h2>2 추정 근거</h2><div class="tw"><table class="t"><thead><tr><th scope="col">문항</th><th scope="col">추정</th><th scope="col">근거(도면 위치)</th><th scope="col">신뢰</th></tr></thead><tbody>';
|
|
QS.forEach(function (q) { var e = EST[q[0]]; html += '<tr><td>' + q[1] + '</td><td><b>' + e[0] + '</b></td><td><span class="chip">' + esc(e[1]) + '</span></td><td>' + e[2] + '</td></tr>'; });
|
|
html += '<tr><td>기둥 단면·철근 형상·철근 생성 방식</td><td>' + badge('dash', '◇', '판단 불가') + '</td><td>도면 문자로 판별할 근거가 없다. 사람이 답한다</td><td>-</td></tr></tbody></table></div>';
|
|
html += '<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>코핑 하면 형상은 정면도의 큰 R 문자(10 m 이상)만 본다. 평면 모서리의 작은 R은 제외한다. 평면의 사각·직각과 각도는 문항이 아니라 아래 값으로 입력한다.</div></div>';
|
|
html += '<h2>3 6문항 확인 후 교각 만들기</h2><div style="max-width:38em;display:flex;flex-direction:column;gap:var(--prism-space-200)">';
|
|
QS.forEach(function (q) {
|
|
html += '<div class="field"><label for="a_' + q[0] + '">' + q[1] + '</label><select id="a_' + q[0] + '" data-a="' + q[0] + '">' + OPTS[q[0]].map(function (o) { return '<option' + (N.ans[q[0]] === o ? ' selected' : '') + '>' + o + '</option>'; }).join('') + '</select></div>';
|
|
});
|
|
[['rs', '철근 형상 종류가 템플릿의 재료표 형식 열과 같은가'], ['rm', '철근 생성 방식(규칙 값 / 철근별 중심선)이 같은가']].forEach(function (q) {
|
|
html += '<div class="field"><label for="' + q[0] + '">' + q[1] + '</label><select id="' + q[0] + '" data-r="' + q[0] + '">' + ['미확인', '예', '아니오'].map(function (o) { return '<option' + (N[q[0]] === o ? ' selected' : '') + '>' + o + '</option>'; }).join('') + '</select></div>';
|
|
});
|
|
html += '<fieldset class="card sunken"><legend class="lbl">각도 값 확인 <span class="muted">템플릿을 가르지 않는 값. 비워도 된다</span></legend><div class="grid3">' +
|
|
[['skew', '코핑 평면 skew (°)'], ['brot', '받침 회전 (°)'], ['frot', '기초 회전 (°)']].map(function (q) { return '<div class="field"><label for="g_' + q[0] + '">' + q[1] + '</label><input id="g_' + q[0] + '" type="text" inputmode="decimal" data-g="' + q[0] + '" value="' + esc(N[q[0]]) + '"></div>'; }).join('') + '</div></fieldset>';
|
|
var bad = nBad();
|
|
html += '<section class="strip" role="status" aria-live="polite"><h3 style="margin:0 0 .25rem">결과 판정</h3><p class="concl">' + (bad.length ? badge('bad', '■', '같은 템플릿으로 만들 수 없음') + ' 다른 문항: ' + esc(bad.join(', ')) : badge('good', '●', '이 템플릿으로 만들 수 있음')) + '</p>' +
|
|
(bad.length ? '<p class="muted" style="margin:.25rem 0 0">가장 가까운 템플릿에 생성기·검사기를 추가해야 한다. 그래도 진행하면 교각 이름 옆과 모든 화면 상단에 형식 불일치 경고가 계속 표시된다(닫을 수 없다).</p>' : '') + '</section>';
|
|
html += '<div class="field' + (N.err ? ' bad' : '') + '"><label for="nm">교각 이름<span class="req">필수</span></label><input id="nm" type="text" value="' + esc(N.name) + '" style="max-width:20rem">' + (N.err ? '<span class="err"><span aria-hidden="true">■</span>' + esc(N.err) + '</span>' : '') + '</div>';
|
|
html += '<div class="field"><label for="tp">템플릿</label><select id="tp" style="max-width:20rem">' + ranked.map(function (t, i) { return '<option value="' + t.n + '"' + (N.tpl === t.n ? ' selected' : '') + '>' + (i + 1) + '위 ' + t.n + '</option>'; }).join('') + '</select></div>';
|
|
html += '<div class="row"><button class="btn primary" type="button" id="mk">' + (bad.length ? '그래도 만들기' : '교각 만들기') + '</button></div></div>';
|
|
main.innerHTML = html;
|
|
var y = window.scrollY;
|
|
main.querySelectorAll('[data-a]').forEach(function (e) { e.onchange = function () { N.ans[e.getAttribute('data-a')] = e.value; viewNew(); window.scrollTo(0, y); $('#' + e.id).focus(); }; });
|
|
main.querySelectorAll('[data-r]').forEach(function (e) { e.onchange = function () { N[e.getAttribute('data-r')] = e.value; viewNew(); window.scrollTo(0, y); $('#' + e.id).focus(); }; });
|
|
main.querySelectorAll('[data-g]').forEach(function (e) { e.oninput = function () { N[e.getAttribute('data-g')] = e.value; }; });
|
|
$('#nm').oninput = function () { N.name = this.value; };
|
|
$('#tp').onchange = function () { N.tpl = this.value; viewNew(); window.scrollTo(0, y); $('#tp').focus(); };
|
|
$('#mk').onclick = function () {
|
|
N.name = $('#nm').value.trim();
|
|
if (!N.name) { N.err = '교각 이름이 비어 있다. 폴더를 만들 이름이 필요하다. 이름을 입력한다'; viewNew(); $('#nm').focus(); return; }
|
|
N.err = '';
|
|
toast("교각 '" + N.name + "'을(를) 만들었다. 올린 도면이 교각 폴더로 옮겨졌고 6문항 답과 각도 값이 저장되었다." + (bad.length ? ' 형식 불일치 경고가 표시된다.' : ''));
|
|
viewNew();
|
|
};
|
|
}
|
|
|
|
/* ---------- router ---------- */
|
|
function route() {
|
|
var h = location.hash || '#/bundles', m;
|
|
$('#nav-bundles').classList.toggle('cur', h.indexOf('#/bundles') === 0 || h.indexOf('#/evidence') === 0);
|
|
$('#nav-cand').style.fontWeight = h.indexOf('#/candidates') === 0 ? '700' : '500';
|
|
$('#nav-new').style.fontWeight = h.indexOf('#/new') === 0 ? '700' : '500';
|
|
if ((m = h.match(/^#\/evidence\/(\d+)/))) viewEvidence(+m[1]);
|
|
else if (h.indexOf('#/candidates') === 0) viewCandidates();
|
|
else if (h.indexOf('#/new') === 0) viewNew();
|
|
else viewBundles();
|
|
if (document.activeElement === document.body) main.focus();
|
|
}
|
|
window.addEventListener('hashchange', function () { window.scrollTo(0, 0); route(); });
|
|
route();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|