1842 lines
64 KiB
JavaScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// ========================================
|
|
// 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
|
|
};
|
|
})();
|