Files
Rinjae 57fde15597 db
2025-12-08 21:23:17 +09:00

1842 lines
64 KiB
JavaScript

/**
* DB Console JavaScript
*/
var DbConsole = (function() {
'use strict';
// Private 변수
var editor = null;
var grid = null;
var gridApi = null;
var config = {
contextPath: '',
sessionId: '',
datasources: []
};
var currentConnection = {
connected: false,
datasourceType: '',
jndiName: '',
jdbcUrl: '',
username: '',
schema: ''
};
var sqlLogs = [];
var heartbeatInterval = null;
// 상수
var DOWNLOAD_THRESHOLD = 500;
var HEARTBEAT_INTERVAL = 60000; // 1분
// ========================================
// 초기화
// ========================================
function init(options) {
config = $.extend(config, options);
initEditor();
initGrid();
initEventHandlers();
initDatasourceSelect();
initContextMenu();
restoreSettings();
// 페이지 종료 시 세션 정리
$(window).on('beforeunload', function() {
if (currentConnection.connected) {
disconnect();
}
});
}
// Monaco Editor 초기화
function initEditor() {
require.config({ paths: { 'vs': config.contextPath + '/addon/monaco' }});
require(['vs/editor/editor.main'], function() {
// 저장된 테마 불러오기
var savedTheme = loadSetting('editorTheme', 'vs-dark');
// 초기 SQL (샘플)
var initialSql = '-- DB Console\n' +
'-- 테이블 더블클릭: SELECT 문 자동 생성\n' +
'-- Ctrl+Enter: 커서 위치 SQL 실행\n' +
'-- Ctrl+Shift+Enter: 전체 SQL 실행\n' +
'-- 셀 우클릭: 복호화/복사 메뉴\n' +
'\n';
editor = monaco.editor.create(document.getElementById('sql-editor'), {
value: initialSql,
language: 'sql',
theme: savedTheme,
minimap: { enabled: false },
automaticLayout: true,
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
wordWrap: 'on',
tabSize: 4
});
// 테마 셀렉트 동기화
$('#editor-theme').val(savedTheme);
// Ctrl+Enter로 실행 (커서 위치)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function() {
execute();
});
// Ctrl+Shift+Enter로 전체 실행 (Run All)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter, function() {
executeAll();
});
// F5로 실행
editor.addCommand(monaco.KeyCode.F5, function() {
execute();
});
});
}
// AG Grid 초기화
function initGrid() {
var gridOptions = {
columnDefs: [],
rowData: [],
defaultColDef: {
sortable: true,
resizable: true,
filter: true,
minWidth: 120,
editable: true // 셀 편집 가능 (복사 편의)
},
rowSelection: {
mode: 'multiRow',
enableClickSelection: true
},
animateRows: true,
suppressCellFocus: false,
enableCellTextSelection: false, // 텍스트 선택 비활성화 (셀 선택과 충돌 방지)
ensureDomOrder: true,
onGridReady: function(params) {
gridApi = params.api;
},
// 셀 편집 시작 시 전체 선택
onCellEditingStarted: function(params) {
setTimeout(function() {
var input = document.querySelector('.ag-cell-edit-input');
if (input) {
input.select();
}
}, 10);
},
// 편집 완료 시 원래 값 유지 (읽기 전용 동작)
onCellEditingStopped: function(params) {
// 원래 값으로 복원 (수정 방지)
if (params.oldValue !== params.newValue) {
params.node.setDataValue(params.column.colId, params.oldValue);
}
}
};
var gridDiv = document.getElementById('result-grid');
grid = agGrid.createGrid(gridDiv, gridOptions);
}
// 이벤트 핸들러 초기화
function initEventHandlers() {
// 실행 버튼 (커서 위치)
$('#btn-execute').on('click', execute);
// Run All 버튼 (전체 실행)
$('#btn-execute-all').on('click', executeAll);
// COMMIT 버튼
$('#btn-commit').on('click', function() {
showCommitConfirm();
});
// ROLLBACK 버튼
$('#btn-rollback').on('click', function() {
if (confirm('변경사항을 롤백하시겠습니까?')) {
rollback();
}
});
// 연결/해제 버튼
$('#btn-connect').on('click', connect);
$('#btn-disconnect').on('click', function() {
if (confirm('연결을 해제하시겠습니까? 미커밋 트랜잭션은 롤백됩니다.')) {
disconnect();
}
});
// 데이터소스 변경 - localStorage 저장
$('#datasource-select').on('change', function() {
var idx = $(this).val();
if (idx !== '') {
var ds = config.datasources[idx];
if (ds.type === 'DIRECT' && !ds.jdbcUrl) {
// 직접 입력인 경우
$('#direct-input-area').show();
} else {
$('#direct-input-area').hide();
}
// 데이터소스 선택값 저장
saveSetting('datasource', idx);
}
});
// 결과 탭
$('.dbconsole-result-tab').on('click', function() {
var target = $(this).data('target');
$('.dbconsole-result-tab').removeClass('active');
$(this).addClass('active');
$('.dbconsole-result-content').removeClass('active');
$('#' + target).addClass('active');
});
// 내보내기 버튼
$('#btn-export-csv').on('click', function() { exportResult('csv'); });
$('#btn-export-md').on('click', function() { exportResult('md'); });
$('#btn-export-sql').on('click', function() { exportResult('sql'); });
$('#btn-export-json').on('click', function() { exportResult('json'); });
// Fetch Count 변경 - localStorage 저장
$('#fetch-count').on('change', function() {
saveSetting('fetchCount', $(this).val());
});
// 에디터 테마 변경 - localStorage 저장
$('#editor-theme').on('change', function() {
var theme = $(this).val();
if (editor) {
monaco.editor.setTheme(theme);
}
saveSetting('editorTheme', theme);
});
// 복호화 모드 체크박스 - localStorage 저장
$('#chk-decrypt-mode').on('change', function() {
var enabled = $(this).is(':checked');
toggleDecryptMode(enabled);
saveSetting('decryptMode', enabled);
});
// 리사이저
initResizer();
// 키보드 단축키 (그리드 영역)
$('#result-grid').on('keydown', function(e) {
// Ctrl+Shift+C: 선택 영역 CSV 복사
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
e.preventDefault();
copySelectedCells('csv');
}
});
}
// 데이터소스 셀렉트 초기화
function initDatasourceSelect() {
var $select = $('#datasource-select');
$select.empty();
$select.append('<option value="">-- 데이터소스 선택 --</option>');
$.each(config.datasources, function(idx, ds) {
$select.append('<option value="' + idx + '">' + ds.displayName + '</option>');
});
}
// 리사이저 초기화
function initResizer() {
var $resizer = $('.dbconsole-resizer');
var $sidebar = $('.dbconsole-sidebar');
var isResizing = false;
$resizer.on('mousedown', function(e) {
isResizing = true;
$('body').css('cursor', 'col-resize');
});
$(document).on('mousemove', function(e) {
if (!isResizing) return;
var newWidth = e.clientX;
if (newWidth >= 200 && newWidth <= 400) {
$sidebar.css('width', newWidth + 'px');
}
});
$(document).on('mouseup', function() {
if (isResizing) {
isResizing = false;
$('body').css('cursor', '');
}
});
}
// ========================================
// 연결 관리
// ========================================
function connect() {
var dsIdx = $('#datasource-select').val();
if (dsIdx === '') {
alert('데이터소스를 선택하세요.');
return;
}
var ds = config.datasources[dsIdx];
var request = {
sessionId: config.sessionId,
datasourceType: ds.type,
jndiName: ds.jndiName || '',
jdbcUrl: ds.jdbcUrl || $('#direct-jdbc-url').val(),
username: ds.username || $('#direct-username').val(),
password: $('#direct-password').val(),
driverClassName: ds.driverClassName || 'oracle.jdbc.OracleDriver'
};
// 직접 입력 검증
if (ds.type === 'DIRECT' && !ds.jdbcUrl) {
if (!request.jdbcUrl) {
alert('JDBC URL을 입력하세요.');
return;
}
if (!request.username) {
alert('사용자명을 입력하세요.');
return;
}
}
showLoading(true);
// 간단한 테스트 쿼리로 연결 확인
request.sql = 'SELECT 1 FROM DUAL';
request.maxRows = 1;
$.ajax({
url: config.contextPath + '/admin/dbconsole/execute.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
showLoading(false);
if (result.success) {
currentConnection = {
connected: true,
datasourceType: ds.type,
jndiName: ds.jndiName,
jdbcUrl: request.jdbcUrl,
username: request.username,
schema: ds.schema || request.username
};
updateConnectionStatus();
loadSchemas();
startHeartbeat();
addSqlLog('연결 성공', 'CONNECT', true, '데이터소스: ' + ds.displayName);
} else {
alert('연결 실패: ' + result.message);
addSqlLog('연결 실패', 'CONNECT', false, result.message);
}
},
error: function(xhr) {
showLoading(false);
alert('연결 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
}
});
}
function disconnect() {
if (!currentConnection.connected) return;
var request = {
sessionId: config.sessionId
};
$.ajax({
url: config.contextPath + '/admin/dbconsole/disconnect.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
currentConnection.connected = false;
updateConnectionStatus();
clearSchemaTree();
stopHeartbeat();
addSqlLog('연결 해제', 'DISCONNECT', true, '');
},
error: function() {
currentConnection.connected = false;
updateConnectionStatus();
stopHeartbeat();
}
});
}
function updateConnectionStatus() {
var $status = $('#connection-status');
var $dot = $status.find('.status-dot');
var $text = $status.find('.status-text');
if (currentConnection.connected) {
$dot.removeClass('disconnected pending').addClass('connected');
$text.text('연결됨: ' + (currentConnection.schema || currentConnection.username));
$('#btn-connect').hide();
$('#btn-disconnect').show();
$('#btn-execute, #btn-execute-all, #btn-commit, #btn-rollback').prop('disabled', false);
} else {
$dot.removeClass('connected pending').addClass('disconnected');
$text.text('연결 안됨');
$('#btn-connect').show();
$('#btn-disconnect').hide();
$('#btn-execute, #btn-execute-all, #btn-commit, #btn-rollback').prop('disabled', true);
}
}
// ========================================
// SQL 실행
// ========================================
// 커서 위치의 SQL 구문 찾기
function getSqlAtCursor() {
if (!editor) return '';
var model = editor.getModel();
var position = editor.getPosition();
var fullText = model.getValue();
// 세미콜론으로 구문 분리 (문자열 내 세미콜론 제외)
var statements = parseSqlStatements(fullText);
if (statements.length === 0) return fullText.trim();
// 커서 위치(offset) 계산
var cursorOffset = model.getOffsetAt(position);
// 커서가 속한 구문 찾기
var currentOffset = 0;
for (var i = 0; i < statements.length; i++) {
var stmt = statements[i];
var stmtStart = fullText.indexOf(stmt.text, currentOffset);
var stmtEnd = stmtStart + stmt.text.length;
if (cursorOffset >= stmtStart && cursorOffset <= stmtEnd + 1) {
return stmt.text.trim();
}
currentOffset = stmtEnd + 1; // +1 for semicolon
}
// 찾지 못하면 마지막 구문 반환
return statements[statements.length - 1].text.trim();
}
// SQL 구문 파싱 (세미콜론 기준, 문자열 내 세미콜론 무시)
function parseSqlStatements(text) {
var statements = [];
var current = '';
var inSingleQuote = false;
var inDoubleQuote = false;
var inLineComment = false;
var inBlockComment = false;
for (var i = 0; i < text.length; i++) {
var char = text[i];
var nextChar = text[i + 1] || '';
// 라인 주석 시작
if (!inSingleQuote && !inDoubleQuote && !inBlockComment && char === '-' && nextChar === '-') {
inLineComment = true;
}
// 라인 주석 끝
if (inLineComment && (char === '\n' || char === '\r')) {
inLineComment = false;
}
// 블록 주석 시작
if (!inSingleQuote && !inDoubleQuote && !inLineComment && char === '/' && nextChar === '*') {
inBlockComment = true;
}
// 블록 주석 끝
if (inBlockComment && char === '*' && nextChar === '/') {
current += char;
current += nextChar;
i++;
inBlockComment = false;
continue;
}
// 문자열 처리
if (!inLineComment && !inBlockComment) {
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
} else if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
}
}
// 세미콜론 (구문 구분자)
if (char === ';' && !inSingleQuote && !inDoubleQuote && !inLineComment && !inBlockComment) {
var trimmed = current.trim();
if (trimmed) {
statements.push({ text: trimmed });
}
current = '';
continue;
}
current += char;
}
// 마지막 구문 (세미콜론 없이 끝난 경우)
var lastTrimmed = current.trim();
if (lastTrimmed) {
statements.push({ text: lastTrimmed });
}
return statements;
}
// SQL에서 주석 제거
function removeComments(sql) {
var result = '';
var inSingleQuote = false;
var inDoubleQuote = false;
var inLineComment = false;
var inBlockComment = false;
for (var i = 0; i < sql.length; i++) {
var char = sql[i];
var nextChar = sql[i + 1] || '';
// 라인 주석 시작
if (!inSingleQuote && !inDoubleQuote && !inBlockComment && char === '-' && nextChar === '-') {
inLineComment = true;
i++; // skip next '-'
continue;
}
// 라인 주석 끝
if (inLineComment && (char === '\n' || char === '\r')) {
inLineComment = false;
result += char; // 줄바꿈은 유지
continue;
}
// 블록 주석 시작
if (!inSingleQuote && !inDoubleQuote && !inLineComment && char === '/' && nextChar === '*') {
inBlockComment = true;
i++; // skip '*'
continue;
}
// 블록 주석 끝
if (inBlockComment && char === '*' && nextChar === '/') {
inBlockComment = false;
i++; // skip '/'
continue;
}
// 주석 내부면 skip
if (inLineComment || inBlockComment) {
continue;
}
// 문자열 처리
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
} else if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
}
result += char;
}
return result.trim();
}
// 현재 커서 위치 기준 실행
function execute() {
if (!currentConnection.connected) {
alert('먼저 데이터소스에 연결하세요.');
return;
}
var sql = '';
// 선택 영역이 있으면 선택 영역 실행
if (editor) {
var selection = editor.getSelection();
if (selection && !selection.isEmpty()) {
sql = editor.getModel().getValueInRange(selection);
} else {
// 커서 위치의 구문 실행
sql = getSqlAtCursor();
}
}
// 주석 제거 후 실행할 SQL이 있는지 확인
var sqlWithoutComments = removeComments(sql);
if (!sqlWithoutComments) {
alert('실행할 SQL을 입력하세요. (주석만 있습니다)');
return;
}
executeSql(sql);
}
// 전체 SQL 실행 (Run All)
function executeAll() {
if (!currentConnection.connected) {
alert('먼저 데이터소스에 연결하세요.');
return;
}
if (!editor) return;
var fullText = editor.getModel().getValue();
var statements = parseSqlStatements(fullText);
// 주석만 있는 구문 필터링
var executableStatements = statements.filter(function(stmt) {
return removeComments(stmt.text).length > 0;
});
if (executableStatements.length === 0) {
alert('실행할 SQL을 입력하세요. (주석만 있습니다)');
return;
}
// 여러 구문 순차 실행
executeStatementsSequentially(executableStatements, 0);
}
// 구문 순차 실행
function executeStatementsSequentially(statements, index) {
if (index >= statements.length) {
return;
}
var sql = statements[index].text;
// 주석 제거 후 COMMIT/ROLLBACK 체크
var sqlWithoutComments = removeComments(sql);
var upperSql = sqlWithoutComments.toUpperCase();
// COMMIT/ROLLBACK은 특별 처리
if (upperSql === 'COMMIT') {
showCommitConfirm();
return; // COMMIT 후에는 중단 (사용자 확인 필요)
}
if (upperSql === 'ROLLBACK') {
if (confirm('변경사항을 롤백하시겠습니까?')) {
rollback();
}
return;
}
executeSql(sql, function() {
// 다음 구문 실행
executeStatementsSequentially(statements, index + 1);
});
}
// 단일 SQL 실행
function executeSql(sql, callback) {
// COMMIT/ROLLBACK 인터셉트
var upperSql = sql.toUpperCase().trim();
if (upperSql === 'COMMIT') {
showCommitConfirm();
return;
}
if (upperSql === 'ROLLBACK') {
if (confirm('변경사항을 롤백하시겠습니까?')) {
rollback();
}
return;
}
var fetchCount = $('#fetch-count').val();
var maxRows = fetchCount === 'all' ? 0 : parseInt(fetchCount);
var request = {
sessionId: config.sessionId,
sql: sql,
maxRows: maxRows,
datasourceType: currentConnection.datasourceType,
jndiName: currentConnection.jndiName,
jdbcUrl: currentConnection.jdbcUrl,
username: currentConnection.username
};
showLoading(true);
var startTime = Date.now();
$.ajax({
url: config.contextPath + '/admin/dbconsole/execute.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
showLoading(false);
var elapsed = Date.now() - startTime;
if (result.success) {
if (result.queryType === 'SELECT') {
displaySelectResult(result);
} else {
displayUpdateResult(result);
}
addSqlLog(sql, result.queryType, true, result.message + ' (' + elapsed + 'ms)');
clearEditorErrors();
} else {
displayError(result.message);
addSqlLog(sql, 'ERROR', false, result.message);
markEditorError(sql, result.message);
}
updateTransactionIndicator(result.transactionActive);
if (callback) callback();
},
error: function(xhr) {
showLoading(false);
var msg = xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText;
displayError('실행 오류: ' + msg);
addSqlLog(sql, 'ERROR', false, msg);
}
});
}
// ========================================
// COMMIT / ROLLBACK
// ========================================
function showCommitConfirm() {
$('#commit-modal').modal('show');
$('#commit-password').val('').focus();
}
function commit() {
var password = $('#commit-password').val();
if (!password) {
alert('비밀번호를 입력하세요.');
return;
}
var request = {
sessionId: config.sessionId,
password: password
};
showLoading(true);
$.ajax({
url: config.contextPath + '/admin/dbconsole/commit.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
showLoading(false);
$('#commit-modal').modal('hide');
if (result.success) {
alert('COMMIT 완료');
addSqlLog('COMMIT', 'COMMIT', true, result.message);
} else {
alert('COMMIT 실패: ' + result.message);
addSqlLog('COMMIT', 'COMMIT', false, result.message);
}
},
error: function(xhr) {
showLoading(false);
alert('COMMIT 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
}
});
}
function rollback() {
var request = {
sessionId: config.sessionId
};
showLoading(true);
$.ajax({
url: config.contextPath + '/admin/dbconsole/rollback.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
showLoading(false);
if (result.success) {
alert('ROLLBACK 완료');
addSqlLog('ROLLBACK', 'ROLLBACK', true, result.message);
} else {
alert('ROLLBACK 실패: ' + result.message);
addSqlLog('ROLLBACK', 'ROLLBACK', false, result.message);
}
},
error: function(xhr) {
showLoading(false);
alert('ROLLBACK 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
}
});
}
// ========================================
// 암호화 데이터 복호화
// ========================================
var decryptModeEnabled = false;
var contextMenuTarget = null;
// 복호화 모드 토글
function toggleDecryptMode(enabled) {
decryptModeEnabled = enabled;
// 그리드 행 다시 그리기 (cellRenderer가 다시 호출되도록)
if (gridApi) {
gridApi.redrawRows();
}
}
// 암호화된 값인지 확인 (Base64 패턴)
// 예: "FnJ/5lM72/Q7gBn7KG+piiABZLALMVQQ4YK8akyFJ5Q="
function isEncryptedValue(value) {
if (!value || typeof value !== 'string') {
return false;
}
var trimmed = value.trim();
// 최소 길이 체크 (Base64로 인코딩된 최소 16바이트 = 24자 정도)
if (trimmed.length < 20) {
return false;
}
// Base64 패턴: A-Z, a-z, 0-9, +, /, = (패딩)
// 끝에 =나 ==가 있거나, +나 /가 포함된 경우 Base64로 판단
var base64Pattern = /^[A-Za-z0-9+/]+=*$/;
if (!base64Pattern.test(trimmed)) {
return false;
}
// Base64 특수문자(+, /)가 포함되어 있거나 =로 끝나면 암호화 데이터로 판단
var hasBase64Chars = /[+/]/.test(trimmed) || /=$/.test(trimmed);
// 길이가 4의 배수인지 확인 (유효한 Base64)
var isValidLength = trimmed.length % 4 === 0;
return hasBase64Chars && isValidLength;
}
// 셀 복호화 (버튼 클릭)
function decryptCell(btn) {
var $cell = $(btn).closest('.cell-encrypted');
var encryptedValue = $cell.data('value');
var rowIndex = $cell.data('row');
var colId = $cell.data('col');
doDecrypt(encryptedValue, rowIndex, colId);
}
// 실제 복호화 수행
function doDecrypt(encryptedValue, rowIndex, colId) {
if (!encryptedValue) {
alert('복호화할 값이 없습니다.');
return;
}
$.ajax({
url: config.contextPath + '/admin/dbconsole/decrypt.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ value: encryptedValue }),
success: function(response) {
if (response.success) {
// 복호화 성공 - 값 저장소에 저장하고 그리드 새로고침
if (rowIndex !== null && rowIndex !== undefined && colId) {
var key = rowIndex + '_' + colId;
decryptedValues[key] = response.decryptedValue;
// 그리드 셀 새로고침
if (gridApi) {
gridApi.refreshCells({ force: true });
}
} else {
// row/col 정보 없으면 alert로 표시
alert('복호화 결과:\n' + response.decryptedValue);
}
} else {
alert('복호화 실패: ' + (response.message || '알 수 없는 오류'));
}
},
error: function(xhr) {
alert('복호화 요청 오류: ' + xhr.statusText);
}
});
}
// 컨텍스트 메뉴 초기화
function initContextMenu() {
// 컨텍스트 메뉴 HTML (셀 우클릭용)
var menuHtml = '<div id="dbconsole-context-menu" class="dbconsole-context-menu">' +
'<div class="context-menu-item" data-action="decrypt"><i class="bi bi-unlock"></i> 이 셀 복호화</div>' +
'<div class="context-menu-item" data-action="copy"><i class="bi bi-clipboard"></i> 이 셀 복사</div>' +
'<div class="context-menu-divider"></div>' +
'<div class="context-menu-item" data-action="copy-selection-csv"><i class="bi bi-file-earmark-spreadsheet"></i> 선택 행 복사 (CSV)</div>' +
'<div class="context-menu-item" data-action="copy-selection-md"><i class="bi bi-markdown"></i> 선택 행 복사 (MD)</div>' +
'<div class="context-menu-item" data-action="copy-selection-sql"><i class="bi bi-file-code"></i> 선택 행 복사 (SQL)</div>' +
'<div class="context-menu-item" data-action="copy-selection-json"><i class="bi bi-filetype-json"></i> 선택 행 복사 (JSON)</div>' +
'</div>';
$('body').append(menuHtml);
var $menu = $('#dbconsole-context-menu');
// 그리드 셀 우클릭
$('#result-grid').on('contextmenu', '.ag-cell', function(e) {
e.preventDefault();
var cellValue = $(this).text().trim();
// (NULL) 표시 제거
if (cellValue === '(NULL)') cellValue = '';
// row/col 정보 추출
var rowIndex = null;
var colId = null;
// data-value 속성이 있으면 (암호화된 값) 그걸 사용
var $encrypted = $(this).find('.cell-encrypted');
if ($encrypted.length) {
cellValue = $encrypted.data('value') || cellValue;
rowIndex = $encrypted.data('row');
colId = $encrypted.data('col');
}
// row/col 정보가 없으면 AG Grid API에서 추출 시도
if (rowIndex === null || rowIndex === undefined) {
var $row = $(this).closest('.ag-row');
if ($row.length) {
var rowIdAttr = $row.attr('row-index');
if (rowIdAttr !== undefined) {
rowIndex = parseInt(rowIdAttr, 10);
}
}
// colId 추출
var colIdAttr = $(this).attr('col-id');
if (colIdAttr) {
colId = colIdAttr;
}
}
contextMenuTarget = {
element: $(this),
value: cellValue,
rowIndex: rowIndex,
colId: colId
};
$menu.css({
top: e.pageY + 'px',
left: e.pageX + 'px'
}).show();
});
// 메뉴 아이템 클릭
$menu.on('click', '.context-menu-item', function() {
var action = $(this).data('action');
if (action === 'decrypt' && contextMenuTarget) {
// 셀에 바로 복호화 결과 표시
doDecrypt(contextMenuTarget.value, contextMenuTarget.rowIndex, contextMenuTarget.colId);
} else if (action === 'copy' && contextMenuTarget) {
copyToClipboard(contextMenuTarget.value || '');
} else if (action.startsWith('copy-selection-')) {
// 선택 영역 복사
var format = action.replace('copy-selection-', '');
copySelectedCells(format);
}
$menu.hide();
contextMenuTarget = null;
});
// 다른 곳 클릭시 메뉴 숨김
$(document).on('click', function() {
$menu.hide();
});
}
// 클립보드 복사
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
}
// 선택된 행 복사 (다양한 형식 지원)
function copySelectedCells(format) {
if (!gridApi) {
alert('그리드가 초기화되지 않았습니다.');
return;
}
// 선택된 행 가져오기 (AG Grid Community는 행 선택만 지원)
var selectedRows = gridApi.getSelectedRows();
if (!selectedRows || selectedRows.length === 0) {
alert('복사할 행을 선택하세요.\n(클릭: 단일 선택, Ctrl+클릭: 다중 선택, Shift+클릭: 범위 선택)');
return;
}
// 선택된 데이터 추출
var selectedData = extractSelectedData(selectedRows);
if (selectedData.rows.length === 0) {
alert('복사할 데이터가 없습니다.');
return;
}
var content = '';
switch (format) {
case 'csv':
content = formatSelectionToCsv(selectedData);
break;
case 'md':
content = formatSelectionToMarkdown(selectedData);
break;
case 'sql':
content = formatSelectionToSql(selectedData);
break;
case 'json':
content = formatSelectionToJson(selectedData);
break;
default:
content = formatSelectionToCsv(selectedData);
}
copyToClipboard(content);
alert('선택된 ' + selectedData.rows.length + '개 행이 ' + format.toUpperCase() + ' 형식으로 클립보드에 복사되었습니다.');
}
// 선택된 행에서 데이터 추출
function extractSelectedData(selectedRows) {
if (!selectedRows || selectedRows.length === 0) {
return { columns: [], rows: [] };
}
// 컬럼 정보 가져오기 (행 번호 컬럼 제외)
var columnDefs = gridApi.getColumnDefs() || [];
var columns = [];
columnDefs.forEach(function(colDef) {
// 행 번호 컬럼(_rowNum) 제외
if (colDef.field && colDef.field !== '_rowNum') {
columns.push({
colId: colDef.field,
headerName: colDef.headerName || colDef.field
});
}
});
// 데이터 추출
var rows = selectedRows.map(function(rowData) {
var row = {};
columns.forEach(function(col) {
row[col.colId] = rowData[col.colId];
});
return row;
});
return {
columns: columns,
rows: rows
};
}
// 선택 데이터 -> CSV 형식
function formatSelectionToCsv(data) {
var lines = [];
// 헤더
lines.push(data.columns.map(function(c) { return c.headerName; }).join(','));
// 데이터
data.rows.forEach(function(row) {
var values = data.columns.map(function(col) {
var val = row[col.colId];
if (val === null || val === undefined) return '';
val = String(val).replace(/"/g, '""');
if (val.indexOf(',') >= 0 || val.indexOf('"') >= 0 || val.indexOf('\n') >= 0) {
val = '"' + val + '"';
}
return val;
});
lines.push(values.join(','));
});
return lines.join('\n');
}
// 선택 데이터 -> Markdown 형식
function formatSelectionToMarkdown(data) {
var lines = [];
// 헤더
lines.push('| ' + data.columns.map(function(c) { return c.headerName; }).join(' | ') + ' |');
lines.push('| ' + data.columns.map(function() { return '---'; }).join(' | ') + ' |');
// 데이터
data.rows.forEach(function(row) {
var values = data.columns.map(function(col) {
var val = row[col.colId];
if (val === null || val === undefined) return '';
return String(val).replace(/\|/g, '\\|').replace(/\n/g, ' ');
});
lines.push('| ' + values.join(' | ') + ' |');
});
return lines.join('\n');
}
// 선택 데이터 -> INSERT SQL 형식
function formatSelectionToSql(data) {
var tableName = 'TABLE_NAME';
var columnNames = data.columns.map(function(c) { return c.colId; }).join(', ');
var lines = [];
data.rows.forEach(function(row) {
var values = data.columns.map(function(col) {
var val = row[col.colId];
if (val === null || val === undefined) return 'NULL';
val = String(val);
// CLOB 처리 (4000바이트 초과 시 분할)
if (val.length > 4000) {
return splitClobValue(val);
}
return "'" + val.replace(/'/g, "''") + "'";
});
lines.push('INSERT INTO ' + tableName + ' (' + columnNames + ') VALUES (' + values.join(', ') + ');');
});
return lines.join('\n');
}
// 선택 데이터 -> JSON 형식
function formatSelectionToJson(data) {
var result = data.rows.map(function(row) {
var obj = {};
data.columns.forEach(function(col) {
obj[col.colId] = row[col.colId];
});
return obj;
});
return JSON.stringify(result, null, 2);
}
// ========================================
// 결과 표시
// ========================================
// 복호화된 값 저장소 (rowIndex_colId -> decryptedValue)
var decryptedValues = {};
function displaySelectResult(result) {
if (!gridApi) return;
// 복호화 값 저장소 초기화
decryptedValues = {};
// 행 번호 컬럼 (첫 번째)
var rowNumCol = {
headerName: '#',
field: '_rowNum',
width: 60,
minWidth: 50,
maxWidth: 80,
pinned: 'left',
editable: false,
sortable: false,
filter: false,
resizable: false,
suppressMovable: true,
cellClass: 'row-number-cell',
valueGetter: function(params) {
return params.node.rowIndex + 1;
}
};
// 데이터 컬럼 정의
var dataColumnDefs = result.columns.map(function(col, idx) {
var colMeta = result.columnMetas ? result.columnMetas[idx] : null;
return {
headerName: col,
field: col,
// valueGetter로 실제 값 반환 (복호화된 값 포함)
valueGetter: function(params) {
var key = params.node.rowIndex + '_' + col;
// 복호화된 값이 있으면 그 값 반환
if (decryptedValues[key] !== undefined) {
return decryptedValues[key];
}
return params.data[col];
},
cellRenderer: function(params) {
var key = params.node.rowIndex + '_' + col;
var originalValue = params.data[col];
var displayValue = params.value;
// NULL 체크
if (originalValue === null || originalValue === undefined) {
return '<span class="cell-null">(NULL)</span>';
}
var originalStr = String(originalValue);
var displayStr = String(displayValue);
// LOB 타입
if (colMeta && colMeta.lobType) {
return '<span class="cell-lob">' + escapeHtml(displayStr) + '</span>';
}
// 복호화된 값인 경우 (저장소에 있음)
if (decryptedValues[key] !== undefined) {
return '<span class="cell-decrypted" title="원본: ' + escapeHtml(originalStr) + '">' +
'<i class="bi bi-unlock-fill"></i> ' + escapeHtml(displayStr) + '</span>';
}
// 복호화 모드 활성화 + 암호화된 값 감지 시에만 버튼 표시 (원본값 기준)
if (decryptModeEnabled && isEncryptedValue(originalStr)) {
var shortValue = originalStr.length > 20 ? originalStr.substring(0, 20) + '...' : originalStr;
return '<span class="cell-encrypted" data-value="' + escapeHtml(originalStr) + '" data-row="' + params.node.rowIndex + '" data-col="' + col + '">' +
escapeHtml(shortValue) +
' <button class="btn-decrypt" onclick="DbConsole.decryptCell(this)" title="복호화">' +
'<i class="bi bi-unlock"></i></button></span>';
}
return escapeHtml(displayStr);
}
};
});
var columnDefs = [rowNumCol].concat(dataColumnDefs);
gridApi.updateGridOptions({
columnDefs: columnDefs,
rowData: result.rows || []
});
gridApi.sizeColumnsToFit();
// 결과 탭으로 전환
$('.dbconsole-result-tab[data-target="result-grid-container"]').click();
// 메시지 표시
$('#result-message').text(result.message || '');
}
function displayUpdateResult(result) {
// 그리드 초기화
if (gridApi) {
gridApi.updateGridOptions({
columnDefs: [],
rowData: []
});
}
// 메시지 표시
$('#result-message').text(result.message || '');
alert(result.message);
}
function displayError(message) {
$('#result-message').text('오류: ' + message);
}
function updateTransactionIndicator(active) {
var $indicator = $('#transaction-indicator');
if (active) {
$indicator.addClass('active').text('트랜잭션 활성');
} else {
$indicator.removeClass('active').text('');
}
}
// ========================================
// 스키마 탐색기
// ========================================
function loadSchemas() {
var request = {
sessionId: config.sessionId,
datasourceType: currentConnection.datasourceType,
jndiName: currentConnection.jndiName,
jdbcUrl: currentConnection.jdbcUrl,
username: currentConnection.username
};
$.ajax({
url: config.contextPath + '/admin/dbconsole/schemas.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(schemas) {
renderSchemaTree(schemas);
}
});
}
function renderSchemaTree(schemas) {
var $tree = $('#schema-tree');
$tree.empty();
// 현재 스키마를 우선 표시
var currentSchema = (currentConnection.schema || currentConnection.username || '').toUpperCase();
var sortedSchemas = schemas.sort(function(a, b) {
if (a === currentSchema) return -1;
if (b === currentSchema) return 1;
return a.localeCompare(b);
});
$.each(sortedSchemas, function(idx, schema) {
var $item = $('<div class="schema-item" data-schema="' + schema + '">' +
'<span class="tree-toggle tree-icon"><i class="bi bi-chevron-right"></i></span>' +
'<span class="tree-icon"><i class="bi bi-database"></i></span>' +
'<span class="schema-name">' + schema + '</span>' +
'</div>');
var $children = $('<div class="tree-children" data-schema="' + schema + '"></div>');
$item.on('click', function(e) {
e.stopPropagation();
var $toggle = $(this).find('.tree-toggle i');
if ($children.hasClass('expanded')) {
$children.removeClass('expanded');
$toggle.removeClass('bi-chevron-down').addClass('bi-chevron-right');
} else {
$children.addClass('expanded');
$toggle.removeClass('bi-chevron-right').addClass('bi-chevron-down');
if ($children.children().length === 0) {
loadTables(schema, $children);
}
}
});
$tree.append($item).append($children);
// 현재 스키마 자동 확장
if (schema === currentSchema) {
$item.click();
}
});
}
function loadTables(schema, $container) {
var request = {
sessionId: config.sessionId,
schemaName: schema,
datasourceType: currentConnection.datasourceType,
jndiName: currentConnection.jndiName,
jdbcUrl: currentConnection.jdbcUrl,
username: currentConnection.username
};
$container.html('<div class="text-muted small p-2">로딩 중...</div>');
$.ajax({
url: config.contextPath + '/admin/dbconsole/tables.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(tables) {
renderTables(schema, tables, $container);
},
error: function() {
$container.html('<div class="text-danger small p-2">로드 실패</div>');
}
});
}
function renderTables(schema, tables, $container) {
$container.empty();
if (tables.length === 0) {
$container.html('<div class="text-muted small p-2">테이블 없음</div>');
return;
}
$.each(tables, function(idx, table) {
var typeClass = table.tableType === 'VIEW' ? 'view' : '';
var $item = $('<div class="table-item" data-schema="' + schema + '" data-table="' + table.tableName + '">' +
'<span class="tree-toggle tree-icon"><i class="bi bi-chevron-right"></i></span>' +
'<span class="tree-icon"><i class="bi bi-table"></i></span>' +
'<span class="table-name" title="' + (table.comments || '') + '">' + table.tableName + '</span>' +
'<span class="table-type ' + typeClass + '">' + table.tableType + '</span>' +
'</div>');
var $columns = $('<div class="tree-children"></div>');
// 테이블 클릭 - 확장/축소
$item.find('.tree-toggle').on('click', function(e) {
e.stopPropagation();
var $toggle = $(this).find('i');
if ($columns.hasClass('expanded')) {
$columns.removeClass('expanded');
$toggle.removeClass('bi-chevron-down').addClass('bi-chevron-right');
} else {
$columns.addClass('expanded');
$toggle.removeClass('bi-chevron-right').addClass('bi-chevron-down');
if ($columns.children().length === 0) {
loadColumns(schema, table.tableName, $columns);
}
}
});
// 테이블 더블클릭 - SELECT 생성 (새 라인에 세미콜론 포함)
$item.on('dblclick', function(e) {
e.stopPropagation();
var sql = 'SELECT * FROM ' + schema + '.' + table.tableName + ';';
if (editor) {
var model = editor.getModel();
var lastLine = model.getLineCount();
var lastLineContent = model.getLineContent(lastLine);
// 마지막 라인이 비어있지 않으면 새 라인 추가
var prefix = lastLineContent.trim() ? '\n' : '';
editor.executeEdits('', [{
range: new monaco.Range(lastLine, model.getLineMaxColumn(lastLine), lastLine, model.getLineMaxColumn(lastLine)),
text: prefix + sql
}]);
// 커서를 새로 추가된 SQL 끝으로 이동
var newLastLine = model.getLineCount();
editor.setPosition({ lineNumber: newLastLine, column: model.getLineMaxColumn(newLastLine) });
editor.focus();
}
});
// 우클릭 - DDL 조회
$item.on('contextmenu', function(e) {
e.preventDefault();
showTableDdl(schema, table.tableName);
});
$container.append($item).append($columns);
});
}
function loadColumns(schema, tableName, $container) {
var request = {
sessionId: config.sessionId,
schemaName: schema,
tableName: tableName,
datasourceType: currentConnection.datasourceType,
jndiName: currentConnection.jndiName,
jdbcUrl: currentConnection.jdbcUrl,
username: currentConnection.username
};
$container.html('<div class="text-muted small p-2">로딩 중...</div>');
$.ajax({
url: config.contextPath + '/admin/dbconsole/columns.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(columns) {
renderColumns(columns, $container);
},
error: function() {
$container.html('<div class="text-danger small p-2">로드 실패</div>');
}
});
}
function renderColumns(columns, $container) {
$container.empty();
$.each(columns, function(idx, col) {
var badges = '';
if (col.primaryKey) badges += '<span class="column-pk" title="Primary Key">PK</span> ';
if (col.foreignKey) badges += '<span class="column-fk" title="Foreign Key">FK</span> ';
if (col.indexed && !col.primaryKey) badges += '<span class="column-idx" title="' + (col.indexName || 'Indexed') + '">IDX</span> ';
var $item = $('<div class="column-item" title="' + (col.comments || '') + '">' +
'<span class="column-name">' + col.columnName + '</span>' +
badges +
'<span class="column-type">' + col.dataType +
(col.dataLength ? '(' + col.dataLength + ')' : '') +
(col.nullable === 'N' ? ' NOT NULL' : '') +
'</span>' +
'</div>');
$container.append($item);
});
}
function clearSchemaTree() {
$('#schema-tree').empty();
}
function showTableDdl(schema, tableName) {
var request = {
sessionId: config.sessionId,
schemaName: schema,
tableName: tableName,
datasourceType: currentConnection.datasourceType,
jndiName: currentConnection.jndiName,
jdbcUrl: currentConnection.jdbcUrl,
username: currentConnection.username
};
$.ajax({
url: config.contextPath + '/admin/dbconsole/ddl.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(request),
success: function(result) {
$('#ddl-modal .modal-title').text('DDL: ' + schema + '.' + tableName);
$('#ddl-content').text(result.ddl || '-- DDL 조회 실패');
$('#ddl-modal').modal('show');
}
});
}
// ========================================
// 내보내기
// ========================================
function exportResult(format) {
if (!gridApi) return;
var rowData = [];
gridApi.forEachNode(function(node) {
rowData.push(node.data);
});
if (rowData.length === 0) {
alert('내보낼 데이터가 없습니다.');
return;
}
var columns = gridApi.getColumnDefs().map(function(col) {
return col.field;
});
var content = '';
var filename = '';
var mimeType = '';
switch (format) {
case 'csv':
content = exportToCsv(columns, rowData);
filename = 'query_result.csv';
mimeType = 'text/csv;charset=utf-8;';
break;
case 'md':
content = exportToMarkdown(columns, rowData);
filename = 'query_result.md';
mimeType = 'text/markdown;charset=utf-8;';
break;
case 'sql':
content = exportToInsertSql(columns, rowData);
filename = 'insert_statements.sql';
mimeType = 'text/plain;charset=utf-8;';
break;
case 'json':
content = JSON.stringify(rowData, null, 2);
filename = 'query_result.do';
mimeType = 'application/json;charset=utf-8;';
break;
}
if (rowData.length >= DOWNLOAD_THRESHOLD) {
downloadFile(content, filename, mimeType);
} else {
copyToClipboard(content);
alert('클립보드에 복사되었습니다. (' + rowData.length + '건)');
}
}
function exportToCsv(columns, rows) {
var lines = [];
lines.push(columns.join(','));
$.each(rows, function(idx, row) {
var values = columns.map(function(col) {
var val = row[col];
if (val === null || val === undefined) return '';
val = String(val).replace(/"/g, '""');
if (val.indexOf(',') >= 0 || val.indexOf('"') >= 0 || val.indexOf('\n') >= 0) {
val = '"' + val + '"';
}
return val;
});
lines.push(values.join(','));
});
return lines.join('\n');
}
function exportToMarkdown(columns, rows) {
var lines = [];
lines.push('| ' + columns.join(' | ') + ' |');
lines.push('| ' + columns.map(function() { return '---'; }).join(' | ') + ' |');
$.each(rows, function(idx, row) {
var values = columns.map(function(col) {
var val = row[col];
if (val === null || val === undefined) return '';
return String(val).replace(/\|/g, '\\|');
});
lines.push('| ' + values.join(' | ') + ' |');
});
return lines.join('\n');
}
function exportToInsertSql(columns, rows) {
var tableName = 'TABLE_NAME'; // 실제 테이블명은 알 수 없으므로 placeholder
var lines = [];
$.each(rows, function(idx, row) {
var values = columns.map(function(col) {
var val = row[col];
if (val === null || val === undefined) return 'NULL';
val = String(val);
// CLOB 처리 (4000바이트 초과 시 분할)
if (val.length > 4000) {
return splitClobValue(val);
}
return "'" + val.replace(/'/g, "''") + "'";
});
lines.push('INSERT INTO ' + tableName + ' (' + columns.join(', ') + ') VALUES (' + values.join(', ') + ');');
});
return lines.join('\n');
}
function splitClobValue(str) {
var chunks = [];
var maxBytes = 3900; // 안전 마진
var currentChunk = '';
var currentBytes = 0;
for (var i = 0; i < str.length; i++) {
var char = str.charAt(i);
var charBytes = getByteLength(char);
if (currentBytes + charBytes > maxBytes) {
chunks.push(currentChunk);
currentChunk = char;
currentBytes = charBytes;
} else {
currentChunk += char;
currentBytes += charBytes;
}
}
if (currentChunk) {
chunks.push(currentChunk);
}
if (chunks.length === 1) {
return "TO_CLOB('" + chunks[0].replace(/'/g, "''") + "')";
}
return chunks.map(function(chunk) {
return "TO_CLOB('" + chunk.replace(/'/g, "''") + "')";
}).join(' || ');
}
function getByteLength(char) {
var code = char.charCodeAt(0);
if (code <= 0x7f) return 1;
if (code <= 0x7ff) return 2;
if (code <= 0xffff) return 3;
return 4;
}
function downloadFile(content, filename, mimeType) {
var blob = new Blob(['\ufeff' + content], { type: mimeType }); // BOM 포함
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
}
// ========================================
// SQL Log
// ========================================
function addSqlLog(sql, type, success, result) {
var entry = {
time: new Date().toLocaleTimeString(),
sql: sql,
type: type,
success: success,
result: result
};
sqlLogs.unshift(entry);
if (sqlLogs.length > 100) {
sqlLogs.pop();
}
renderSqlLogs();
}
function renderSqlLogs() {
var $log = $('#sql-log');
$log.empty();
$.each(sqlLogs, function(idx, entry) {
var statusClass = entry.success ? 'success' : 'error';
var $entry = $('<div class="sql-log-entry ' + statusClass + '">' +
'<div class="sql-log-time">[' + entry.time + '] ' + entry.type + '</div>' +
'<div class="sql-log-sql">' + escapeHtml(entry.sql) + '</div>' +
'<div class="sql-log-result">' + escapeHtml(entry.result) + '</div>' +
'</div>');
$log.append($entry);
});
}
// ========================================
// 에디터 에러 마커
// ========================================
function markEditorError(sql, message) {
if (!editor) return;
var model = editor.getModel();
monaco.editor.setModelMarkers(model, 'sql-error', [{
startLineNumber: 1,
startColumn: 1,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
message: message,
severity: monaco.MarkerSeverity.Error
}]);
}
function clearEditorErrors() {
if (!editor) return;
monaco.editor.setModelMarkers(editor.getModel(), 'sql-error', []);
}
// ========================================
// 하트비트
// ========================================
function startHeartbeat() {
stopHeartbeat();
heartbeatInterval = setInterval(function() {
if (!currentConnection.connected) {
stopHeartbeat();
return;
}
$.ajax({
url: config.contextPath + '/admin/dbconsole/heartbeat.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ sessionId: config.sessionId }),
success: function(result) {
if (!result.success) {
currentConnection.connected = false;
updateConnectionStatus();
stopHeartbeat();
alert('세션이 만료되었습니다. 다시 연결하세요.');
}
}
});
}, HEARTBEAT_INTERVAL);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
// ========================================
// 유틸리티
// ========================================
function showLoading(show) {
if (show) {
$('.dbconsole-loading').addClass('show');
} else {
$('.dbconsole-loading').removeClass('show');
}
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// ========================================
// localStorage 설정 관리
// ========================================
var STORAGE_PREFIX = 'dbconsole_';
function saveSetting(key, value) {
try {
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
} catch (e) {
// localStorage 사용 불가 시 무시
}
}
function loadSetting(key, defaultValue) {
try {
var value = localStorage.getItem(STORAGE_PREFIX + key);
return value !== null ? JSON.parse(value) : defaultValue;
} catch (e) {
return defaultValue;
}
}
// 저장된 설정값들을 UI에 적용
function restoreSettings() {
// Fetch Count
var savedFetchCount = loadSetting('fetchCount', '1000');
$('#fetch-count').val(savedFetchCount);
// 복호화 모드
var savedDecryptMode = loadSetting('decryptMode', false);
$('#chk-decrypt-mode').prop('checked', savedDecryptMode);
decryptModeEnabled = savedDecryptMode;
// 데이터소스
var savedDatasource = loadSetting('datasource', '');
if (savedDatasource !== '' && config.datasources[savedDatasource]) {
$('#datasource-select').val(savedDatasource).trigger('change');
}
}
// ========================================
// Public API
// ========================================
return {
init: init,
execute: execute,
executeAll: executeAll,
commit: commit,
rollback: rollback,
connect: connect,
disconnect: disconnect,
exportResult: exportResult,
decryptCell: decryptCell,
toggleDecryptMode: toggleDecryptMode,
copySelectedCells: copySelectedCells
};
})();