From 715687e8d271853f592c166642ed6cecd7f64157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=A7=80=EC=9D=B8?= Date: Wed, 25 Feb 2026 17:20:47 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B1=B4=EC=84=A4=ED=98=84=EC=9E=A5=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC>=EA=B1=B4=EC=84=A4=ED=98=84=EC=9E=A5=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C:=20=EC=97=91=EC=85=80=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=B2=98=EB=A6=AC=EC=8B=9C=20progress=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...nstructionProjectManagementController.java | 182 +++++++++--------- .../java/geoinfo/util/ExcelJobManager.java | 81 -------- .../geoinfo/util/ExcelMergeHeaderUtil.java | 139 ++++--------- .../construction-site-index.jsp | 98 +++++++++- .../WEB-INF/views/admins/frame/mainframe.jsp | 17 ++ 5 files changed, 235 insertions(+), 282 deletions(-) delete mode 100644 src/main/java/geoinfo/util/ExcelJobManager.java diff --git a/src/main/java/geoinfo/admins/constructionProjectManagement/ConstructionProjectManagementController.java b/src/main/java/geoinfo/admins/constructionProjectManagement/ConstructionProjectManagementController.java index ddf2d9c..4316f13 100644 --- a/src/main/java/geoinfo/admins/constructionProjectManagement/ConstructionProjectManagementController.java +++ b/src/main/java/geoinfo/admins/constructionProjectManagement/ConstructionProjectManagementController.java @@ -44,7 +44,6 @@ import geoinfo.com.EgovExcel; import geoinfo.comm.util.ScriptUtil; import geoinfo.comm.util.strUtil; import geoinfo.session.UserInfo; -import geoinfo.util.ExcelJobManager; import geoinfo.util.ExcelMergeHeaderUtil; import geoinfo.util.MyUtil; @@ -885,69 +884,105 @@ public class ConstructionProjectManagementController { return null; } - + /** - * 발주기관 건설현장 CSV 다운로드 처리 + * 발주기관 건설현장 엑셀 다운로드 처리(진행률 표시) * @param workbook * @param request * @param response * @throws Exception */ - @RequestMapping(method = RequestMethod.POST, value="/admins/drilling/inquiry/excel/start.do") - @ResponseBody - public Map downloadDrillingInquiryListExcel(HttpServletRequest request, @RequestParam HashMap params) throws Exception { + @RequestMapping(value = "admins/drilling/inquiry/excel.do") + public void downloadDrillingInquiryListExcel(HttpServletRequest request, HttpServletResponse response, @RequestParam HashMap params) throws Exception { - final String jobId = UUID.randomUUID().toString(); - final HttpServletRequest finalRequest = request; - final HashMap finalParams = params; - final DrillingInquiryService service = drillingInquiryService; - String excelFileName = "발주기관_건설현장_목록_" + ExcelMergeHeaderUtil.getTimeStampString("yyyyMMdd_HHmm") + ".xlsx"; + // ========================= + // 1. 엑셀 헤더 정보 구성 + // ========================= + String[] headers = { + "cid","constName","projectStateCodeName","constStartDate", + "constStateCodeName","inquiryDist","masterCompanyDept", + "masterCompanyAdmin","masterCompanyTel","coinstCompanyDept", + "constCompanyAdmin","constCompanyTel" + }; - ExcelJobManager.startJob(jobId, new ExcelJobManager.ExcelTask() { - public byte[] generate(ExcelJobManager.ProgressCallback callback) throws Exception { + String[][] headerNames = { + {"연번", "사업명", "입력상태", "사업내용", "", "발주기관현황", "", "", "", "건설사현황", "", ""}, + {"", "", "", "사업기간", "사업단계", "발주처", "담당부서", + "담당자", "담당자연락처", "건설사명", "담당자", "담당자연락처"} + }; - String[] headers = {"cid","constName","projectStateCodeName","constStartDate","constStateCodeName","inquiryDist" ,"masterCompanyDept","masterCompanyAdmin","masterCompanyTel","coinstCompanyDept","constCompanyAdmin","constCompanyTel"}; - String[][] headerNames = {{"연번", "사업명", "입력상태", "사업내용", "", "발주기관현황", "", "", "", "건설사현황", "", ""}, - {"", "", "", "사업기간", "사업단계", "발주처", "담당부서", "담당자", "담당자연락처", "건설사명", "담당자", "담당자연락처"}}; - - 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"; - - JSONObject resultObj = drillingInquiryService.drillingInquiryList(finalRequest, finalParams); - List list = (List) resultObj.get("datas"); - Long totalCount = (Long) resultObj.get("count"); - - int idx = 0; - for (EgovMap rowData : list) { - String constStartDate = (String) rowData.get("constStartDate"); - String constEndDate = (String) rowData.get("constEndDate"); - rowData.put("constStartDate", constStartDate + " ~ " + constEndDate); - rowData.put("cid", (totalCount) - (idx++)); - - String inquiryDist = ""; - if (rowData.get("glName") != null) - inquiryDist += rowData.get("glName") + " "; - if (rowData.get("gmName") != null) - inquiryDist += rowData.get("gmName") + " "; - if (rowData.get("gsName") != null) - inquiryDist += rowData.get("gsName"); - - rowData.put("inquiryDist", inquiryDist); - } - - byte[] excelBytes = ExcelMergeHeaderUtil.listToExcelMergeHeaderByteArray(list, headers, headerNames, headerWidths, columnType, "Sheet1", callback); - - return excelBytes; - - } - }, excelFileName); + final int[] headerWidths = { + 1325,15900,4240,6360,5830,8830, + 6890,2915,3710,5035,2915,3710 + }; - Map result = new HashMap(); - result.put("jobId", jobId); - return result; + String[] columnType = { + "String","String","String","String","String","String", + "String","String","String","String","String","String" + }; + + String sheetName = "Sheet1"; + String excelFileName = "발주기관 건설현장 목록"; + + // ========================= + // 2. DB 조회 + // ========================= + JSONObject resultObj = drillingInquiryService.drillingInquiryList(request, params); + + List list = (List) resultObj.get("datas"); + Long totalCount = (Long) resultObj.get("count"); + + // ========================= + // 3. 데이터 없으면 리턴 + // ========================= + if (list == null || list.isEmpty()) { + response.setContentType("text/html;charset=UTF-8"); + response.getWriter().write(""); + return; + } + + // ========================= + // 4. 리스트 후처리 + // ========================= + int idx = 0; + for (EgovMap rowData : list) { + + String constStartDate = (String) rowData.get("constStartDate"); + String constEndDate = (String) rowData.get("constEndDate"); + + rowData.put("constStartDate", + (constStartDate == null ? "" : constStartDate) + + " ~ " + + (constEndDate == null ? "" : constEndDate)); + + rowData.put("cid", (totalCount) - (idx++)); + + String inquiryDist = ""; + if (rowData.get("glName") != null) + inquiryDist += rowData.get("glName") + " "; + if (rowData.get("gmName") != null) + inquiryDist += rowData.get("gmName") + " "; + if (rowData.get("gsName") != null) + inquiryDist += rowData.get("gsName"); + + rowData.put("inquiryDist", inquiryDist.trim()); + } + + // ========================= + // 5. 엑셀 유틸 호출 + // ========================= + ExcelMergeHeaderUtil.listToExcelMergeHeader( + list, + response, + headers, + headerNames, + headerWidths, + columnType, + sheetName, + excelFileName + ); } - + /** * 건설현장 관리 > 발주기관 로그인 내역 화면 * @param params @@ -1119,45 +1154,4 @@ public class ConstructionProjectManagementController { 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 getExcelProgress(@RequestParam String jobId) { - int progress = ExcelJobManager.getProgress(jobId); - Map 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); - } } \ No newline at end of file diff --git a/src/main/java/geoinfo/util/ExcelJobManager.java b/src/main/java/geoinfo/util/ExcelJobManager.java deleted file mode 100644 index 7320d01..0000000 --- a/src/main/java/geoinfo/util/ExcelJobManager.java +++ /dev/null @@ -1,81 +0,0 @@ -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 jobMap = new ConcurrentHashMap(); - - 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; - } -} diff --git a/src/main/java/geoinfo/util/ExcelMergeHeaderUtil.java b/src/main/java/geoinfo/util/ExcelMergeHeaderUtil.java index f1f8121..d9f93e0 100644 --- a/src/main/java/geoinfo/util/ExcelMergeHeaderUtil.java +++ b/src/main/java/geoinfo/util/ExcelMergeHeaderUtil.java @@ -2,6 +2,7 @@ package geoinfo.util; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.net.URLEncoder; import java.time.LocalDate; @@ -17,6 +18,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataFormat; import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.HorizontalAlignment; @@ -24,7 +26,9 @@ import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFDataFormat; import org.apache.poi.xssf.usermodel.XSSFWorkbook; @@ -233,7 +237,8 @@ public class ExcelMergeHeaderUtil { throws IOException { if (ExcelMergeHeaderUtil.isNotEmpty(list)) { // 메모리에 100개의 행을 유지합니다. 행의 수가 넘으면 디스크에 적습니다. - XSSFWorkbook wb = new XSSFWorkbook(); +// XSSFWorkbook wb = new XSSFWorkbook(); + Workbook wb = new SXSSFWorkbook(200); Sheet sheet = wb.createSheet(sheetName); Row headerRow = sheet.createRow(headerNames.length); CellStyle cellStyle1 = wb.createCellStyle(); // 쉼표들어간 숫자 양식 @@ -252,7 +257,8 @@ public class ExcelMergeHeaderUtil { headerFont.setColor(IndexedColors.WHITE.getIndex()); // 글씨 색상 흰색 headerStyle.setFont(headerFont); // 헤더 스타일에 적용 - XSSFDataFormat format = wb.createDataFormat(); +// XSSFDataFormat format = wb.createDataFormat(); + DataFormat format = wb.createDataFormat(); cellStyle1.setDataFormat(format.getFormat("#,##0")); cellStyle2.setDataFormat(format.getFormat("#")); headerStyle.setBorderTop(BorderStyle.THIN); @@ -345,13 +351,34 @@ public class ExcelMergeHeaderUtil { } // 엑셀이름 한글깨짐방지 - String outputFileName = new String(excelFileName.getBytes("KSC5601"), "8859_1"); + String encodedFileName = URLEncoder.encode(excelFileName, "UTF-8").replaceAll("\\+", "%20"); - response.setHeader("Set-Cookie", "fileDownload=true; path=/"); - response.setHeader("Content-Disposition", String.format("attachment; filename=\"" + outputFileName + "_" - + ExcelMergeHeaderUtil.getTimeStampString("yyyyMMdd_HHmm") + ".xlsx\"")); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName + "_" + + ExcelMergeHeaderUtil.getTimeStampString("yyyyMMdd_HHmm") + ".xlsx"); - wb.write(response.getOutputStream()); + // ========================= + // 엑셀을 ByteArray로 생성 + // ========================= + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + wb.write(bos); + + if (wb instanceof SXSSFWorkbook) { + ((SXSSFWorkbook) wb).dispose(); // temp 파일 정리 + } + wb.close(); + + byte[] fileBytes = bos.toByteArray(); + response.setContentLength(fileBytes.length); + + // ========================= + // 전송 + // ========================= + OutputStream os = response.getOutputStream(); + os.write(fileBytes); + os.flush(); + os.close(); + wb.close(); } else { createNoDataAlert(response); @@ -359,104 +386,6 @@ public class ExcelMergeHeaderUtil { } - /** - * 로딩바 표시를 위하여 응답을 return함 - * - * @param response - * @throws IOException - */ - // public static byte[] listToExcelMergeHeaderByteArray(List list, - // String[] headers, String[][] headerNames, int[] headerWidths, String[] - // columnType, String sheetName) throws IOException { - public static byte[] listToExcelMergeHeaderByteArray(List list, String[] headers, String[][] headerNames, - int[] headerWidths, String[] columnType, String sheetName, ExcelJobManager.ProgressCallback callback) - throws IOException { - - if (!ExcelMergeHeaderUtil.isNotEmpty(list)) { - return new byte[0]; - } - - XSSFWorkbook wb = new XSSFWorkbook(); - Sheet sheet = wb.createSheet(sheetName); - - CellStyle headerStyle = wb.createCellStyle(); - CellStyle borderStyle = wb.createCellStyle(); - CellStyle cellStyle1 = wb.createCellStyle(); - CellStyle cellStyle2 = wb.createCellStyle(); - - // ===== 스타일 설정 ===== - borderStyle.setBorderTop(BorderStyle.THIN); - borderStyle.setBorderBottom(BorderStyle.THIN); - borderStyle.setBorderLeft(BorderStyle.THIN); - borderStyle.setBorderRight(BorderStyle.THIN); - - Font headerFont = wb.createFont(); - headerFont.setColor(IndexedColors.WHITE.getIndex()); - headerStyle.setFont(headerFont); - headerStyle.cloneStyleFrom(borderStyle); - headerStyle.setAlignment(HorizontalAlignment.CENTER); - headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); - headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - headerStyle.setFillForegroundColor(IndexedColors.GREY_40_PERCENT.getIndex()); - - XSSFDataFormat format = wb.createDataFormat(); - cellStyle1.setDataFormat(format.getFormat("#,##0")); - cellStyle2.setDataFormat(format.getFormat("#")); - - // ===== 헤더 병합 ===== - sheet.addMergedRegion(new CellRangeAddress(0, 1, 0, 0)); - sheet.addMergedRegion(new CellRangeAddress(0, 1, 1, 1)); - sheet.addMergedRegion(new CellRangeAddress(0, 1, 2, 2)); - sheet.addMergedRegion(new CellRangeAddress(0, 0, 3, 4)); - sheet.addMergedRegion(new CellRangeAddress(0, 0, 5, 8)); - sheet.addMergedRegion(new CellRangeAddress(0, 0, 9, 11)); - - // ===== 헤더 생성 ===== - for (int i = 0; i < headerNames.length; i++) { - Row row = sheet.createRow(i); - for (int j = 0; j < headerNames[i].length; j++) { - Cell cell = row.createCell(j); - cell.setCellValue(headerNames[i][j]); - cell.setCellStyle(headerStyle); - sheet.setColumnWidth(j, headerWidths[j]); - } - } - - // ===== 바디 ===== - for (int i = 0; i < list.size(); i++) { - EgovMap rowData = list.get(i); - Row row = sheet.createRow(i + 2); - - for (int j = 0; j < headers.length; j++) { - Cell cell = row.createCell(j); - Object value = rowData.get(headers[j]); - - if ("Money".equalsIgnoreCase(columnType[j])) { - cell.setCellValue(value instanceof Number ? ((Number) value).doubleValue() : 0); - cell.setCellStyle(cellStyle1); - } else if ("Int".equalsIgnoreCase(columnType[j])) { - cell.setCellValue(value instanceof Number ? ((Number) value).doubleValue() : 0); - cell.setCellStyle(cellStyle2); - } else { - cell.setCellValue(value == null ? "" : String.valueOf(value)); - } - - cell.setCellStyle(borderStyle); - // 진행률 계산 (중간중간 콜백 호출) - if (callback != null) { - int percent = (int) (((i + 1) / (double) list.size()) * 100); - callback.update(percent); - } - } - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - wb.write(bos); - wb.close(); - - return bos.toByteArray(); - } - private static void createNoDataAlert(HttpServletResponse response) throws IOException { response.setHeader("Content-Type", "text/html; charset=UTF-8"); PrintWriter out = response.getWriter(); diff --git a/src/main/webapp/WEB-INF/views/admins/constructionProjectManagement/construction-site-index.jsp b/src/main/webapp/WEB-INF/views/admins/constructionProjectManagement/construction-site-index.jsp index b09c82d..07b2c5b 100644 --- a/src/main/webapp/WEB-INF/views/admins/constructionProjectManagement/construction-site-index.jsp +++ b/src/main/webapp/WEB-INF/views/admins/constructionProjectManagement/construction-site-index.jsp @@ -698,6 +698,101 @@ } }); } + + $(document).ready(function() { + $(document).on("click", '#btnSubmit', function(e) { + + var canvasModal = parent.document.getElementById("canvasModal"); + var canvas = parent.document.getElementById("canvas"); + var ctx = canvas.getContext("2d"); + + function showPer(per) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 원형 진행률 + ctx.strokeStyle = "#f66"; + ctx.lineWidth = 10; + ctx.beginPath(); + ctx.arc(canvas.width/2, canvas.height/2, 50, -Math.PI/2, (-Math.PI/2 + 2*Math.PI*per/100)); + ctx.stroke(); + + // 숫자 표시 + ctx.font = '20px Arial'; + ctx.fillStyle = "#fff"; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(per + '%', canvas.width/2, canvas.height/2); + } + + const params = new URLSearchParams(); + params.append("constTag", trim($('#const-tag').val())); + params.append("constName", trim($('#const-name').val())); + params.append("excelDownload", "Y"); + + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/admins/drilling/inquiry/excel.do?" + params.toString(), true); + xhr.responseType = "blob"; + + var fakePer = 0; + var fakeInterval; + + xhr.onloadstart = function () { + canvasModal.style.display = "flex"; // 모달 표시 + fakePer = 0; + showPer(4); + + // fake progress 시작 (0~90%) + fakeInterval = setInterval(function() { + if (fakePer < 90) { + fakePer += Math.random() * 3; // 0~3%씩 증가 + showPer(Math.floor(fakePer)); + } + }, 100); // 0.1초마다 증가 + }; + + xhr.onprogress = function (e) { + if (e.lengthComputable) { + showPer(Math.floor((e.loaded / e.total) * 100)); + } + }; + + xhr.onload = function () { + if (xhr.status === 200) { + var blob = xhr.response; + + // 서버에서 보낸 Content-Disposition 헤더 확인 + var disposition = xhr.getResponseHeader('Content-Disposition'); + var fileName = "excel.xlsx"; // 기본 파일명 + if (disposition && disposition.indexOf('filename*=') !== -1) { + var matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (matches != null && matches[1]) { + fileName = decodeURIComponent(matches[1]); + // 접두사 UTF-8'' 제거 + fileName = fileName.replace(/^UTF-8''/, ''); + } + } + + // Blob 다운로드 + var link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; // 서버에서 가져온 이름 사용 + document.body.appendChild(link); // Firefox에서 필요 + link.click(); + document.body.removeChild(link); + } + }; + + xhr.onloadend = function () { + clearInterval(fakeInterval); // fake progress 중지 + showPer(100); + setTimeout(function () { + canvasModal.style.display = "none"; // 모달 숨김 + }, 500); + }; + + xhr.send(); + }); + }); @@ -101,5 +114,9 @@ function hideLoadingBar() {
0%
+ +
+ +