/** * 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(''); $.each(config.datasources, function(idx, ds) { $select.append(''); }); } // 리사이저 초기화 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 = '
'; $('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 '(NULL)'; } var originalStr = String(originalValue); var displayStr = String(displayValue); // LOB 타입 if (colMeta && colMeta.lobType) { return '' + escapeHtml(displayStr) + ''; } // 복호화된 값인 경우 (저장소에 있음) if (decryptedValues[key] !== undefined) { return '' + ' ' + escapeHtml(displayStr) + ''; } // 복호화 모드 활성화 + 암호화된 값 감지 시에만 버튼 표시 (원본값 기준) if (decryptModeEnabled && isEncryptedValue(originalStr)) { var shortValue = originalStr.length > 20 ? originalStr.substring(0, 20) + '...' : originalStr; return '' + escapeHtml(shortValue) + ' '; } 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 = $('