<!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>