Realtime DB 추가 및 Historical DB추가
This commit is contained in:
@@ -486,6 +486,18 @@ tr:last-child td { border-bottom: none; }
|
||||
.nm-name-selects { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* ── 포인트빌더 ──────────────────────────────────────────── */
|
||||
.pb-name-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pb-name-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* ── Utility ─────────────────────────────────────────────── */
|
||||
.hidden { display: none !important; }
|
||||
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
<span class="ni">05</span>
|
||||
<span class="nl">노드맵 대시보드</span>
|
||||
</li>
|
||||
<li class="nav-item" data-tab="pb">
|
||||
<span class="ni">06</span>
|
||||
<span class="nl">포인트빌더</span>
|
||||
</li>
|
||||
<li class="nav-item" data-tab="hist">
|
||||
<span class="ni">07</span>
|
||||
<span class="nl">이력 조회</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sb-foot">
|
||||
@@ -308,9 +316,6 @@
|
||||
<div class="pane-tag">NODE MAP / MASTER</div>
|
||||
</header>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div id="nm-stat-row" class="nm-stat-row hidden"></div>
|
||||
|
||||
<!-- 필터 카드 -->
|
||||
<div class="card">
|
||||
<div class="card-cap">필터 조건</div>
|
||||
@@ -336,15 +341,16 @@
|
||||
<input id="nf-nid" class="inp" placeholder="포함 검색"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>데이터 타입</label>
|
||||
<select id="nf-dtype" class="inp">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
<label>데이터 타입 <em>(직접 입력)</em></label>
|
||||
<input id="nf-dtype" class="inp" placeholder="예: Double, Int32"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 이름 OR 조건 선택 (최대 4개) -->
|
||||
<!-- 이름 OR 조건 선택 (최대 4개) — 불러오기 버튼으로 옵션 채움 -->
|
||||
<div class="fg nm-name-row">
|
||||
<label>이름 선택 <em>(OR 조건, 최대 4개)</em></label>
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
이름 선택 <em>(OR 조건, 최대 4개)</em>
|
||||
<button class="btn-b btn-sm" onclick="nmLoadNames()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||||
</label>
|
||||
<div class="nm-name-selects">
|
||||
<select id="nf-name-1" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="nf-name-2" class="inp nm-name-sel"><option value="">— 선택 안 함 —</option></select>
|
||||
@@ -377,6 +383,156 @@
|
||||
<div id="nm-table" class="tbl-wrap hidden"></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
06 포인트빌더
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section class="pane" id="pane-pb">
|
||||
<header class="pane-hdr">
|
||||
<div>
|
||||
<h1>포인트빌더</h1>
|
||||
<p>node_map_master 에서 실시간 모니터링할 포인트를 선택해 realtime_table 을 구성합니다.</p>
|
||||
</div>
|
||||
<div class="pane-tag">REALTIME / BUILD</div>
|
||||
</header>
|
||||
|
||||
<!-- 빌더 카드 -->
|
||||
<div class="cols-2">
|
||||
<div class="card">
|
||||
<div class="card-cap">조건으로 테이블 작성</div>
|
||||
<div class="fg">
|
||||
<label>이름(name) 선택 <em>(OR 조건, 최대 8개)</em>
|
||||
<button class="btn-b btn-sm" onclick="pbLoad()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||||
</label>
|
||||
<div class="pb-name-grid" id="pb-name-grid">
|
||||
<!-- JS 에서 드롭다운 동적 생성 -->
|
||||
<select id="pb-n1" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n2" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n3" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n4" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n5" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n6" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n7" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="pb-n8" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>데이터 타입(data_type) 직접 입력 <em>(OR 조건, 최대 2개)</em></label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<input id="pb-dt1" class="inp" placeholder="예: Double"/>
|
||||
<input id="pb-dt2" class="inp" placeholder="예: Int32"/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-a" onclick="pbBuild()">🔨 테이블 작성하기</button>
|
||||
<div id="pb-build-log" class="logbox hidden" style="margin-top:10px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-cap">수동 포인트 추가</div>
|
||||
<div class="fg">
|
||||
<label>Node ID 직접 입력</label>
|
||||
<input id="pb-manual-nid" class="inp" placeholder="ns=2;s=Honeywell.Experion..."/>
|
||||
</div>
|
||||
<button class="btn-b" onclick="pbAddManual()">+ 추가</button>
|
||||
<div id="pb-manual-log" class="logbox hidden" style="margin-top:10px"></div>
|
||||
|
||||
<div class="card-cap" style="margin-top:20px">실시간 구독 제어</div>
|
||||
<div class="cols-2" style="gap:8px;margin-bottom:10px">
|
||||
<div class="fg">
|
||||
<label>서버 IP</label>
|
||||
<input id="pb-rt-ip" class="inp" value="192.168.0.20"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>포트</label>
|
||||
<input id="pb-rt-port" class="inp" type="number" value="4840"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>클라이언트 호스트</label>
|
||||
<input id="pb-rt-client" class="inp" value="dbsvr"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>계정</label>
|
||||
<input id="pb-rt-user" class="inp" value="mngr"/>
|
||||
</div>
|
||||
<div class="fg" style="grid-column:1/-1">
|
||||
<label>비밀번호</label>
|
||||
<input id="pb-rt-pw" class="inp" type="password" value="mngr"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn-a" onclick="rtStart()">▶ 구독 시작</button>
|
||||
<button class="btn-b" onclick="rtStop()">■ 구독 중지</button>
|
||||
<button class="btn-b btn-sm" onclick="rtStatus()">상태 확인</button>
|
||||
</div>
|
||||
<div id="pb-rt-status" class="logbox hidden" style="margin-top:8px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 포인트 목록 -->
|
||||
<div class="card" style="margin-top:0">
|
||||
<div class="card-cap" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>포인트 목록 <span id="pb-count" class="mut">(0개)</span></span>
|
||||
<button class="btn-b btn-sm" onclick="pbRefresh()">↻ 새로 고침</button>
|
||||
</div>
|
||||
<div id="pb-table" class="tbl-wrap">
|
||||
<div style="padding:20px;color:var(--t2)">포인트가 없습니다. 위에서 테이블을 작성하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
07 이력 조회
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section class="pane" id="pane-hist">
|
||||
<header class="pane-hdr">
|
||||
<div>
|
||||
<h1>이력 조회</h1>
|
||||
<p>history_table 의 시계열 데이터를 조회합니다.</p>
|
||||
</div>
|
||||
<div class="pane-tag">HISTORY / TREND</div>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-cap">조회 조건</div>
|
||||
<div class="fg">
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
태그 선택 <em>(최대 8개, OR 조건)</em>
|
||||
<button class="btn-b btn-sm" onclick="histLoad()" style="margin-left:4px">▼ 옵션 불러오기</button>
|
||||
</label>
|
||||
<div class="pb-name-grid">
|
||||
<select id="hf-t1" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t2" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t3" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t4" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t5" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t6" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t7" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
<select id="hf-t8" class="inp"><option value="">— 선택 안 함 —</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cols-3">
|
||||
<div class="fg">
|
||||
<label>시작 시간</label>
|
||||
<input id="hf-from" class="inp" type="datetime-local"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>종료 시간</label>
|
||||
<input id="hf-to" class="inp" type="datetime-local"/>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>최대 행 수</label>
|
||||
<input id="hf-limit" class="inp" type="number" value="500" min="10" max="5000"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn-a" onclick="histQuery()">🔍 조회</button>
|
||||
<button class="btn-b" onclick="histReset()">초기화</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hist-result-info" class="nm-result-info hidden" style="margin:8px 0"></div>
|
||||
<div id="hist-table" class="tbl-wrap hidden"></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ document.querySelectorAll('.nav-item').forEach(item => {
|
||||
document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
document.getElementById(`pane-${tab}`).classList.add('active');
|
||||
if (tab === 'nm-dash') nmLoad();
|
||||
// nm-dash: 탭 진입 시 API 호출 없음 — 조회 버튼으로만 동작
|
||||
// pb: 탭 진입 시 API 호출 없음 — ▼ 옵션 불러오기 버튼으로만 동작
|
||||
// hist: 탭 진입 시 API 호출 없음
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,30 +414,10 @@ let _nmOffset = 0;
|
||||
let _nmTotal = 0;
|
||||
let _nmLimit = 100;
|
||||
|
||||
async function nmLoad() {
|
||||
// 이름 드롭다운 옵션만 필요할 때 수동으로 불러오는 함수 (버튼 클릭 시)
|
||||
async function nmLoadNames() {
|
||||
try {
|
||||
const [s, n] = await Promise.all([
|
||||
api('GET', '/api/nodemap/stats'),
|
||||
api('GET', '/api/nodemap/names')
|
||||
]);
|
||||
|
||||
const row = document.getElementById('nm-stat-row');
|
||||
row.classList.remove('hidden');
|
||||
row.innerHTML = `
|
||||
<div class="stat"><div class="sv">${s.total.toLocaleString()}</div><div class="sk">전체 노드</div></div>
|
||||
<div class="stat"><div class="sv" style="color:var(--a)">${s.objectCount.toLocaleString()}</div><div class="sk">Object</div></div>
|
||||
<div class="stat"><div class="sv" style="color:var(--grn)">${s.variableCount.toLocaleString()}</div><div class="sk">Variable</div></div>
|
||||
<div class="stat"><div class="sv" style="color:var(--blu)">${s.maxLevel}</div><div class="sk">최대 깊이</div></div>
|
||||
<div class="stat"><div class="sv">${(s.dataTypes||[]).length}</div><div class="sk">데이터 타입 종류</div></div>
|
||||
`;
|
||||
|
||||
// 데이터타입 드롭다운 채우기
|
||||
const sel = document.getElementById('nf-dtype');
|
||||
const curDtype = sel.value;
|
||||
sel.innerHTML = '<option value="">전체</option>' +
|
||||
(s.dataTypes||[]).map(t => `<option value="${esc(t)}"${t===curDtype?' selected':''}>${esc(t)}</option>`).join('');
|
||||
|
||||
// 이름 드롭다운 4개 채우기
|
||||
const n = await api('GET', '/api/nodemap/names');
|
||||
const nameOpts = '<option value="">— 선택 안 함 —</option>' +
|
||||
(n.names||[]).map(nm => `<option value="${esc(nm)}">${esc(nm)}</option>`).join('');
|
||||
['nf-name-1','nf-name-2','nf-name-3','nf-name-4'].forEach(id => {
|
||||
@@ -444,9 +426,7 @@ async function nmLoad() {
|
||||
el.innerHTML = nameOpts;
|
||||
if (cur) el.value = cur;
|
||||
});
|
||||
|
||||
if (s.total > 0) nmQuery(0);
|
||||
} catch (e) { console.error('nmLoad:', e); }
|
||||
} catch (e) { console.error('nmLoadNames:', e); }
|
||||
}
|
||||
|
||||
async function nmQuery(offset) {
|
||||
@@ -458,7 +438,7 @@ async function nmQuery(offset) {
|
||||
const lvMax = document.getElementById('nf-lv-max').value.trim();
|
||||
const cls = document.getElementById('nf-class').value;
|
||||
const nid = document.getElementById('nf-nid').value.trim();
|
||||
const dtype = document.getElementById('nf-dtype').value;
|
||||
const dtype = document.getElementById('nf-dtype').value.trim();
|
||||
const selNames = ['nf-name-1','nf-name-2','nf-name-3','nf-name-4']
|
||||
.map(id => document.getElementById(id).value)
|
||||
.filter(Boolean);
|
||||
@@ -543,7 +523,236 @@ function nmReset() {
|
||||
['nf-name-1','nf-name-2','nf-name-3','nf-name-4'].forEach(id => {
|
||||
document.getElementById(id).value = '';
|
||||
});
|
||||
nmQuery(0);
|
||||
// 초기화 후 자동 조회 없음
|
||||
document.getElementById('nm-result-bar').classList.add('hidden');
|
||||
document.getElementById('nm-table').classList.add('hidden');
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
06 포인트빌더
|
||||
───────────────────────────────────────────────────────────── */
|
||||
const PB_NAME_IDS = ['pb-n1','pb-n2','pb-n3','pb-n4','pb-n5','pb-n6','pb-n7','pb-n8'];
|
||||
const PB_DT_IDS = ['pb-dt1','pb-dt2'];
|
||||
|
||||
async function pbLoad() {
|
||||
try {
|
||||
const n = await api('GET', '/api/nodemap/names');
|
||||
const nameOpts = '<option value="">— 선택 안 함 —</option>' +
|
||||
(n.names||[]).map(nm => `<option value="${esc(nm)}">${esc(nm)}</option>`).join('');
|
||||
PB_NAME_IDS.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const cur = el.value;
|
||||
el.innerHTML = nameOpts;
|
||||
if (cur) el.value = cur;
|
||||
});
|
||||
// 데이터타입은 text input으로 직접 입력 — stats API 호출 없음
|
||||
// 탭 진입 시 자동 조회 없음 — ↻ 새로 고침 버튼을 눌러야만 목록 표시됨
|
||||
} catch (e) { console.error('pbLoad:', e); }
|
||||
}
|
||||
|
||||
async function pbRefresh() {
|
||||
try {
|
||||
const d = await api('GET', '/api/pointbuilder/points');
|
||||
document.getElementById('pb-count').textContent = `(${d.count}개)`;
|
||||
pbRender(d.points || []);
|
||||
rtStatus();
|
||||
} catch (e) { console.error('pbRefresh:', e); }
|
||||
}
|
||||
|
||||
function pbRender(points) {
|
||||
const tbl = document.getElementById('pb-table');
|
||||
if (!points.length) {
|
||||
tbl.innerHTML = '<div style="padding:20px;color:var(--t2)">포인트가 없습니다. 위에서 테이블을 작성하세요.</div>';
|
||||
return;
|
||||
}
|
||||
tbl.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>TagName</th><th>Node ID</th>
|
||||
<th>LiveValue</th><th>Timestamp</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${points.map(p => `
|
||||
<tr>
|
||||
<td class="mut">${p.id}</td>
|
||||
<td style="font-weight:600">${esc(p.tagName)}</td>
|
||||
<td class="mut" style="font-size:11px;font-family:var(--fm)">${esc(p.nodeId)}</td>
|
||||
<td class="val">${p.liveValue != null ? esc(p.liveValue) : '<span style="color:var(--t3)">—</span>'}</td>
|
||||
<td class="mut" style="font-size:11px">${p.liveValue != null ? new Date(p.timestamp).toLocaleString('ko-KR') : '—'}</td>
|
||||
<td><button class="btn-sm btn-b" style="color:var(--red,#e55)" onclick="pbDelete(${p.id})">✕</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
async function pbBuild() {
|
||||
const names = PB_NAME_IDS.map(id => document.getElementById(id).value).filter(Boolean);
|
||||
const dataTypes = PB_DT_IDS.map(id => document.getElementById(id).value).filter(Boolean);
|
||||
|
||||
setGlobal('busy', '포인트 빌드 중');
|
||||
try {
|
||||
const d = await api('POST', '/api/pointbuilder/build', { names, dataTypes });
|
||||
log('pb-build-log', [{ c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + d.message }]);
|
||||
setGlobal(d.success ? 'ok' : 'err', d.success ? `${d.count}개 포인트` : '빌드 실패');
|
||||
if (d.success) await pbRefresh();
|
||||
} catch (e) {
|
||||
log('pb-build-log', [{ c: 'err', t: '❌ ' + e.message }]);
|
||||
setGlobal('err', '오류');
|
||||
}
|
||||
}
|
||||
|
||||
async function pbAddManual() {
|
||||
const nodeId = document.getElementById('pb-manual-nid').value.trim();
|
||||
if (!nodeId) { alert('Node ID를 입력하세요.'); return; }
|
||||
try {
|
||||
const d = await api('POST', '/api/pointbuilder/add', { nodeId });
|
||||
log('pb-manual-log', [{ c: d.success ? 'ok' : 'err', t: d.success
|
||||
? `✅ 추가됨: ${d.point?.tagName} (${d.point?.nodeId})`
|
||||
: '❌ 추가 실패' }]);
|
||||
if (d.success) {
|
||||
document.getElementById('pb-manual-nid').value = '';
|
||||
await pbRefresh();
|
||||
}
|
||||
} catch (e) {
|
||||
log('pb-manual-log', [{ c: 'err', t: '❌ ' + e.message }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function pbDelete(id) {
|
||||
if (!confirm(`포인트 #${id}를 삭제하시겠습니까?`)) return;
|
||||
try {
|
||||
const d = await api('DELETE', `/api/pointbuilder/${id}`);
|
||||
if (d.success) await pbRefresh();
|
||||
else alert('삭제 실패: ' + d.message);
|
||||
} catch (e) { alert('삭제 오류: ' + e.message); }
|
||||
}
|
||||
|
||||
/* ── 실시간 구독 제어 ────────────────────────────────────────── */
|
||||
async function rtStart() {
|
||||
const body = {
|
||||
serverHostName: document.getElementById('pb-rt-ip').value.trim(),
|
||||
port: parseInt(document.getElementById('pb-rt-port').value) || 4840,
|
||||
clientHostName: document.getElementById('pb-rt-client').value.trim(),
|
||||
userName: document.getElementById('pb-rt-user').value.trim(),
|
||||
password: document.getElementById('pb-rt-pw').value
|
||||
};
|
||||
setGlobal('busy', '구독 시작 중');
|
||||
try {
|
||||
const d = await api('POST', '/api/realtime/start', body);
|
||||
log('pb-rt-status', [{ c: 'ok', t: '▶ ' + d.message }]);
|
||||
setGlobal('ok', '구독 중');
|
||||
} catch (e) {
|
||||
log('pb-rt-status', [{ c: 'err', t: '❌ ' + e.message }]);
|
||||
setGlobal('err', '오류');
|
||||
}
|
||||
}
|
||||
|
||||
async function rtStop() {
|
||||
setGlobal('busy', '구독 중지 중');
|
||||
try {
|
||||
const d = await api('POST', '/api/realtime/stop');
|
||||
log('pb-rt-status', [{ c: 'inf', t: '■ ' + d.message }]);
|
||||
setGlobal('ok', '중지됨');
|
||||
} catch (e) { log('pb-rt-status', [{ c: 'err', t: '❌ ' + e.message }]); }
|
||||
}
|
||||
|
||||
async function rtStatus() {
|
||||
try {
|
||||
const d = await api('GET', '/api/realtime/status');
|
||||
const logEl = document.getElementById('pb-rt-status');
|
||||
logEl.classList.remove('hidden');
|
||||
logEl.innerHTML = `<div class="ll ${d.running ? 'ok' : 'inf'}">
|
||||
${d.running ? '▶' : '■'} ${esc(d.message)} (구독 ${d.subscribedCount}개)
|
||||
</div>`;
|
||||
} catch (e) { /* 무시 */ }
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
07 이력 조회
|
||||
───────────────────────────────────────────────────────────── */
|
||||
const HIST_TAG_IDS = ['hf-t1','hf-t2','hf-t3','hf-t4','hf-t5','hf-t6','hf-t7','hf-t8'];
|
||||
|
||||
// "▼ 옵션 불러오기" 버튼 클릭 시에만 호출 — 탭 진입 시 자동 호출 없음
|
||||
async function histLoad() {
|
||||
try {
|
||||
const d = await api('GET', '/api/history/tagnames');
|
||||
const opts = '<option value="">— 선택 안 함 —</option>' +
|
||||
(d.tagNames||[]).map(t => `<option value="${esc(t)}">${esc(t)}</option>`).join('');
|
||||
HIST_TAG_IDS.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const cur = el.value;
|
||||
el.innerHTML = opts;
|
||||
if (cur) el.value = cur;
|
||||
});
|
||||
} catch (e) { console.error('histLoad:', e); }
|
||||
}
|
||||
|
||||
async function histQuery() {
|
||||
const tags = HIST_TAG_IDS.map(id => document.getElementById(id).value).filter(Boolean);
|
||||
const from = document.getElementById('hf-from').value;
|
||||
const to = document.getElementById('hf-to').value;
|
||||
const limit = parseInt(document.getElementById('hf-limit').value) || 500;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
tags.forEach(t => params.append('tagNames', t));
|
||||
if (from) params.set('from', new Date(from).toISOString());
|
||||
if (to) params.set('to', new Date(to).toISOString());
|
||||
params.set('limit', limit);
|
||||
|
||||
setGlobal('busy', '이력 조회 중');
|
||||
try {
|
||||
const d = await api('GET', `/api/history/query?${params}`);
|
||||
const rows = d.rows || [];
|
||||
const tNames = d.tagNames || [];
|
||||
|
||||
const info = document.getElementById('hist-result-info');
|
||||
info.classList.remove('hidden');
|
||||
info.textContent = `총 ${rows.length.toLocaleString()}행 × ${tNames.length}개 태그`;
|
||||
|
||||
const tbl = document.getElementById('hist-table');
|
||||
tbl.classList.remove('hidden');
|
||||
if (!rows.length) {
|
||||
tbl.innerHTML = '<div style="padding:20px;color:var(--t2)">조건에 맞는 이력이 없습니다.</div>';
|
||||
setGlobal('ok', '0건');
|
||||
return;
|
||||
}
|
||||
|
||||
tbl.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시각</th>
|
||||
${tNames.map(t => `<th>${esc(t)}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
<tr>
|
||||
<td class="mut" style="white-space:nowrap">${new Date(r.recordedAt).toLocaleString('ko-KR')}</td>
|
||||
${tNames.map(t => `<td class="val">${r.values?.[t] != null ? esc(r.values[t]) : '<span style="color:var(--t3)">—</span>'}</td>`).join('')}
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
setGlobal('ok', `${rows.length}건`);
|
||||
} catch (e) {
|
||||
setGlobal('err', '조회 실패');
|
||||
console.error('histQuery:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function histReset() {
|
||||
HIST_TAG_IDS.forEach(id => { document.getElementById(id).value = ''; });
|
||||
document.getElementById('hf-from').value = '';
|
||||
document.getElementById('hf-to').value = '';
|
||||
document.getElementById('hf-limit').value = '500';
|
||||
document.getElementById('hist-result-info').classList.add('hidden');
|
||||
document.getElementById('hist-table').classList.add('hidden');
|
||||
}
|
||||
|
||||
/* ── 초기 실행 ───────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user