Developer's Development

[HTML] Luckysheet.js 엑셀 미리보기 본문

개발/HTML

[HTML] Luckysheet.js 엑셀 미리보기

mylee99 2024. 4. 15. 13:31

Sheet.js & xspreadsheet.js 엑셀 미리보기 (jQuery) 포스팅에서 이어지는 이야기

2024.02.05 - [개발/HTML] - [HTML] SheetJS & xspreadsheetJS 엑셀 미리보기 (jQuery)

 

[HTML] SheetJS & xspreadsheetJS 엑셀 미리보기 (jQuery)

파일 미리보기 및 다운로드 포스팅에서 이어지는 이야기 2023.12.29 - [개발/JAVA] - [JAVA] 파일 미리보기 및 다운로드 [JAVA] 파일 미리보기 및 다운로드 javax.crypto 파일 암/복호화 포스팅에서 이어지는

developer-mylee.tistory.com

 

Reference

https://dream-num.github.io/LuckysheetDocs/

 

Home

 

dream-num.github.io

 

Luckysheet.js는 Excel과 유사한 웹 기반의 스프레드시트 라이브러리이며, 엑셀 파일의 미리보기를 제공하는 데 사용한다.
Luckyexcel.js를 사용하여 엑셀 데이터를 파싱 하고, Luckysheet.js로 UI에 렌더링 한다.

 

공식 문서에 나와있는 코드를 확인해 보면,

input file의 change가 감지되었을 때 같은 document 안에 생성되어 있는 div에 선택된 엑셀 파일을 렌더링 한 케이스다.

(동일한 케이스라면 공식 문서의 코드를 그대로 사용해도 에러나 문제가 생기지 않음)

 

하지만 내가 개발해야 했던 환경(client 요구사항)은,

1) 미리보기 버튼 클릭 시, 백엔드를 무조건 다녀와야 한다. (파일 암복호화 작업)

2) 같은 document 창이 아닌, 새로운 창 전체에 엑셀 파일을 렌더링 한다.

 

처음엔 XMLHttpRequest를 통한 백엔드 통신 후 onload function에서 window.open을 하고,

newWindow.luckysheet.create 와 같은 방식으로 아래와 같이 작성하였다.

하지만 이 방식은 newWindow에서 js를 전부 로드하지 못할 때 간헐적으로 javascript 에러가 발생하며,

로드를 완료해 create에 성공한 경우라도 렌더링이 깨져 셀 스타일을 정확하게 가져오지 못한다.

 

jQuery
//최종 코드
gridExcelToWeb = function (file, orgFileNm) {
    //새 창으로 열기 위해 작성한 코드
    const newWindow = window.open('', '_blank');
    newWindow.document.write("<html><head><link rel='stylesheet' href='/css/pluginsCss.css'>" +
            "<link rel='stylesheet' href='/css/plugins.css'>" +
            "<link rel='stylesheet' href='/css/luckysheet.css'>" +
            "<link rel='stylesheet' href='/css/iconfont.css'>" +
            "<script src='/js/jquery.min.js'><\/script>" +
            "<script src='/js/luckyexcel.umd.js'><\/script>" +
            "<script src='/js/plugin.js'><\/script>" +
            "<script src='/js/luckysheet.umd.js'><\/script>" +
            "<title>"+orgFileNm+"</title>" +
            "</head><body><div id='luckysheet' style='width:100%; height:100%;'></div></body></html>");

    //LuckyExcel을 사용하여 데이터를 파싱하고 LuckySheet에 적용
    LuckyExcel.transformExcelToLucky (file, function (exportJson) {
        if (newWindow.luckysheet) {
            newWindow.luckysheet.destroy();
        }

        newWindow.luckysheet.create({
            container: 'luckysheet',
            showinfobar: false,
            showtoolbar: false,
            data: exportJson.sheets
        });
    });
}
코드를 작성하며 났던 에러들
Uncaught TypeError: Cannot read properties of undefined (reading 'getContext')
//luckysheet를 create할 container는 newWindow의 #luckysheet div인데, 
//luckysheet.create 앞에 newWindow를 명시해주지 않아 현재 창에서 해당 container를 찾으며 발생한 에러

cannot read properties of undefined (reading 'forEach')
//sheet data가 정의되지 않았거나 값이 없기 때문에 발생한 에러
//이 경우, 데이터가 제대로 파싱되지 않았거나 데이터 구조가 예상과 다른 경우일 수 있음.

cannot read properties of undefined (reading 'create')
//Luckysheet.create를 실행할 때 발생하며, Luckysheet 객체가 정의되지 않았거나 제대로 초기화되지 않았음을 나타냄.

 

뭐가 문제인지, 어떻게 고쳐야 하는지 삽질을 한 달 정도.. 했다.

혹시 blob 객체로 파일을 받아오는 방식이 잘못된 건가? 해서 blob과 file에 대한 공부도 했었고,

혹시 새 창 말고 기존에 사용하고 있는 bPopup에 띄우면 괜찮을까? 해서 팝업 방식으로 변경해 볼까도 했었고,

부모창에서 자식창으로 동적으로 반영하는 게 문제가 되는 걸까도 싶었고,

.. 기타 등등 이슈를 GPT가 함께 고민해 주었다.

AI라고 GPT한테 화도 엄청 냈었고, 팀원들한테도 아 엑셀 진짜 꼴 보기 싫어죽겠어요..(100% 진심이긴 함)도 시전

 

GPT 흔적

 

 

차근차근 문제를 접근해 보니, 해결해야 할 문제들은 이랬다.

1) 렌더링해야 할 HTML에 "엑셀 문서를 파싱하고 렌더링 하는 데 필요한 모든 js와 css"가 확실히 로드되어야 한다. 그러려면 부모창에서 newWindow에 동적으로 반영하는 방법은 포기하는 게 맞고, 전용의 새로운 HTML을 사용해야 한다.

2) back단에서 복호화 작업 완료 후, 복호화한 파일을 리턴할 새로운 HTML의 경로와 함께 반환해야 한다.

 

1번은 떡이고 당연히 2번이 골칫덩어리였다.

사용하고 있던 방식은,

1) 파일 리턴 방식 : response.setHeader에 inline 방식으로 담아서 보낸다. outputStream도 사용 중이다.

2) 화면 전환 방식 : Spring Boot MVC 환경에서 return "/test/testHTML"과 같이 Controller에서 String을 리턴한다. 근데 이걸 또 기존 화면 전환이 아니라 새 창으로 열어야 한다....

 

위의 2가지는 함께 리턴할 수 없다. 이유는 HTTP 응답에 대해 오직 하나의 출력 스트림만 사용할 수 있다는 제한 때문.

맨 마지막에 GPT가 해준 답변이 답안이 되었다. "두 가지 요청을 분리해서 처리하라"라고.

첫 번째 요청에서는 HTML 경로를 요청하고, 서버에서는 해당 HTML 파일을 새 창으로 반환한다.

두 번째 요청에서는 복호화 파일을 요청하고, 서버에서는 해당 파일을 반환한다.

 

 

이제 최종 코드 시작

1. 첫 번째 작업 (HTML 경로 요청)

현재 창에서 새 창으로 열 HTML을 서버에 요청하고, 새 창에서 파일 복호화 작업에 필요한 파라미터를 함께 보낸다.

서버에서는 전달받은 파라미터를 Model에 담아 새로운 HTML을 반환한다.

jQuery
//새 창으로 열기 위해 target _blank로 사용함
$fileDecryptForm.attr({"action":"/filePreview","target":"_blank","method":"post"}).submit();
Controller
@RequestMapping("/filePreview")
public String filePreview( FileVo fileVo, Model model ) {
	model.addAttribute( "fileParam", fileVo );

	return "/filePreviewHTML";
}

 

 

2. 두 번째 작업 (복호화 파일 요청 및 엑셀 파일 렌더링)

새로 열린 창에서 model에 담겨있는 파라미터와 함께 XMLHttpRequest로 복호화 파일을 요청한다.

서버에서는 복호화 작업 완료 후 파일을 반환한다.

HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<title>엑셀 미리보기</title>
	<link href="/css/pluginsCss.css" rel="stylesheet">
	<link href="/css/plugins.css" rel="stylesheet">
	<link href="/css/luckysheet.css" rel="stylesheet">
	<link href="/css/iconfont.css" rel="stylesheet">
        <!-- Luckysheet uses jQuery -->
	<script src="/js/jquery.min.js"></script>
	<script src="/js/luckyexcel.umd.js"></script>
	<script src="/js/plugin.js"></script>
	<script src="/js/luckysheet.umd.js"></script>
</head>
<body>
<div id="luckysheet" style="margin:0px; padding:0px; position:absolute; width:100%; left:0px; top:50px; bottom:0px; outline:none;"></div>
</body>
<script th:inline="javascript">
	$(function () {
		window.onload = () => {

			const request = new XMLHttpRequest();
			request.open('POST', '/fileDownload', true);
			request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
			request.send("encryptedFilePath=" + [[${fileParam.encryptedFilePath}]] + "&decryptedFile=" + [[${fileParam.decryptedFile}]]
					+ "&previewYn=" + [[${fileParam.previewYn}]] + "&excelYn=" + [[${fileParam.excelYn}]]);
			request.responseType = 'blob';
			request.onload = function () {
				if (request.status === 200) {
					const blob = new Blob([request.response], {type: 'application/xlsx'});
					if (blob.size > 0) {
						gridExcelToWeb(blob);
					}
				} else {
					alert("처리중 오류가 발생하였습니다.");
				}
			};

			gridExcelToWeb = function (file) {
				// //LuckyExcel을 사용하여 데이터를 파싱하고 LuckySheet에 적용
				LuckyExcel.transformExcelToLucky(file, function (exportJson) {
					luckysheet.destroy();

					luckysheet.create({
						container: 'luckysheet',
						showinfobar: false,
						showtoolbar: false,
						data: exportJson.sheets
					});
				});
			}
		}
	});
</script>
</html>
Controller
@ResponseBody
@RequestMapping("/fileDownload")
public void fileDownload( FileVo fileVo, HttpServletRequest request, HttpServletResponse response ) throws Exception {
    //파일 복호화
    fileVo.setEncryptedFile( new File( fileVo.getEncryptedFilePath() ) );
    FileUtil.decryptFile( fileVo, request, response );
}

 

이렇게 하면 아무런 에러 없이 정상적으로 새 창에 엑셀 파일을 렌더링 한다.

지금 와서 보면 정말 간단한 이슈였는데 왜 이렇게까지 돌고 돌아왔는지도 모르겠고..

이제 엑셀 작업 안 해도 된다는 생각에 후련한 마음 반, 더 빨리 끝내지 못해 아쉬운 마음 반..?

Comments