Realtime DB 추가 및 Historical DB추가
This commit is contained in:
@@ -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