엑셀다운로드시 progress bar

main
유지인 2026-02-20 09:18:09 +09:00
parent 15a58a9c3b
commit 804309bbd5
5 changed files with 978 additions and 535 deletions

View File

@ -4,13 +4,13 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.SecureRandom;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
@ -22,6 +22,9 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.json.simple.JSONArray; import org.json.simple.JSONArray;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser; import org.json.simple.parser.JSONParser;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
@ -41,6 +44,7 @@ import geoinfo.com.EgovExcel;
import geoinfo.comm.util.ScriptUtil; import geoinfo.comm.util.ScriptUtil;
import geoinfo.comm.util.strUtil; import geoinfo.comm.util.strUtil;
import geoinfo.session.UserInfo; import geoinfo.session.UserInfo;
import geoinfo.util.ExcelJobManager;
import geoinfo.util.ExcelMergeHeaderUtil; import geoinfo.util.ExcelMergeHeaderUtil;
import geoinfo.util.MyUtil; import geoinfo.util.MyUtil;
@ -889,57 +893,59 @@ public class ConstructionProjectManagementController {
* @param response * @param response
* @throws Exception * @throws Exception
*/ */
@RequestMapping(value = "admins/drilling/inquiry/excel.do") @RequestMapping(method = RequestMethod.POST, value="/admins/drilling/inquiry/excel/start.do")
public void downloadDrillingInquiryListExcel(HttpServletRequest request, HttpServletResponse response, HSSFWorkbook workbook, @RequestParam HashMap<String, Object> params) throws Exception { @ResponseBody
public Map<String,Object> downloadDrillingInquiryListExcel(HttpServletRequest request, @RequestParam HashMap<String, Object> params) throws Exception {
HashMap<String, Object> map = new HashMap<String, Object>(); final String jobId = UUID.randomUUID().toString();
final HttpServletRequest finalRequest = request;
final HashMap<String,Object> finalParams = params;
final DrillingInquiryService service = drillingInquiryService;
String excelFileName = "발주기관_건설현장_목록_" + ExcelMergeHeaderUtil.getTimeStampString("yyyyMMdd_HHmm") + ".xlsx";
String[] headers = {"cid","constName","projectStateCodeName","constStartDate","constStateCodeName","inquiryDist" ,"masterCompanyDept","masterCompanyAdmin","masterCompanyTel","coinstCompanyDept","constCompanyAdmin","constCompanyTel"}; ExcelJobManager.startJob(jobId, new ExcelJobManager.ExcelTask() {
String[][] headerNames = {{"연번", "사업명", "입력상태", "사업내용", "", "발주기관현황", "", "", "", "건설사현황", "", ""}, public byte[] generate(ExcelJobManager.ProgressCallback callback) throws Exception {
{"", "", "", "사업기간", "사업단계", "발주처", "담당부서", "담당자", "담당자연락처", "건설사명", "담당자", "담당자연락처"}};
final int[] headerWidths = {1325, 15900, 4240, 6360, 5830, 8830, 6890, 2915, 3710, 5035, 2915, 3710}; String[] headers = {"cid","constName","projectStateCodeName","constStartDate","constStateCodeName","inquiryDist" ,"masterCompanyDept","masterCompanyAdmin","masterCompanyTel","coinstCompanyDept","constCompanyAdmin","constCompanyTel"};
String[] columnType = {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}; String[][] headerNames = {{"연번", "사업명", "입력상태", "사업내용", "", "발주기관현황", "", "", "", "건설사현황", "", ""},
String sheetName = "Sheet1"; {"", "", "", "사업기간", "사업단계", "발주처", "담당부서", "담당자", "담당자연락처", "건설사명", "담당자", "담당자연락처"}};
String excelFileName = "발주기관 건설현장 목록"; final int[] headerWidths = {1325, 15900, 4240, 6360, 5830, 8830, 6890, 2915, 3710, 5035, 2915, 3710};
String[] columnType = {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"};
String sheetName = "Sheet1";
// int startIndex = 0; JSONObject resultObj = drillingInquiryService.drillingInquiryList(finalRequest, finalParams);
Long totalCount = 0L; List<EgovMap> list = (List<EgovMap>) resultObj.get("datas");
// DB 조회 Long totalCount = (Long) resultObj.get("count");
JSONObject resultObj = drillingInquiryService.drillingInquiryList(request, params);
// 여기에서 list 꺼내기 int idx = 0;
List<EgovMap> list = (List<EgovMap>) resultObj.get("datas"); for (EgovMap rowData : list) {
totalCount = (Long) resultObj.get("count"); String constStartDate = (String) rowData.get("constStartDate");
String constEndDate = (String) rowData.get("constEndDate");
rowData.put("constStartDate", constStartDate + " ~ " + constEndDate);
rowData.put("cid", (totalCount) - (idx++));
int idx = 0; String inquiryDist = "";
for (EgovMap rowData : list) { if (rowData.get("glName") != null)
// 공사기간 형식 처리: startDate ~ endDate inquiryDist += rowData.get("glName") + " ";
String constStartDate = (String) rowData.get("constStartDate"); if (rowData.get("gmName") != null)
String constEndDate = (String) rowData.get("constEndDate"); inquiryDist += rowData.get("gmName") + " ";
rowData.put("constStartDate", constStartDate + " ~ " + constEndDate); // 공사기간을 'startDate ~ endDate' 형식으로 변환 if (rowData.get("gsName") != null)
rowData.put("cid", (totalCount) - (idx++)); inquiryDist += rowData.get("gsName");
String glName = ""; rowData.put("inquiryDist", inquiryDist);
String gmName = ""; }
String gsName = "";
String inquiryDist = ""; // 발주처
if ((String)rowData.get("glName") != null) {
glName = (String)rowData.get("glName");
inquiryDist = inquiryDist + glName + " ";
}if ((String)rowData.get("gmName") != null) {
gmName = (String)rowData.get("gmName");
inquiryDist = inquiryDist + gmName + " ";
}if ((String)rowData.get("gsName") != null) {
gsName = (String)rowData.get("gsName");
inquiryDist = inquiryDist + gsName;
}
rowData.put("inquiryDist", inquiryDist);
} byte[] excelBytes = ExcelMergeHeaderUtil.listToExcelMergeHeaderByteArray(list, headers, headerNames, headerWidths, columnType, "Sheet1", callback);
ExcelMergeHeaderUtil.listToExcelMergeHeader(list, response, headers, headerNames, headerWidths, columnType, sheetName, excelFileName); return excelBytes;
}
}, excelFileName);
Map<String,Object> result = new HashMap<String,Object>();
result.put("jobId", jobId);
return result;
} }
/** /**
@ -1113,4 +1119,45 @@ public class ConstructionProjectManagementController {
ExcelMergeHeaderUtil.listToExcelMergeHeaderForLoginHistory(resultList, response, headers, headerNames, headerWidths, columnType, sheetName, excelFileName); ExcelMergeHeaderUtil.listToExcelMergeHeaderForLoginHistory(resultList, response, headers, headerNames, headerWidths, columnType, sheetName, excelFileName);
} }
/**
* ( )
* @param jobId
* @return
*/
@RequestMapping(value="/admins/drilling/inquiry/excel/progress.do", method=RequestMethod.GET)
@ResponseBody
public Map<String,Object> getExcelProgress(@RequestParam String jobId) {
int progress = ExcelJobManager.getProgress(jobId);
Map<String,Object> result = new HashMap<>();
result.put("progress", progress);
return result;
}
/**
* ( )
* @param jobId
* @param response
* @throws IOException
*/
@RequestMapping(value="/admins/drilling/inquiry/excel/download.do", method=RequestMethod.GET)
public void downloadExcel(@RequestParam String jobId, HttpServletResponse response) throws IOException {
byte[] data = ExcelJobManager.getResult(jobId);
if(data == null){
response.sendError(404, "엑셀 파일을 찾을 수 없습니다.");
return;
}
// Job 단위로 저장된 파일명 사용
String fileName = ExcelJobManager.getFileName(jobId);
fileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setHeader("Content-Transfer-Encoding", "binary");
response.setContentType("application/octet-stream");
response.getOutputStream().write(data);
response.flushBuffer();
// 완료 후 캐시 제거
ExcelJobManager.clear(jobId);
}
} }

View File

@ -0,0 +1,81 @@
package geoinfo.util;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import org.json.simple.JSONObject;
import egovframework.rte.psl.dataaccess.util.EgovMap;
import geoinfo.admins.user.service.DrillingInquiryService;
/**
*
* @author JIYOO
*/
public class ExcelJobManager {
private static class Job {
byte[] data;
String fileName;
int progress;
}
private static Map<String, Job> jobMap = new ConcurrentHashMap<String, Job>();
public static void startJob(final String jobId, final ExcelTask excelTask, final String fileName) {
final Job job = new Job();
job.progress = 0;
job.fileName = fileName;
jobMap.put(jobId, job);
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] excelBytes = excelTask.generate(new ProgressCallback() {
@Override
public void update(int percent) {
job.progress = percent;
}
});
job.data = excelBytes;
job.progress = 100;
} catch (Exception e) {
job.progress = -1;
e.printStackTrace();
}
}
}).start();
}
public static int getProgress(String jobId) {
Job job = jobMap.get(jobId);
return job == null ? 0 : job.progress;
}
public static byte[] getResult(String jobId) {
Job job = jobMap.get(jobId);
return job == null ? null : job.data;
}
public static String getFileName(String jobId) {
Job job = jobMap.get(jobId);
return job == null ? "excel.xlsx" : job.fileName;
}
public static void clear(String jobId) {
jobMap.remove(jobId);
}
public static interface ProgressCallback {
void update(int percent);
}
public static interface ExcelTask {
byte[] generate(ProgressCallback callback) throws Exception;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -574,6 +574,7 @@
// 엑셀 다운로드 // 엑셀 다운로드
function clickExcelDownload(){ function clickExcelDownload(){
/*
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("constTag", trim($('#const-tag').val())); params.append("constTag", trim($('#const-tag').val()));
@ -595,6 +596,107 @@
// AJAX가 아닌 직접 다운로드 요청 // AJAX가 아닌 직접 다운로드 요청
window.location.href = "/admins/drilling/inquiry/excel.do?" + params.toString(); window.location.href = "/admins/drilling/inquiry/excel.do?" + params.toString();
$('#excelDownload').val(""); $('#excelDownload').val("");
*/
const params = new URLSearchParams();
params.append("constTag", trim($('#const-tag').val()));
params.append("constName", trim($('#const-name').val()));
params.append("excelDownload", "Y");
parent[1].showLoadingBar();
fetch("/admins/drilling/inquiry/excel.do", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params.toString()
})
.then(response => {
if (!response.ok) throw new Error("다운로드 실패");
const disposition = response.headers.get("Content-Disposition");
let fileName = "excel.xlsx";
if (disposition && disposition.includes("filename=")) {
fileName = disposition.split("filename=")[1].replace(/"/g, "");
}
return response.blob().then(blob => ({ blob, fileName }));
})
.then(({ blob, fileName }) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch(err => {
alert("엑셀 다운로드 중 오류 발생");
console.error(err);
})
.finally(() => {
$('#excelDownload').val("");
parent.hideLoadingBar();
});
}
let currentJobId = null;
function startExcelDownload() {
const params = new URLSearchParams();
params.append("constTag", trim($('#const-tag').val()));
params.append("constName", trim($('#const-name').val()));
params.append("excelDownload", "Y");
parent.showLoadingBar();
// parent.document.getElementById("progressWrap").style.display = "block";
fetch("/admins/drilling/inquiry/excel/start.do", {
method: "POST",
headers: {"Content-Type":"application/x-www-form-urlencoded"},
body: params.toString()
})
.then(res => res.json())
.then(data => {
currentJobId = data.jobId;
pollProgress();
});
$('#excelDownload').val("");
}
function pollProgress() {
fetch("/admins/drilling/inquiry/excel/progress.do?jobId=" + currentJobId)
.then(res => res.json())
.then(data => {
let percent = data.progress;
if(percent < 0){
alert("엑셀 생성 중 오류 발생");
return;
}
parent.document.getElementById("progressBar").style.width = percent + "%";
parent.document.getElementById("progressText").innerText = percent + "%";
if(percent < 100){
setTimeout(pollProgress, 500);
} else {
setTimeout(function(){
window.location = "/admins/drilling/inquiry/excel/download.do?jobId=" + currentJobId;
parent.hideLoadingBar();
}, 700);
}
});
} }
</script><style> </script><style>
.drilling .page-content-inner { .drilling .page-content-inner {
@ -780,7 +882,8 @@ li {
</div> </div>
<div class="table-info-group"> <div class="table-info-group">
<span>Total: <span id="count">-</span>건</span> <span>Total: <span id="count">-</span>건</span>
<span class="pull-right"><img src="${pageContext.request.contextPath}/images/admins/excel.gif" style="cursor:hand" onClick="javascript:clickExcelDownload()"></span> <%-- <span class="pull-right"><img src="${pageContext.request.contextPath}/images/admins/excel.gif" style="cursor:hand" onClick="javascript:clickExcelDownload()"></span> --%>
<span class="pull-right"><img src="${pageContext.request.contextPath}/images/admins/excel.gif" style="cursor:hand" onClick="startExcelDownload()"></span>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
@ -835,7 +938,6 @@ li {
</div> </div>
</section> </section>
<!-- 페이지 컨테이너 끝 --> <!-- 페이지 컨테이너 끝 -->
</form> </form>
</body> </body>
</html> </html>

View File

@ -7,7 +7,79 @@
<script> <script>
var waitWin; var waitWin;
function showLoadingBar() {
document.getElementById("loadingModal").style.display = "flex";
document.getElementById("progressBar").style.width = "0%";
document.getElementById("progressText").innerText = "0%";
}
function hideLoadingBar() {
document.getElementById("loadingModal").style.display = "none";
}
</script> </script>
<style type="text/css">
/* 전체 화면 반투명 배경 */
#loadingModal {
display: none; /* 기본 숨김 */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* background: rgba(0,0,0,0.5); /* 반투명 검정 */ */
z-index: 9999;
/* display: flex; */
align-items: center;
justify-content: center;
}
/* 내용 박스 */
#loadingContent {
background: #fff;
padding: 30px 50px;
border-radius: 10px;
text-align: center;
min-width: 300px;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
}
/* 로딩 스피너 */
.spinner {
border: 6px solid #f3f3f3;
border-top: 6px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg);}
100% { transform: rotate(360deg);}
}
/* 진행률 바 */
#progressWrap {
width: 100%;
height: 20px;
background: #eee;
border-radius: 10px;
margin: 10px 0;
}
#progressBar {
width: 0%;
height: 100%;
background: #4caf50;
border-radius: 10px;
}
#progressText {
margin-top: 5px;
font-weight: bold;
}
</style>
</head> </head>
<body leftmargin="0" topmargin="0" marginheight="0" marginwidth="0"> <body leftmargin="0" topmargin="0" marginheight="0" marginwidth="0">
<!-- <!--
@ -18,5 +90,16 @@ var waitWin;
<iframe src="${pageContext.request.contextPath}/admins/${menuId}/${pId}.do?isFirst=true" frameborder="0" height="740" width="100%" scrolling="yes" name="iframeMain" style="overflow-x: hidden;"></iframe> <iframe src="${pageContext.request.contextPath}/admins/${menuId}/${pId}.do?isFirst=true" frameborder="0" height="740" width="100%" scrolling="yes" name="iframeMain" style="overflow-x: hidden;"></iframe>
<!-- 로딩 모달 -->
<div id="loadingModal">
<div id="loadingContent">
<div class="spinner"></div>
<div id="loadingText">엑셀 파일 생성 중...</div>
<div id="progressWrap">
<div id="progressBar"></div>
</div>
<div id="progressText">0%</div>
</div>
</div>
</body> </body>
</html> </html>