이미지 Drag and Drop
RealGrid에 이미지 파일을 Drag and Drop으로 첨부하고 미리보기를 표시하는 예제입니다.
파일을 드래그하여 원하는 행의 셀에 놓으면 첫 번째 컬럼인 이미지 첨부에 표시됩니다.
첨부한 이미지는 썸네일로 표시되며 클릭하면 다운로드할 수 있습니다.
파일 옆의 X 버튼을 누르면 해당 첨부파일이 그리드에서 제거됩니다. 같은 행에 이미지를 추가로 놓으면 기존 이미지는 유지한 채 새 이미지가 아래로 추가됩니다.
이 예제에서는 파일을 Base64로 인코딩하여 그리드의 데이터에 저장합니다.
실제 서비스에서는 저장된 파일 정보를 서버로 전송하거나, 파일을 놓는 시점에 서버로 업로드하도록 구현하세요.
※ IE에서는 동작하지 않습니다.
※ 서버 처리 부분은 직접 구현하셔야 합니다.
Loading RealGrid...
아래는 현재 예제에서 이미지 표시, 파일 추가·삭제, Drag and Drop 이벤트 연결에 필요한 핵심 코드입니다.
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, function(ch) {
return {
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
}[ch];
});
}
function getFiles(grid, dataRow) {
const files = grid.getDataSource().getValue(dataRow, 'Files');
return Array.isArray(files) ? files : [];
}
function getImageUrl(file) {
return file && typeof file.dataURL === 'string' &&
file.dataURL.startsWith('data:image/') ? file.dataURL : '';
}
// 첨부 이미지를 썸네일로 표시하고 다운로드와 삭제 버튼을 제공.
const htmlRenderer = {
type: 'html',
callback: function(grid, cell, w, h) {
const dataRow = cell.index.dataRow;
const files = getFiles(grid, dataRow);
let str = '';
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileName = escapeHtml(file.fileName);
const imageUrl = getImageUrl(file);
const downloadUrl = 'data:application/octet-stream;base64,' +
(file.base64Data || '');
const content = imageUrl
? '<img class="file-preview" src="' + escapeHtml(imageUrl) +
'" alt="' + fileName + '">'
: fileName;
str += '<span class="file-item">' +
'<a class="file-download" download="' + fileName +
'" href="' + downloadUrl + '">' + content + '</a>' +
'<button type="button" class="file-delete" data-row="' +
dataRow + '" data-index="' + i +
'" onclick="fileDelete(this)">X</button>' +
'</span>';
}
return str;
}
};
function deleteFile(el, dataProvider, gridView) {
const dataRow = Number(el.dataset.row);
const fileIndex = Number(el.dataset.index);
if (!Number.isInteger(dataRow) || dataRow < 0 ||
!Number.isInteger(fileIndex) || fileIndex < 0) return;
const fileValue = dataProvider.getValue(dataRow, 'Files');
const files = Array.isArray(fileValue) ? fileValue : [];
dataProvider.setValue(dataRow, 'Files',
files.filter((_, index) => index !== fileIndex));
}
// 기존 파일을 유지한 채 새 파일들을 순서대로 Base64 변환하여 추가.
function fileTobase64(index, dataRow, files, outputs, dataProvider, gridView) {
if (index >= files.length) {
dataProvider.setValue(dataRow, 'Files', outputs);
return;
}
const file = files[index];
const reader = new FileReader();
reader.onload = function() {
const dataURL = reader.result;
outputs.push({
fileName: file.name,
dataURL: dataURL,
base64Data: dataURL.split(',')[1]
});
fileTobase64(index + 1, dataRow, files, outputs, dataProvider, gridView);
};
reader.onerror = function() {
console.error('파일 읽기 실패:', file.name);
fileTobase64(index + 1, dataRow, files, outputs, dataProvider, gridView);
};
reader.readAsDataURL(file);
}
function dragEnter(gridContainer) {
gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
element.ondragenter = () => element.classList.add('drag-over-cell');
element.ondragleave = (event) => {
const relatedTarget = event.relatedTarget;
if (!relatedTarget || !element.contains(relatedTarget)) {
element.classList.remove('drag-over-cell');
}
};
element.ondrop = element.ondragover = (event) => event.preventDefault();
});
}
function dragLeave(ev, gridContainer) {
if (!ev.relatedTarget || !gridContainer.contains(ev.relatedTarget)) {
gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
element.classList.remove('drag-over-cell');
});
}
}
function dropFiles(ev, gridView, dataProvider) {
ev.preventDefault();
const gridContainer = gridView.getContainer();
gridContainer.querySelectorAll('.rg-data-cell').forEach(element => {
element.ondrop = null;
element.ondragenter = null;
element.ondragleave = null;
element.ondragover = null;
element.classList.remove('drag-over-cell');
});
const cellIndex = gridView.mouseToIndex(ev.clientX, ev.clientY, true);
if (!cellIndex || cellIndex.dataRow < 0) {
alert('정확한 위치에 파일을 drop 하세요');
return;
}
const files = Array.from(ev.dataTransfer.files || []);
if (files.length === 0) return;
const dataRow = cellIndex.dataRow;
const existingFiles = dataProvider.getValue(dataRow, 'Files');
const outputs = Array.isArray(existingFiles) ? [...existingFiles] : [];
fileTobase64(0, dataRow, files, outputs, dataProvider, gridView);
}
// 그리드 생성 시 첫 번째 컬럼으로 사용할 설정입니다.
const fileDownloadColumn = {
name: 'FileDownload',
fieldName: 'FileDownload',
width: 180,
header: { text: '이미지 첨부' },
editable: false,
styleName: 'left-column',
styleCallback: function(grid, cell) {
return { renderer: htmlRenderer };
}
};
// 기존 그리드의 FileDownload 컬럼에 렌더러를 연결합니다.
gridView.columnByName('FileDownload').styleCallback = fileDownloadColumn.styleCallback;
const gridContainer = gridView.getContainer();
function fileDelete(el) {
deleteFile(el, dataProvider, gridView);
}
window.fileDelete = fileDelete;
function dragEnterHandler(ev) {
dragEnter(gridContainer);
}
function dragLeaveHandler(ev) {
dragLeave(ev, gridContainer);
}
function dragOverHandler(ev) {
ev.preventDefault();
}
function dropHandler(ev) {
dropFiles(ev, gridView, dataProvider);
}
const realgridDiv = document.getElementById('realgrid');
realgridDiv.ondragenter = dragEnterHandler;
realgridDiv.ondragleave = dragLeaveHandler;
realgridDiv.ondrop = dropHandler;
realgridDiv.ondragover = dragOverHandler;이미지 미리보기와 첨부파일 배치에 사용하는 CSS입니다.
.drag-over-cell { background-color: lightblue; }
.right-column { text-align: right; }
.left-column { text-align: left; }
.file-item {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
margin: 3px;
}
.file-download { display: inline-flex; align-items: center; min-width: 0; overflow-wrap: anywhere; }
.file-preview { width: 55px; height: 55px; object-fit: contain; }
.file-delete {
flex: 0 0 10px;
box-sizing: border-box;
width: 10px;
min-width: 10px;
height: 10px;
padding: 0;
border: 0;
background-color: #222;
color: #fff;
text-align: center;
font-size: 10px;
line-height: 10px;
cursor: pointer;
}