From b069e1d1be248d3030996ea4b3ec069c3f10e80c Mon Sep 17 00:00:00 2001 From: Kana Date: Mon, 15 Jan 2024 18:00:19 +0900 Subject: [PATCH] add excel upload --- .../no1/wms/category/CategoryController.java | 112 +++++++++++++---- .../com/no1/wms/category/CategoryDto.java | 4 +- .../java/com/no1/wms/excel/EgovWebUtil.java | 17 +++ ...xcelUtils.java => ExcelDownlodeUtils.java} | 54 +++++++- .../com/no1/wms/excel/ExcelManagerXlsx.java | 110 +++++++++++++++++ .../no1/wms/excel/ExcelRequestManager.java | 86 +++++++++++++ .../webapp/WEB-INF/views/category/list.jsp | 65 +++++++++- .../webapp/WEB-INF/views/category/test.jsp | 115 +++++++----------- src/main/webapp/WEB-INF/views/main.jsp | 32 +++++ .../excelform/카테고리 데이터 입력 서식.xlsx | Bin 0 -> 7485 bytes 10 files changed, 496 insertions(+), 99 deletions(-) create mode 100644 src/main/java/com/no1/wms/excel/EgovWebUtil.java rename src/main/java/com/no1/wms/excel/{ExcelUtils.java => ExcelDownlodeUtils.java} (54%) create mode 100644 src/main/java/com/no1/wms/excel/ExcelManagerXlsx.java create mode 100644 src/main/java/com/no1/wms/excel/ExcelRequestManager.java create mode 100644 src/main/webapp/WEB-INF/views/main.jsp create mode 100644 src/main/webapp/excelfiles/excelform/카테고리 데이터 입력 서식.xlsx diff --git a/src/main/java/com/no1/wms/category/CategoryController.java b/src/main/java/com/no1/wms/category/CategoryController.java index c30c745..9e7e4f3 100644 --- a/src/main/java/com/no1/wms/category/CategoryController.java +++ b/src/main/java/com/no1/wms/category/CategoryController.java @@ -1,23 +1,32 @@ package com.no1.wms.category; -import java.util.List; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import com.no1.wms.excel.ExcelUtils; +import com.no1.wms.excel.ExcelRequestManager; +import com.no1.wms.excel.ExcelDownlodeUtils; @@ -28,16 +37,15 @@ public class CategoryController { CategoryService categoryService; @Autowired - ExcelUtils excelUtils; + ExcelDownlodeUtils excelDownlodeUtils; //테스트 @GetMapping("/category/test") - public String testPage(Model m) { - List dto = categoryService.selectAllCategory(); - m.addAttribute("dto", dto); + public String testPage(Model m, HttpServletRequest request) { + return "category/test"; - } + }; // 카테고리 리스트 출력 @GetMapping("/category/list") @@ -77,7 +85,7 @@ public class CategoryController { //List dto = categoryService.categoryList(page); //m.addAttribute("list", dto); return "category/list"; - } + }; // 상세페이지 @PostMapping("/category/read") @@ -86,13 +94,13 @@ public class CategoryController { CategoryDto dto = categoryService.selectByKanCode(kan_code); m.addAttribute("dto", dto); return "category/read"; - } + }; // 생성 - 폼 @PostMapping("/category/create") public String create() { return "category/create"; - } + }; // 생성 - Ajax @@ -105,7 +113,7 @@ public class CategoryController { } else { return false; } - } + }; // 수정 - 폼 @PostMapping("/category/update") @@ -113,7 +121,7 @@ public class CategoryController { CategoryDto dto = categoryService.selectByKanCode(kan_code); m.addAttribute("dto", dto); return "category/update"; - } + }; // 수정 - Ajax @PutMapping("/category/update_process") @@ -126,7 +134,7 @@ public class CategoryController { } else { return false; } - } + }; // 삭제 @DeleteMapping("/category/delete") @@ -138,18 +146,18 @@ public class CategoryController { } else { return false; } - } + }; // 엑셀다운로드테스트 - @GetMapping("/category/download") - public void downloadExcel(HttpServletResponse response) { + @GetMapping("/category/downloadTest") + public void downloadExcelTest(HttpServletResponse response) { List dto = categoryService.selectAllCategory(); String excelFileName = "카테고리 테스트 파일"; String sheetName = "카테고리"; String[] columnName = {"KAN_CODE","대분류","중분류","소분류","세분류"}; - excelUtils.downloadCategoryExcelFile(excelFileName, response, sheetName, columnName, dto); + excelDownlodeUtils.downloadCategoryExcelFile(excelFileName, response, sheetName, columnName, dto); - } + }; // KAN코드 중복확인 메서드 @PostMapping("/category/checkKancode") @@ -157,7 +165,7 @@ public class CategoryController { public String chackKancode(String kan_code) { String checkkan = categoryService.kanCheck(kan_code); return checkkan; - } + }; //카테고리 검색 모달 @GetMapping("/category/categorysearch") @@ -192,9 +200,67 @@ public class CategoryController { m.addAttribute("p" , page); return "modal/categorysearch"; - } + }; + + @GetMapping("/category/downlodeCategoryForm") + public void downlodeCategoryForm (HttpServletResponse response) throws IOException{ + String categoryFormName = "카테고리 데이터 입력 서식.xlsx"; + excelDownlodeUtils.downlodeExcelForm(response, categoryFormName); + }; + @PostMapping("/category/uplodeExcel") + public String uploadExcel(@ModelAttribute("dto") CategoryDto dto , RedirectAttributes redirectAttributes,final MultipartHttpServletRequest multiRequest, + HttpServletRequest request,ModelMap model) { + + Map resMap = new HashMap<>(); + + + try { + + ExcelRequestManager em = new ExcelRequestManager(); + + // 멀티파트 요청 객체에서 파일 맵을 가져옴 + final Map files = multiRequest.getFileMap(); + //초기화 + List> apply =null; + + //엑셀파일 가져와서 저장 및 읽기 + //변수는 멀티파트 요청 객체의 파일맵, 저장할 엑셀파일명 이름에 추가할 숫자(그냥 0으로 해도 됨) + //마찬가지로 엑셀파일 명 이름에 추가할 문자열, uplode폴더에 들어갈 폴더명(카테고리같은 파트 이름으로 해주세요) + //폴더가 없으면 자동생성되게 해뒀습니다. + //마지막으로 HttpServletRequest + apply = em.parseExcelSpringMultiPart(files, "테스트파일", 0, "", "category", request); + + for (int i = 0; i < apply.size(); i++) { + + dto.setKan_code(apply.get(i).get("cell_0")); + dto.setCls_nm_1(apply.get(i).get("cell_1")); + dto.setCls_nm_2(apply.get(i).get("cell_2")); + dto.setCls_nm_3(apply.get(i).get("cell_3")); + dto.setCls_nm_4(apply.get(i).get("cell_4")); + dto.setActivation(true); + + categoryService.createProcess(dto); + + } + + resMap.put("res", "ok"); + resMap.put("msg", "업로드 성공"); + } catch (Exception e) { + System.out.println(e.toString()); + resMap.put("res", "error"); + resMap.put("msg", "업로드 실패"); + } + + redirectAttributes.addFlashAttribute("resMap", resMap); + + return "redirect:/category/list"; + }; + + + + diff --git a/src/main/java/com/no1/wms/category/CategoryDto.java b/src/main/java/com/no1/wms/category/CategoryDto.java index dddabe6..47511b0 100644 --- a/src/main/java/com/no1/wms/category/CategoryDto.java +++ b/src/main/java/com/no1/wms/category/CategoryDto.java @@ -3,6 +3,7 @@ package com.no1.wms.category; import org.apache.ibatis.type.Alias; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -16,5 +17,6 @@ public class CategoryDto { private String cls_nm_2; private String cls_nm_3; private String cls_nm_4; - private boolean activation; + @Builder.Default + private Boolean activation = true; } diff --git a/src/main/java/com/no1/wms/excel/EgovWebUtil.java b/src/main/java/com/no1/wms/excel/EgovWebUtil.java new file mode 100644 index 0000000..c647c1c --- /dev/null +++ b/src/main/java/com/no1/wms/excel/EgovWebUtil.java @@ -0,0 +1,17 @@ +package com.no1.wms.excel; + +//../을 통해 부모디렉토리로 이동하는 방법을 막기 위한 보안 +public class EgovWebUtil { + public static String filePathBlackList(String value) { + String returnValue = value; + if (returnValue == null || returnValue.trim().equals("")) { + return ""; + } + + returnValue = returnValue.replaceAll("\\.\\./", ""); + returnValue = returnValue.replaceAll("\\.\\.\\\\", ""); + + return returnValue; + } + +} diff --git a/src/main/java/com/no1/wms/excel/ExcelUtils.java b/src/main/java/com/no1/wms/excel/ExcelDownlodeUtils.java similarity index 54% rename from src/main/java/com/no1/wms/excel/ExcelUtils.java rename to src/main/java/com/no1/wms/excel/ExcelDownlodeUtils.java index 32e5af0..0bd2d6d 100644 --- a/src/main/java/com/no1/wms/excel/ExcelUtils.java +++ b/src/main/java/com/no1/wms/excel/ExcelDownlodeUtils.java @@ -1,16 +1,21 @@ package com.no1.wms.excel; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.List; import javax.servlet.http.HttpServletResponse; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import com.no1.wms.category.CategoryDto; @@ -19,8 +24,12 @@ import com.no1.wms.category.CategoryDto; @Service -public class ExcelUtils { +public class ExcelDownlodeUtils { + + + + //마지막 List dto 이부분을 수정해서 만들어야함. public void downloadCategoryExcelFile(String excelFileName, HttpServletResponse response, String sheetName, String[] columnName, List dto) { String fileName = ""; @@ -32,7 +41,7 @@ public class ExcelUtils { response.setContentType("ms-vnd/excel"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";"); - Workbook workbook = new HSSFWorkbook(); + Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(sheetName); Row row = null; @@ -45,6 +54,7 @@ public class ExcelUtils { cell.setCellValue(columnName[i]); } rowNum += 1; + //수정부분 makeCategoryBody(dto,row,sheet,cell,rowNum); @@ -77,5 +87,43 @@ public class ExcelUtils { } } + //엑셀 폼 파일 다운로드 + //엑셀 폼은 직접 만들어서 src/main/webapp/excelfiles/excelform 이 경로의 폴더안에 넣으면 됩니다. + //확장자는 .xlsx로 해주세요. + //매개변수는 HttpServletResponse response와 파일명 String을 넣으면 되고 파일명은 한글도 가능합니다. + public void downlodeExcelForm(HttpServletResponse response, String formName) throws IOException { + String excelfilesDirectory = "src/main/webapp/excelfiles/excelform/"; + + + File file = new File(excelfilesDirectory+formName); + + try( FileInputStream fis = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(fis); + OutputStream out = response.getOutputStream()){ + String encodedFilename = URLEncoder.encode(formName, "UTF-8").replaceAll("\\+", "%20"); + + response.addHeader("Content-Disposition", "attachment;filename=\""+encodedFilename+"\""); + response.setContentType("application/vnd.ms-excel"); + // 응답 크기 명시 + response.setContentLength((int)file.length()); + int read = 0; + while((read = bis.read()) != -1) { + out.write(read); + } + + + }catch(IOException e) { + e.printStackTrace(); + } + + + } + + + + + + + } diff --git a/src/main/java/com/no1/wms/excel/ExcelManagerXlsx.java b/src/main/java/com/no1/wms/excel/ExcelManagerXlsx.java new file mode 100644 index 0000000..53d08e8 --- /dev/null +++ b/src/main/java/com/no1/wms/excel/ExcelManagerXlsx.java @@ -0,0 +1,110 @@ +package com.no1.wms.excel; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +public class ExcelManagerXlsx { + + private static ExcelManagerXlsx excelXlsxMng; + + public ExcelManagerXlsx() { + } + + public static ExcelManagerXlsx getInstance() { + if (excelXlsxMng == null) { + excelXlsxMng = new ExcelManagerXlsx(); + } + return excelXlsxMng; + } + + public List> getListXlsxRead(String excel) throws Exception { + + List> list = new ArrayList>(); + File file = new File( excel ); + if( !file.exists() || !file.isFile() || !file.canRead() ) { + throw new IOException( excel ); + } + XSSFWorkbook wb = new XSSFWorkbook( new FileInputStream(file) ); + + //xls시 이용 + //HSSFWorkbook wb = new HSSFWorkbook ( new FileInputStream(file) ); + + int sheetIndex = 0; + + try { + // 모든 시트 순회 + for( int i=0; i<1; i++ ) { + XSSFSheet sheet = wb.getSheetAt(sheetIndex); + for( Row row : sheet ) { + // 첫 번째 시트만 사용 + if(row.getRowNum() == 0) { + continue; + } + + HashMap hMap = new HashMap(); + String valueStr = ""; + + int cellLength = (int) row.getLastCellNum();/// + + for (int j = 0; j < row.getLastCellNum(); j++) { + Cell cell = row.getCell(j); + + if (cell == null || cell.getCellType() == CellType.BLANK) { + valueStr = ""; + }else{ + switch(cell.getCellType()){ + case STRING : + valueStr = cell.getStringCellValue(); + break; + case NUMERIC : // 날짜 형식이든 숫자 형식이든 다 CELL_TYPE_NUMERIC으로 인식함. + if(DateUtil.isCellDateFormatted(cell)){ // 날짜 유형의 데이터일 경우, + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.KOREA); + String formattedStr = dateFormat.format(cell.getDateCellValue()); + valueStr = formattedStr; + break; + }else{ //숫자 데이터일 경우, + Double numericCellValue = cell.getNumericCellValue(); + if(Math.floor(numericCellValue) == numericCellValue){ // 소수점 이하를 버린 값이 원래의 값과 같다면,, + valueStr = numericCellValue.intValue() + ""; // int형으로 소수점 이하 버리고 String으로 데이터 담는다. + }else{ + valueStr = numericCellValue + ""; + } + break; + } + case BOOLEAN : + valueStr = cell.getBooleanCellValue() + ""; + break; + } + + } + // 엑셀의 열 인덱스와 함께 값 매핑 + hMap.put("cell_"+j ,valueStr); + + } + // 결과 리스트에 추가 + list.add(hMap); + } + sheetIndex++; + } + + }catch(Exception ex){ + ex.printStackTrace(); + } + + return list; + } + +} diff --git a/src/main/java/com/no1/wms/excel/ExcelRequestManager.java b/src/main/java/com/no1/wms/excel/ExcelRequestManager.java new file mode 100644 index 0000000..fa8cf26 --- /dev/null +++ b/src/main/java/com/no1/wms/excel/ExcelRequestManager.java @@ -0,0 +1,86 @@ +package com.no1.wms.excel; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.multipart.MultipartFile; + +public class ExcelRequestManager { + + public List> parseExcelSpringMultiPart + (Map files , String KeyStr, int fileKeyParam,String atchFileId ,String storePath,HttpServletRequest request) throws Exception{ + List> list = null; + int fileKey = fileKeyParam; + + String storePathString = ""; + String atchFileIdString = ""; + + String folder = request.getServletContext().getRealPath("/"); + + if ("".equals(storePath) || storePath == null) { + storePathString = folder+"/excelfiles/upload/"; + } else { + storePathString = folder+"/excelfiles/upload/"+storePath; + } + + if (!"".equals(atchFileId) || atchFileId != null) { + atchFileIdString = atchFileId; + } + + File saveFolder = new File(EgovWebUtil.filePathBlackList(storePathString)); + + //폴더 없으면 생성 + if (!saveFolder.exists() || saveFolder.isFile()) { + saveFolder.mkdirs(); + } + + Iterator> itr = files.entrySet().iterator(); + MultipartFile file; + String filePath = ""; + + while (itr.hasNext()) { + //파일 이름을 가져오는데 파일이 여러개일 경우 + Entry entry = itr.next(); + + file = entry.getValue(); + String orginFileName = file.getOriginalFilename(); + + if ("".equals(orginFileName)) { + continue; + } + + //파일의 확장자 + int index = orginFileName.lastIndexOf("."); + String fileExt = orginFileName.substring(index + 1); + //파일의 이름 정하기 지정한 KeyStr값과 시간 값과 fileKey를 넣어서 만듬 + String newName = KeyStr + getTimeStamp() + fileKey; + + if (!"".equals(orginFileName)) { + filePath = storePathString + File.separator + newName+"."+fileExt; + file.transferTo(new File(EgovWebUtil.filePathBlackList(filePath))); + } + list = ExcelManagerXlsx.getInstance().getListXlsxRead(filePath); + + fileKey++; + } + return list; + } + + private static String getTimeStamp() { + long currentTimeMillis = System.currentTimeMillis(); + Date currentDate = new Date(currentTimeMillis); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); + String formattedTimeStamp = dateFormat.format(currentDate); + return formattedTimeStamp; + } + + +} diff --git a/src/main/webapp/WEB-INF/views/category/list.jsp b/src/main/webapp/WEB-INF/views/category/list.jsp index 7e1b41c..e65beb7 100644 --- a/src/main/webapp/WEB-INF/views/category/list.jsp +++ b/src/main/webapp/WEB-INF/views/category/list.jsp @@ -69,7 +69,9 @@
- + + +
+
+
+
+ + + + + + + + +
+ +
+
+
+
+ +
+
+
+
@@ -113,6 +137,16 @@ document.body.appendChild(form); form.submit(); }); + + $("#downlodeExcelForm").on("click",function(){ + var form = document.createElement("form"); + form.action = "/category/downlodeCategoryForm"; + form.method = "GET"; + document.body.appendChild(form); + form.submit(); + }); + + $("body").on("click", ".detailTr", function(){ var kan_code = $(this).data("kan_code"); @@ -166,8 +200,13 @@ }); - + + var msg = "${resMap.msg}"; + if (msg != "") alert(msg); + });//ready + + function pagingFunction(clickedId){ var searchn1 = $("#searchn1").val(); var search1 = $("#search1").val(); @@ -196,7 +235,27 @@ document.body.appendChild(form); form.submit(); - } + + }//pagingFunction + + + function _onSubmit(){ + + if($("#file").val() == ""){ + alert("파일을 업로드해주세요."); + $("#file").focus(); + return false; + } + + + + return true; + } + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/category/test.jsp b/src/main/webapp/WEB-INF/views/category/test.jsp index 8a91481..3e72406 100644 --- a/src/main/webapp/WEB-INF/views/category/test.jsp +++ b/src/main/webapp/WEB-INF/views/category/test.jsp @@ -5,77 +5,54 @@ test상세페이지 - + - - test success -
-
- -${item.kan_code} : ${item.cls_nm_1} > ${item.cls_nm_2} > ${item.cls_nm_3} > ${item.cls_nm_4}
-활성화 : ${item.activation}
-
-
- 코드테스트 버튼 : -
- - -
- - +
+
+
+ + + + + + + + +
+ +
+
+
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/main.jsp b/src/main/webapp/WEB-INF/views/main.jsp new file mode 100644 index 0000000..e39f901 --- /dev/null +++ b/src/main/webapp/WEB-INF/views/main.jsp @@ -0,0 +1,32 @@ +<%@ page contentType="text/html; charset=UTF-8"%> + + + + +Insert title here + + +
+
+
+ + 달력 +
+
+ + 로고 +
+
+
+
+ + 부족한 재고 +
+
+ + 개시판(공지사항) +
+
+
+ + \ No newline at end of file diff --git a/src/main/webapp/excelfiles/excelform/카테고리 데이터 입력 서식.xlsx b/src/main/webapp/excelfiles/excelform/카테고리 데이터 입력 서식.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6a42a08056ae7059847bffb1b8fed3536fd63188 GIT binary patch literal 7485 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 MzwZ}S^1nX)4|x9OZ~y=R literal 0 HcmV?d00001