다른 명령
편집 요약 없음 |
|||
| (같은 사용자의 중간 판 하나는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
=== 화면 === | === 기본 화면 === | ||
<source lang=html> | <source lang=html> | ||
| 279번째 줄: | 279번째 줄: | ||
</source> | </source> | ||
=== Flask + 오라클 연결하여 데이터 조회/입력/삭제/수정 === | |||
=== | |||
<source lang=html> | <source lang=html> | ||
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>