/* ───────────────────────────────────────────────────────────── 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 = ` ${tNames.map(t => ``).join('')} ${rows.map(r => ` ${tNames.map(t => { const raw = r.Values?.[t] ?? null; const display = raw != null ? esc(String(fmtVal(raw))) : ''; return ``; }).join('')} `).join('')}
${queryInterval ? '집계 시각' : '시각'}${esc(t.toUpperCase())}
${fmtTs(r[timeColumn])}${display}
`; 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); }