RealGrid2 가이드
Tip 🆕
이미지 Drag and Drop

이미지 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 {
      '&': '&amp;', '<': '&lt;', '>': '&gt;',
      '"': '&quot;', "'": '&#39;'
    }[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;
}