/* ─────────────────────────────────────────────────────────────
07 이력 조회
───────────────────────────────────────────────────────────── */
const HIST_TAG_IDS = ['hf-t1','hf-t2','hf-t3','hf-t4','hf-t5','hf-t6','hf-t7','hf-t8'];
// 상태 표시기 업데이트 함수
function histUpdateStatus(state, message) {
const el = document.getElementById('hist-load-status');
if (!el) return;
// 상태 클래스 제거 후 재설정
el.classList.remove('loading', 'success', 'error');
if (state) {
el.classList.add(state);
}
// 아이콘/텍스트 업데이트 - .status-dot span은 유지
const icons = { loading: '⏳', success: '✅', error: '❌' };
const icon = icons[state] || '';
// .status-dot span 찾기
const dot = el.querySelector('.status-dot');
// 텍스트 노드 업데이트 (첫 번째 텍스트 노드)
const textNode = Array.from(el.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
if (textNode) {
textNode.textContent = icon ? `${icon} ${message}` : message;
}
}
// "▼ 옵션 불러오기" 버튼 클릭 시에만 호출 — 탭 진입 시 자동 호출 없음
async function histLoad() {
const el = document.getElementById('hist-load-status');
if (!el) return;
// 로딩 상태 표시
histUpdateStatus('loading', '조회 중...');
try {
const startTime = Date.now();
const d = await api('GET', '/api/history/tags');
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const tagNames = d || [];
if (tagNames.length === 0) {
histUpdateStatus('error', '조회 데이터 없음 (0개)');
} else {
const opts = '' +
tagNames.map(t => ``).join('');
// 기존 선택값 보존
const selectedValues = HIST_TAG_IDS.map(id => document.getElementById(id).value);
// DOM 업데이트를 requestAnimationFrame으로 지연하여 레이아웃 충돌 방지
requestAnimationFrame(() => {
HIST_TAG_IDS.forEach((id, index) => {
const sel = document.getElementById(id);
sel.innerHTML = opts;
if (selectedValues[index]) sel.value = selectedValues[index];
});
});
histUpdateStatus('success', `옵션 조회 완료 (${tagNames.length}개, ${elapsed}초)`);
}
} catch (e) {
console.error('histLoad:', e);
histUpdateStatus('error', `조회 실패: ${e.message}`);
}
}
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 interval = document.getElementById('hf-interval').value || '5 minutes';
const limit = parseInt(document.getElementById('hf-limit').value) || 500;
if (!tags.length) {
histShowStatus('err', '❌', '태그를 최소 1개 선택하세요', ' 태그 선택란에서 태그를 선택해주세요.');
return;
}
// 간격이 기본값(60초)과 다른 경우 시간 간격 조회 API 사용
if (interval === '1 minute') {
// 기본 간격(1분) 이하인 경우 기존 API 사용
histShowStatus('busy', '⏳', '조회 중...', ` ${tags.length}개 태그, 제한: ${limit}행`);
try {
const d = await api('POST', '/api/history/query', {
tagNames: tags,
from: from ? new Date(from).toISOString() : null,
to: to ? new Date(to).toISOString() : null,
limit: limit
});
const rows = d.Rows || [];
const tNames = d.TagNames || [];
renderHistoryTable(rows, tNames, interval);
} catch (e) {
histShowStatus('err', '❌', '조회 실패', ` ${e.message}\n\n컨솔에서 상세 오류를 확인하세요.`);
setGlobal('err', '조회 실패');
console.error('histQuery 오류:', e);
}
} else {
// 사용자 지정 간격인 경우 시간 간격 조회 API 사용
const body = {
tagNames: tags,
from: from ? new Date(from).toISOString() : null,
to: to ? new Date(to).toISOString() : null,
interval: interval,
limit: limit
};
// 상태 표시: 조회 시작
histShowStatus('busy', '⏳', '조회 중...', ` ${tags.length}개 태그, 간격: ${interval}, 제한: ${limit}행`);
try {
const d = await api('POST', '/api/text-to-sql/query-history-interval', body);
if (!d.success) {
throw new Error(d.error || '조회 실패');
}
const rows = d.rows || [];
const tNames = d.tagNames || [];
renderHistoryTable(rows, tNames, interval, d.baseIntervalSeconds, d.queryInterval);
} catch (e) {
histShowStatus('err', '❌', '조회 실패', ` ${e.message}\n\n컨솔에서 상세 오류를 확인하세요.`);
setGlobal('err', '조회 실패');
console.error('histQuery 오류:', e);
}
}
}
/**
* 이력 조회 결과 테이블 렌더링
*/
function renderHistoryTable(rows, tNames, interval, baseIntervalSeconds, queryInterval) {
const tbl = document.getElementById('hist-table');
tbl.classList.remove('hidden');
const info = document.getElementById('hist-result-info');
info.classList.remove('hidden');
let infoText = `총 ${rows.length.toLocaleString()}행 × ${tNames.length}개 태그`;
if (queryInterval) {
infoText += ` | 집계 간격: ${queryInterval}`;
}
info.textContent = infoText;
if (!rows.length) {
histShowStatus('ok', '✅', '조회 완료 (0건)', ` 조건에 맞는 데이터가 없습니다. | 태그: ${tNames.join(', ') || '전체'}`);
tbl.innerHTML = '
조건에 맞는 이력이 없습니다.
';
setGlobal('ok', '0건');
return;
}
// 시간 간격 조회인 경우 TimeBucket 열 사용
const timeColumn = rows[0].timeBucket ? 'timeBucket' : 'RecordedAt';
tbl.innerHTML = `
| ${queryInterval ? '집계 시각' : '시각'} |
${tNames.map(t => `${esc(t.toUpperCase())} | `).join('')}
${rows.map(r => `
| ${fmtTs(r[timeColumn])} |
${tNames.map(t => {
const raw = r.Values?.[t] ?? null;
const display = raw != null ? esc(String(fmtVal(raw))) : '—';
return `${display} | `;
}).join('')}
`).join('')}
`;
let detailText = ` 태그: ${tNames.join(', ')}`;
if (queryInterval) {
detailText += ` | 집계 간격: ${queryInterval}`;
}
detailText += ` | 시각 범위: ${document.getElementById('hf-from').value || '전체'} ~ ${document.getElementById('hf-to').value || '전체'}`;
histShowStatus('ok', '✅', `조회 완료 (${rows.length}건)`, detailText);
setGlobal('ok', `${rows.length}건`);
}
/**
* 이력 조회 상태 표시 창 업데이트
* @param {string} state - 'pending' | 'busy' | 'ok' | 'err'
* @param {string} icon - 표시할 아이콘
* @param {string} text - 메인 상태 텍스트
* @param {string} detail - 상세 설명
*/
function histShowStatus(state, icon, text, detail) {
const box = document.getElementById('hist-status-box');
const iconEl = document.getElementById('hist-status-icon');
const textEl = document.getElementById('hist-status-text');
const detEl = document.getElementById('hist-status-detail');
box.classList.remove('hidden', 'pending', 'busy', 'ok', 'err');
box.classList.add(state);
iconEl.textContent = icon;
textEl.textContent = text;
detEl.textContent = detail || '';
}
function histReset() {
HIST_TAG_IDS.forEach(id => { document.getElementById(id).value = ''; });
dtClearField('from');
dtClearField('to');
document.getElementById('hf-interval').value = '1 minute';
document.getElementById('hf-limit').value = '500';
document.getElementById('hist-result-info').classList.add('hidden');
document.getElementById('hist-table').classList.add('hidden');
// 상태 표시 창 초기화
histShowStatus('pending', '⏸', '대기 중', '조회 조건을 설정하고 조회 버튼을 눌러주세요.');
}
/* ─────────────────────────────────────────────────────────────
07-2 하이퍼테이블 관리
───────────────────────────────────────────────────────────── */
/**
* 하이퍼테이블 상태 불러오기
*/
async function htLoadStatus() {
try {
const d = await api('GET', '/api/experion/hypertable/status');
// 상태 표시 창 업데이트
const box = document.getElementById('ht-status-box');
const iconEl = document.getElementById('ht-status-icon');
const textEl = document.getElementById('ht-status-text');
const detEl = document.getElementById('ht-status-detail');
box.classList.remove('hidden', 'pending', 'busy', 'ok', 'err');
// 정보 패널 업데이트
const infoPanel = document.getElementById('ht-info-panel');
document.getElementById('ht-info-table').textContent = d.tableName || '-';
document.getElementById('ht-info-records').textContent = d.recordCount != null ? d.recordCount.toLocaleString() : '-';
document.getElementById('ht-info-retention').textContent = d.hasRetentionPolicy ? '설정됨' : '미설정';
document.getElementById('ht-info-compression').textContent = d.hasCompression ? '활성화됨' : '비활성화됨';
if (d.isHypertable) {
box.classList.add('ok');
iconEl.textContent = '✅';
textEl.textContent = '하이퍼테이블 활성화됨';
detEl.textContent = d.statusMessage || 'history_table이 하이퍼테이블로 변환되었습니다.';
infoPanel.classList.remove('hidden');
} else {
box.classList.add('pending');
iconEl.textContent = '⚠️';
textEl.textContent = '일반 테이블';
detEl.textContent = d.statusMessage || 'history_table이 아직 하이퍼테이블로 변환되지 않았습니다.';
infoPanel.classList.add('hidden');
}
} catch (e) {
console.error('htLoadStatus 오류:', e);
const box = document.getElementById('ht-status-box');
const iconEl = document.getElementById('ht-status-icon');
const textEl = document.getElementById('ht-status-text');
const detEl = document.getElementById('ht-status-detail');
box.classList.remove('hidden', 'pending', 'busy', 'ok', 'err');
box.classList.add('err');
iconEl.textContent = '❌';
textEl.textContent = '상태 조회 실패';
detEl.textContent = e.message || '알 수 없는 오류';
document.getElementById('ht-info-panel').classList.add('hidden');
}
}
/**
* 하이퍼테이블 생성
*/
async function htCreate() {
const tableName = document.getElementById('ht-table-name').value || 'history_table';
const retentionEnabled = document.getElementById('ht-auto-retention').checked;
const compressionEnabled = document.getElementById('ht-auto-compression').checked;
const aggregateEnabled = document.getElementById('ht-auto-aggregate').checked;
// 생성 전 확인
const confirmMsg = retentionEnabled || compressionEnabled
? `다음 옵션으로 하이퍼테이블을 생성합니다:\n\n` +
`- 테이블명: ${tableName}\n` +
`- 보관 기간: ${retentionEnabled ? document.getElementById('ht-retention-period').value : '설정 안함'}\n` +
`- 압축: ${compressionEnabled ? '활성화 (' + document.getElementById('ht-compression-period').value + ')' : '비활성화'}\n` +
`- 연속 집계: ${aggregateEnabled ? '생성' : '생성 안함'}\n\n` +
`계속 진행하시겠습니까?`
: `기본 설정으로 하이퍼테이블을 생성합니다:\n\n` +
`- 테이블명: ${tableName}\n` +
`- 보관 기간: 설정 안함\n` +
`- 압축: 설정 안함\n\n` +
`계속 진행하시겠습니까?`;
if (!confirm(confirmMsg)) return;
try {
// 상태 표시 창을 로딩 모드로
const box = document.getElementById('ht-status-box');
const iconEl = document.getElementById('ht-status-icon');
const textEl = document.getElementById('ht-status-text');
const detEl = document.getElementById('ht-status-detail');
box.classList.remove('hidden', 'pending', 'ok', 'err');
box.classList.add('busy');
iconEl.textContent = '⏳';
textEl.textContent = '생성 중...';
detEl.textContent = '하이퍼테이블을 생성하고 있습니다. 잠시 기다려주세요.';
document.getElementById('ht-info-panel').classList.add('hidden');
const body = {
tableName: tableName,
timeColumn: 'recorded_at',
timeInterval: '1 day',
migrateData: true,
setRetentionPolicy: retentionEnabled,
retentionPeriod: retentionEnabled ? document.getElementById('ht-retention-period').value : undefined,
enableCompression: compressionEnabled,
compressionPeriod: compressionEnabled ? document.getElementById('ht-compression-period').value : undefined,
createContinuousAggregate: aggregateEnabled
};
// 불필요한 필드는 제거
Object.keys(body).forEach(key => {
if (body[key] === undefined) delete body[key];
});
await api('POST', '/api/experion/hypertable/create', body);
// 상태 표시: 성공
box.classList.remove('busy');
box.classList.add('ok');
iconEl.textContent = '✅';
textEl.textContent = '생성 완료';
detEl.textContent = '하이퍼테이블이 성공적으로 생성되었습니다. 상태 새로고침 버튼을 눌러주세요.';
// 상태 새로고침
setTimeout(() => htLoadStatus(), 500);
} catch (e) {
console.error('htCreate 오류:', e);
const box = document.getElementById('ht-status-box');
const iconEl = document.getElementById('ht-status-icon');
const textEl = document.getElementById('ht-status-text');
const detEl = document.getElementById('ht-status-detail');
box.classList.remove('busy', 'pending', 'ok', 'err');
box.classList.add('err');
iconEl.textContent = '❌';
textEl.textContent = '생성 실패';
detEl.textContent = e.message || '알 수 없는 오류';
}
}
/**
* 보관 기간 설정 토글
*/
function htToggleRetention() {
const checked = document.getElementById('ht-auto-retention').checked;
const panel = document.getElementById('ht-retention-panel');
panel.classList.toggle('ht-hidden', !checked);
}
/**
* 압축 설정 토글
*/
function htToggleCompression() {
const checked = document.getElementById('ht-auto-compression').checked;
const panel = document.getElementById('ht-compression-panel');
panel.classList.toggle('ht-hidden', !checked);
}