메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

핸드온테이블 그리드 ORACLE DB 연결 예제

데브카페
Devcafe (토론 | 기여)님의 2025년 10월 17일 (금) 16:23 판 (→‎서버)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

기본 화면


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Handsontable 데이터 그리드</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/12.3.1/handsontable.full.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/12.3.1/handsontable.full.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            padding: 30px;
        }
        
        h1 {
            color: #333;
            margin-bottom: 10px;
            font-size: 28px;
        }
        
        .subtitle {
            color: #666;
            margin-bottom: 25px;
            font-size: 14px;
        }
        
        .controls {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            transition: all 0.3s;
            color: white;
        }
        
        .btn-add {
            background: #10b981;
        }
        
        .btn-add:hover {
            background: #059669;
            transform: translateY(-2px);
        }
        
        .btn-remove {
            background: #ef4444;
        }
        
        .btn-remove:hover {
            background: #dc2626;
            transform: translateY(-2px);
        }
        
        .btn-export {
            background: #3b82f6;
        }
        
        .btn-export:hover {
            background: #2563eb;
            transform: translateY(-2px);
        }
        
        .btn-clear {
            background: #f59e0b;
        }
        
        .btn-clear:hover {
            background: #d97706;
            transform: translateY(-2px);
        }
        
        #grid-container {
            width: 100%;
            height: 500px;
            overflow: hidden;
        }
        
        .info {
            margin-top: 20px;
            padding: 15px;
            background: #f3f4f6;
            border-radius: 8px;
            font-size: 14px;
            color: #374151;
        }
        
        .info-item {
            margin-bottom: 5px;
        }
        
        .info-item strong {
            color: #1f2937;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 Handsontable 데이터 그리드</h1>
        <p class="subtitle">실시간 데이터 편집 및 관리 시스템</p>
        
        <div class="controls">
            <button class="btn-add" onclick="addRow()">➕ 행 추가</button>
            <button class="btn-remove" onclick="removeRow()">➖ 선택 행 삭제</button>
            <button class="btn-export" onclick="exportData()">💾 CSV 내보내기</button>
            <button class="btn-clear" onclick="clearFilters()">🔄 필터 초기화</button>
        </div>
        
        <div id="grid-container"></div>
        
        <div class="info">
            <div class="info-item"><strong>🔍 기능:</strong> 셀 편집, 정렬, 필터링, 행/열 크기 조정</div>
            <div class="info-item"><strong>💡 팁:</strong> 컬럼 헤더를 클릭하여 정렬, 드롭다운에서 필터 선택 가능</div>
            <div class="info-item"><strong>⌨️ 단축키:</strong> Ctrl+C(복사), Ctrl+V(붙여넣기), Delete(삭제)</div>
        </div>
    </div>

    <script>
        // 샘플 데이터 생성
        const data = [
            { id: 1, name: '김철수', department: '개발팀', position: '팀장', salary: 5500, email: 'kim@example.com', joinDate: '2020-01-15' },
            { id: 2, name: '이영희', department: '영업팀', position: '대리', salary: 4200, email: 'lee@example.com', joinDate: '2021-03-20' },
            { id: 3, name: '박민수', department: '개발팀', position: '과장', salary: 4800, email: 'park@example.com', joinDate: '2019-07-10' },
            { id: 4, name: '최지현', department: '인사팀', position: '사원', salary: 3500, email: 'choi@example.com', joinDate: '2022-05-01' },
            { id: 5, name: '정수진', department: '마케팅팀', position: '차장', salary: 5200, email: 'jung@example.com', joinDate: '2018-11-30' },
            { id: 6, name: '강동원', department: '영업팀', position: '부장', salary: 6000, email: 'kang@example.com', joinDate: '2017-02-14' },
            { id: 7, name: '윤서영', department: '개발팀', position: '사원', salary: 3800, email: 'yoon@example.com', joinDate: '2023-01-10' },
            { id: 8, name: '임하늘', department: '마케팅팀', position: '대리', salary: 4300, email: 'lim@example.com', joinDate: '2021-08-25' },
            { id: 9, name: '송민호', department: '인사팀', position: '과장', salary: 4900, email: 'song@example.com', joinDate: '2020-06-15' },
            { id: 10, name: '한지우', department: '영업팀', position: '사원', salary: 3600, email: 'han@example.com', joinDate: '2022-09-01' }
        ];

        const container = document.getElementById('grid-container');
        
        const hot = new Handsontable(container, {
            data: data,
            colHeaders: ['ID', '이름', '부서', '직급', '급여(만원)', '이메일', '입사일'],
            columns: [
                { data: 'id', type: 'numeric', readOnly: true },
                { data: 'name', type: 'text' },
                { 
                    data: 'department', 
                    type: 'dropdown',
                    source: ['개발팀', '영업팀', '인사팀', '마케팅팀', '재무팀']
                },
                { 
                    data: 'position', 
                    type: 'dropdown',
                    source: ['사원', '대리', '과장', '차장', '부장', '팀장']
                },
                { data: 'salary', type: 'numeric', numericFormat: { pattern: '0,0' } },
                { data: 'email', type: 'text' },
                { data: 'joinDate', type: 'date', dateFormat: 'YYYY-MM-DD' }
            ],
            rowHeaders: true,
            width: '100%',
            height: 500,
            licenseKey: 'non-commercial-and-evaluation',
            dropdownMenu: true,
            filters: true,
            columnSorting: true,
            contextMenu: true,
            manualColumnResize: true,
            manualRowResize: true,
            stretchH: 'all',
            autoWrapRow: true,
            autoWrapCol: true,
            fillHandle: {
                direction: 'vertical',
                autoInsertRow: false
            },
            language: 'ko-KR'
        });

        // 행 추가 함수
        function addRow() {
            const newId = hot.countRows() + 1;
            const newRow = {
                id: newId,
                name: '',
                department: '개발팀',
                position: '사원',
                salary: 3000,
                email: '',
                joinDate: new Date().toISOString().split('T')[0]
            };
            hot.alter('insert_row_below', hot.countRows(), 1);
            hot.setDataAtRow(hot.countRows() - 1, [
                newRow.id, 
                newRow.name, 
                newRow.department, 
                newRow.position, 
                newRow.salary, 
                newRow.email, 
                newRow.joinDate
            ]);
        }

        // 선택 행 삭제 함수
        function removeRow() {
            const selected = hot.getSelected();
            if (selected && selected.length > 0) {
                const startRow = selected[0][0];
                const endRow = selected[0][2];
                const rowCount = endRow - startRow + 1;
                
                if (confirm(`선택된 ${rowCount}개의 행을 삭제하시겠습니까?`)) {
                    hot.alter('remove_row', startRow, rowCount);
                }
            } else {
                alert('삭제할 행을 선택해주세요.');
            }
        }

        // CSV 내보내기 함수
        function exportData() {
            const exportPlugin = hot.getPlugin('exportFile');
            exportPlugin.downloadFile('csv', {
                bom: true,
                columnDelimiter: ',',
                columnHeaders: true,
                exportHiddenColumns: false,
                exportHiddenRows: false,
                fileExtension: 'csv',
                filename: 'employee_data_[YYYY]-[MM]-[DD]',
                mimeType: 'text/csv',
                rowDelimiter: '\r\n',
                rowHeaders: false
            });
        }

        // 필터 초기화 함수
        function clearFilters() {
            const filtersPlugin = hot.getPlugin('filters');
            filtersPlugin.clearConditions();
            filtersPlugin.filter();
            hot.render();
        }

        // 데이터 변경 이벤트
        hot.addHook('afterChange', function(changes, source) {
            if (source !== 'loadData') {
                console.log('데이터 변경:', changes);
            }
        });
    </script>
</body>
</html>

Flask + 오라클 연결하여 데이터 조회/입력/삭제/수정


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oracle DB 연동 데이터 그리드</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/12.3.1/handsontable.full.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/12.3.1/handsontable.full.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            padding: 30px;
        }
        
        h1 {
            color: #333;
            margin-bottom: 10px;
            font-size: 28px;
        }
        
        .subtitle {
            color: #666;
            margin-bottom: 25px;
            font-size: 14px;
        }
        
        .status {
            padding: 10px 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            font-size: 14px;
            font-weight: 600;
        }
        
        .status.success {
            background: #d1fae5;
            color: #065f46;
        }
        
        .status.error {
            background: #fee2e2;
            color: #991b1b;
        }
        
        .status.info {
            background: #dbeafe;
            color: #1e40af;
        }
        
        .controls {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            transition: all 0.3s;
            color: white;
        }
        
        button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        
        .btn-load {
            background: #8b5cf6;
        }
        
        .btn-load:hover:not(:disabled) {
            background: #7c3aed;
            transform: translateY(-2px);
        }
        
        .btn-save {
            background: #10b981;
        }
        
        .btn-save:hover:not(:disabled) {
            background: #059669;
            transform: translateY(-2px);
        }
        
        .btn-add {
            background: #3b82f6;
        }
        
        .btn-add:hover:not(:disabled) {
            background: #2563eb;
            transform: translateY(-2px);
        }
        
        .btn-remove {
            background: #ef4444;
        }
        
        .btn-remove:hover:not(:disabled) {
            background: #dc2626;
            transform: translateY(-2px);
        }
        
        .btn-export {
            background: #f59e0b;
        }
        
        .btn-export:hover:not(:disabled) {
            background: #d97706;
            transform: translateY(-2px);
        }
        
        #grid-container {
            width: 100%;
            height: 500px;
            overflow: hidden;
        }
        
        .info {
            margin-top: 20px;
            padding: 15px;
            background: #f3f4f6;
            border-radius: 8px;
            font-size: 14px;
            color: #374151;
        }
        
        .info-item {
            margin-bottom: 5px;
        }
        
        .info-item strong {
            color: #1f2937;
        }
        
        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,.3);
            border-radius: 50%;
            border-top-color: #fff;
            animation: spin 1s ease-in-out infinite;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🗄️ Oracle Database 연동 데이터 그리드</h1>
        <p class="subtitle">실시간 CRUD 작업 지원 시스템</p>
        
        <div id="status" class="status info" style="display: none;">
            연결 상태를 확인하는 중...
        </div>
        
        <div class="controls">
            <button class="btn-load" onclick="loadData()">
                🔄 데이터 불러오기
            </button>
            <button class="btn-save" onclick="saveChanges()" id="saveBtn" disabled>
                💾 변경사항 저장
            </button>
            <button class="btn-add" onclick="addRow()" id="addBtn">
                ➕ 행 추가
            </button>
            <button class="btn-remove" onclick="removeRow()" id="removeBtn">
                ➖ 선택 행 삭제
            </button>
            <button class="btn-export" onclick="exportData()">
                📥 CSV 내보내기
            </button>
        </div>
        
        <div id="grid-container"></div>
        
        <div class="info">
            <div class="info-item"><strong>🔌 연결:</strong> Flask 백엔드(http://localhost:5000)를 통해 Oracle DB 연결</div>
            <div class="info-item"><strong>✏️ 편집:</strong> 셀을 더블클릭하여 직접 수정 가능</div>
            <div class="info-item"><strong>💾 저장:</strong> 변경 후 '변경사항 저장' 버튼 클릭으로 DB 반영</div>
            <div class="info-item"><strong>⚠️ 주의:</strong> Flask 서버가 실행 중이어야 합니다 (python app.py)</div>
        </div>
    </div>

    <script>
        const API_URL = 'http://localhost:5000/api';
        let hot;
        let changedData = [];
        let originalData = [];

        // Handsontable 초기화
        function initHandsontable() {
            const container = document.getElementById('grid-container');
            
            hot = new Handsontable(container, {
                data: [],
                colHeaders: ['ID', '이름', '부서', '직급', '급여(만원)', '이메일', '입사일'],
                columns: [
                    { data: 'id', type: 'numeric', readOnly: true },
                    { data: 'name', type: 'text' },
                    { 
                        data: 'department', 
                        type: 'dropdown',
                        source: ['개발팀', '영업팀', '인사팀', '마케팅팀', '재무팀']
                    },
                    { 
                        data: 'position', 
                        type: 'dropdown',
                        source: ['사원', '대리', '과장', '차장', '부장', '팀장']
                    },
                    { data: 'salary', type: 'numeric', numericFormat: { pattern: '0,0.00' } },
                    { data: 'email', type: 'text' },
                    { data: 'join_date', type: 'date', dateFormat: 'YYYY-MM-DD' }
                ],
                rowHeaders: true,
                width: '100%',
                height: 500,
                licenseKey: 'non-commercial-and-evaluation',
                dropdownMenu: true,
                filters: true,
                columnSorting: true,
                contextMenu: true,
                manualColumnResize: true,
                manualRowResize: true,
                stretchH: 'all',
                afterChange: onDataChange,
                language: 'ko-KR'
            });
        }

        // 데이터 변경 감지
        function onDataChange(changes, source) {
            if (source === 'loadData' || !changes) return;
            
            changes.forEach(([row, prop, oldValue, newValue]) => {
                if (oldValue !== newValue) {
                    const rowData = hot.getSourceDataAtRow(row);
                    changedData.push({
                        id: rowData.id,
                        field: prop,
                        value: newValue
                    });
                }
            });
            
            if (changedData.length > 0) {
                document.getElementById('saveBtn').disabled = false;
                showStatus('변경사항이 있습니다. 저장 버튼을 클릭하세요.', 'info');
            }
        }

        // 상태 메시지 표시
        function showStatus(message, type = 'info') {
            const statusEl = document.getElementById('status');
            statusEl.textContent = message;
            statusEl.className = `status ${type}`;
            statusEl.style.display = 'block';
            
            if (type === 'success' || type === 'error') {
                setTimeout(() => {
                    statusEl.style.display = 'none';
                }, 5000);
            }
        }

        // 데이터 불러오기
        async function loadData() {
            showStatus('데이터를 불러오는 중...', 'info');
            
            try {
                const response = await fetch(`${API_URL}/employees`);
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const data = await response.json();
                originalData = JSON.parse(JSON.stringify(data));
                hot.loadData(data);
                changedData = [];
                document.getElementById('saveBtn').disabled = true;
                
                showStatus(`${data.length}개의 레코드를 불러왔습니다.`, 'success');
            } catch (error) {
                showStatus(`오류: ${error.message}. Flask 서버가 실행 중인지 확인하세요.`, 'error');
                console.error('데이터 로드 오류:', error);
            }
        }

        // 변경사항 저장
        async function saveChanges() {
            if (changedData.length === 0) {
                showStatus('저장할 변경사항이 없습니다.', 'info');
                return;
            }
            
            showStatus(`${changedData.length}개의 변경사항을 저장하는 중...`, 'info');
            
            try {
                const response = await fetch(`${API_URL}/employees/batch`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ changes: changedData })
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const result = await response.json();
                showStatus(result.message, 'success');
                changedData = [];
                document.getElementById('saveBtn').disabled = true;
                
                // 데이터 새로고침
                await loadData();
            } catch (error) {
                showStatus(`저장 오류: ${error.message}`, 'error');
                console.error('저장 오류:', error);
            }
        }

        // 행 추가
        async function addRow() {
            const newEmployee = {
                name: '새 직원',
                department: '개발팀',
                position: '사원',
                salary: 3000,
                email: '',
                join_date: new Date().toISOString().split('T')[0]
            };
            
            showStatus('새 직원을 추가하는 중...', 'info');
            
            try {
                const response = await fetch(`${API_URL}/employees`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(newEmployee)
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const result = await response.json();
                showStatus(result.message, 'success');
                
                // 데이터 새로고침
                await loadData();
            } catch (error) {
                showStatus(`추가 오류: ${error.message}`, 'error');
                console.error('추가 오류:', error);
            }
        }

        // 선택 행 삭제
        async function removeRow() {
            const selected = hot.getSelected();
            if (!selected || selected.length === 0) {
                showStatus('삭제할 행을 선택해주세요.', 'error');
                return;
            }
            
            const startRow = selected[0][0];
            const endRow = selected[0][2];
            const rowsToDelete = [];
            
            for (let i = startRow; i <= endRow; i++) {
                const rowData = hot.getSourceDataAtRow(i);
                if (rowData && rowData.id) {
                    rowsToDelete.push(rowData.id);
                }
            }
            
            if (rowsToDelete.length === 0) {
                showStatus('삭제할 유효한 행이 없습니다.', 'error');
                return;
            }
            
            if (!confirm(`선택된 ${rowsToDelete.length}개의 행을 삭제하시겠습니까?`)) {
                return;
            }
            
            showStatus(`${rowsToDelete.length}개의 행을 삭제하는 중...`, 'info');
            
            try {
                for (const id of rowsToDelete) {
                    const response = await fetch(`${API_URL}/employees/${id}`, {
                        method: 'DELETE'
                    });
                    
                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                }
                
                showStatus(`${rowsToDelete.length}개의 행이 삭제되었습니다.`, 'success');
                
                // 데이터 새로고침
                await loadData();
            } catch (error) {
                showStatus(`삭제 오류: ${error.message}`, 'error');
                console.error('삭제 오류:', error);
            }
        }

        // CSV 내보내기
        function exportData() {
            const exportPlugin = hot.getPlugin('exportFile');
            exportPlugin.downloadFile('csv', {
                bom: true,
                columnDelimiter: ',',
                columnHeaders: true,
                exportHiddenColumns: false,
                exportHiddenRows: false,
                fileExtension: 'csv',
                filename: 'employees_[YYYY]-[MM]-[DD]',
                mimeType: 'text/csv',
                rowDelimiter: '\r\n',
                rowHeaders: false
            });
            
            showStatus('CSV 파일이 다운로드되었습니다.', 'success');
        }

        // 페이지 로드 시 초기화
        window.onload = function() {
            initHandsontable();
            loadData();
        };
    </script>
</body>
</html>

Comments