diff --git a/src/main/java/com/interplug/qcast/biz/excelDown/ExcelDownService.java b/src/main/java/com/interplug/qcast/biz/excelDown/ExcelDownService.java index b45c2fb9..9b800124 100644 --- a/src/main/java/com/interplug/qcast/biz/excelDown/ExcelDownService.java +++ b/src/main/java/com/interplug/qcast/biz/excelDown/ExcelDownService.java @@ -12,7 +12,6 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -24,7 +23,7 @@ public class ExcelDownService { private final ZipFileManager zipFileManager; - @Autowired Messages message; + Messages message; @Value("${file.ini.root.path}") private String baseDirPath; diff --git a/src/main/java/com/interplug/qcast/biz/file/FileController.java b/src/main/java/com/interplug/qcast/biz/file/FileController.java new file mode 100644 index 00000000..b206bffb --- /dev/null +++ b/src/main/java/com/interplug/qcast/biz/file/FileController.java @@ -0,0 +1,58 @@ +package com.interplug.qcast.biz.file; + +import com.interplug.qcast.biz.file.dto.FileRequest; +import com.interplug.qcast.biz.object.dto.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/api/file") +@RequiredArgsConstructor +@Tag(name = "FileController", description = "파일 관련 API") +public class FileController { + + private final FileService fileService; + + @Operation(description = "파일을 다운로드 한다.") + @PostMapping("/fileDownload") + @ResponseStatus(HttpStatus.OK) + public void fileDownload( + HttpServletRequest request, + HttpServletResponse response, + @RequestBody FileRequest fileRequest) + throws Exception { + + fileService.downloadFile(request, response, fileRequest); + } + + @Operation(description = "모든 파일을 zip 다운로드 한다.") + @PostMapping("/zipFileDownload") + @ResponseStatus(HttpStatus.OK) + public void allFileDownload( + HttpServletRequest request, + HttpServletResponse response, + @RequestBody FileRequest fileRequest) + throws Exception { + + fileService.downloadZipFile(request, response, fileRequest); + } + + @Operation(description = "파일을 업로드 한다.") + @PostMapping("/fileUpload") + @ResponseStatus(HttpStatus.OK) + public Integer fileUpload( + HttpServletRequest request, HttpServletResponse response, FileRequest fileRequest) + throws Exception { + List saveFileList = fileService.getUploadFileList(request, fileRequest); + Integer resultCnt = fileService.setFile(saveFileList, null); + return resultCnt; + } +} diff --git a/src/main/java/com/interplug/qcast/biz/file/FileMapper.java b/src/main/java/com/interplug/qcast/biz/file/FileMapper.java new file mode 100644 index 00000000..27010fd8 --- /dev/null +++ b/src/main/java/com/interplug/qcast/biz/file/FileMapper.java @@ -0,0 +1,18 @@ +package com.interplug.qcast.biz.file; + +import com.interplug.qcast.biz.file.dto.FileRequest; +import com.interplug.qcast.biz.file.dto.FileResponse; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileMapper { + + Integer deleteFile(FileRequest fileDeleteReq) throws Exception; + + Integer insertFile(FileRequest fileInsertReq) throws Exception; + + FileResponse selectFile(FileRequest fileDeleteReq); + + List selectFileList(FileRequest fileDeleteReq); +} diff --git a/src/main/java/com/interplug/qcast/biz/file/FileService.java b/src/main/java/com/interplug/qcast/biz/file/FileService.java new file mode 100644 index 00000000..8df58d4e --- /dev/null +++ b/src/main/java/com/interplug/qcast/biz/file/FileService.java @@ -0,0 +1,304 @@ +package com.interplug.qcast.biz.file; + +import com.interplug.qcast.biz.file.dto.FileRequest; +import com.interplug.qcast.biz.file.dto.FileResponse; +import com.interplug.qcast.config.Exception.ErrorCode; +import com.interplug.qcast.config.Exception.QcastException; +import com.interplug.qcast.config.message.Messages; +import com.interplug.qcast.util.ZipFileManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FileService { + + Messages message; + + private final FileMapper fileMapper; + + @Value("${file.ini.root.path}") + private String baseDirPath; + + private ZipFileManager zipFileManager; + + public List getUploadFileList(HttpServletRequest request, FileRequest fileRequest) + throws Exception { + List saveFileList = new ArrayList<>(); + List multipartFileList = new ArrayList<>(); + + if (!MultipartHttpServletRequest.class.isAssignableFrom(request.getClass())) { + return new ArrayList<>(); + } + + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map> mapMultipart = multipartRequest.getMultiFileMap(); + if (mapMultipart.isEmpty()) { + return null; + } + + String strParamKey; + for (String s : mapMultipart.keySet()) { + strParamKey = s; + multipartFileList.addAll(mapMultipart.get(strParamKey)); + } + + int iFileCnt = multipartFileList.size(); + if (iFileCnt > 0) { + int iFileSeq = 1; + + String strSeparator = File.separator; + String strFileFolderPath = + baseDirPath + strSeparator + fileRequest.getObjectNo() + strSeparator; + + // planNo가 있는 경우 + if (fileRequest.getPlanNo() != null && !fileRequest.getPlanNo().isEmpty()) { + strFileFolderPath += fileRequest.getPlanNo() + strSeparator; + } + + log.info("### fileFolderPath : {}", strFileFolderPath); + + // 파일 폴더 생성 + makeFileFolder(strFileFolderPath); + + for (MultipartFile multipartFile : multipartFileList) { + if (!multipartFile.isEmpty()) { + + // 파일 확장자 발췌 및 확장자 소문자 변환 + String strFileExt = multipartFile.getOriginalFilename(); + if (strFileExt != null && !strFileExt.isEmpty()) { + strFileExt = strFileExt.substring(strFileExt.lastIndexOf(".") + 1).toLowerCase().trim(); + } else { + strFileExt = ""; + } + + // 파일 확장자 및 사이즈 검증 + List listExtension = this.getFileExtension(); + if (!validFileExtension(listExtension, strFileExt)) { + multipartFile.getInputStream().close(); + continue; + } + + String strSrcFileNm = multipartFile.getOriginalFilename(); + File file = new File(strFileFolderPath, strSrcFileNm); + + multipartFile.transferTo(file); + + FileRequest fileSaveReq = new FileRequest(); + fileSaveReq.setObjectNo(fileRequest.getObjectNo()); + fileSaveReq.setPlanNo(fileRequest.getPlanNo()); + fileSaveReq.setCategory(fileRequest.getCategory()); + fileSaveReq.setFaileName(strSrcFileNm); + fileSaveReq.setUserId(fileRequest.getUserId()); + saveFileList.add(fileSaveReq); + + iFileSeq++; + } + } + } + + return saveFileList; + } + + // 파일 확장자 검증 + private boolean validFileExtension(List listExtension, String strFileExt) + throws Exception { + boolean bResult = true; + + try { + + if (strFileExt == null || strFileExt.isEmpty()) { + return false; + } + + for (String s : listExtension) { + strFileExt = strFileExt.toLowerCase(); + + // 업로드 가능한 파일 확장자 검증 + if (s.equals(strFileExt)) { + return true; + } + } + + } catch (Exception e) { + log.error("### FileService - validFileExtension : Exception Error"); + return false; + } + + return bResult; + } + + // 업로드 가능한 파일 확장자 조회 + public List getFileExtension() { + return Arrays.asList( + "mp3", "wma", "wav", "m4a", "3gp", "hwp", "doc", "docx", "ppt", "pptx", "zip", "xls", + "xlsx", "pdf", "txt", "jpg", "gif", "png"); + } + + // 파일 폴더 생성 + private void makeFileFolder(String strMakeDir) { + try { + File dir = new File(strMakeDir.replace("//", "/")); + if (!dir.exists()) { + if (!dir.mkdirs()) { + log.error("### Failed to create directories: {}", strMakeDir); + throw new QcastException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + } catch (Exception e) { + log.error("### FileService - makeFileFolder : Exception Error"); + } + } + + // 파일정보 저장 및 삭제 + public Integer setFile(List saveFilelist, List deleteFilelist) + throws Exception { + + Integer iResult = 0; + + // 파일 삭제 + if (deleteFilelist != null && !deleteFilelist.isEmpty()) { + for (FileRequest fileDeleteReq : deleteFilelist) { + if (!"".equals(fileDeleteReq.getObjectNo())) { + fileMapper.deleteFile(fileDeleteReq); // 파일 삭제 + } + } + } + + // 파일 저장 + if (saveFilelist != null && !saveFilelist.isEmpty()) { + for (FileRequest fileInsertReq : saveFilelist) { + if (!"".equals(fileInsertReq.getObjectNo()) && !"".equals(fileInsertReq.getCategory())) { + iResult += fileMapper.insertFile(fileInsertReq); + } + } + } + + return iResult; + } + + // 파일 1건 다운로드 + public void downloadFile( + HttpServletRequest request, HttpServletResponse response, FileRequest fileRequest) + throws Exception { + + // 필수값 체크 + if (fileRequest.getObjectNo() == null || fileRequest.getObjectNo().isEmpty()) { + throw new QcastException( + ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Object No")); + } else if (fileRequest.getNo() == 0) { + throw new QcastException( + ErrorCode.INVALID_INPUT_VALUE, message.getMessage("common.message.required.data", "No")); + } + + // 파일정보 조회 + FileResponse fileResponse = fileMapper.selectFile(fileRequest); + if (fileResponse == null) { + throw new QcastException( + ErrorCode.NOT_FOUND, message.getMessage(" common.message.file.download.exists")); + } + + // 첨부파일 물리적 경로 + String strSeparator = File.separator; + String filePath = baseDirPath + strSeparator + fileResponse.getObjectNo(); + if (fileResponse.getPlanNo() != null && !fileResponse.getPlanNo().isEmpty()) { + filePath += strSeparator + fileResponse.getPlanNo(); + } + filePath += strSeparator + fileResponse.getFaileName(); + + File file = new File(filePath); + if (!file.exists()) { + log.error("### File not found: {}", filePath); + throw new QcastException(ErrorCode.INTERNAL_SERVER_ERROR); + } + + String mimeType = URLConnection.guessContentTypeFromName(file.getName()); + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + + String originalFileName = fileResponse.getFaileName(); + String encodedFileName = + URLEncoder.encode(originalFileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + + response.setHeader("Content-Transfer-Encoding", "binary;"); + response.setHeader("Pragma", "no-cache;"); + response.setHeader("Expires", "-1;"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\""); + response.setContentType(mimeType); + response.setContentLength((int) file.length()); + + try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { + FileCopyUtils.copy(inputStream, response.getOutputStream()); + } catch (IOException e) { + throw new QcastException( + ErrorCode.INTERNAL_SERVER_ERROR, + message.getMessage("common.message.file.download.error")); + } + } + + // 파일 N건 ZIP 다운로드 + public void downloadZipFile( + HttpServletRequest request, HttpServletResponse response, FileRequest fileRequest) + throws Exception { + + // 필수값 체크 + if (fileRequest.getObjectNo() == null || fileRequest.getObjectNo().isEmpty()) { + throw new QcastException( + ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Object No")); + } + + String strZipFileName = fileRequest.getZipFileName(); + if (strZipFileName == null || strZipFileName.isEmpty()) { + strZipFileName = "Download"; + } + + // 파일 조회 + List fileList = fileMapper.selectFileList(fileRequest); + if (fileList == null || fileList.size() == 0) { + throw new QcastException( + ErrorCode.NOT_FOUND, message.getMessage(" common.message.file.download.exists")); + } + + // 첨부파일 물리적 경로 + FileResponse fileResponse = fileList.get(0); + String zipFilePath = fileResponse.getObjectNo(); + String filePath = baseDirPath + File.separator + fileResponse.getObjectNo() + File.separator; + if (fileResponse.getPlanNo() != null && !fileResponse.getPlanNo().isEmpty()) { + zipFilePath += File.separator + fileResponse.getPlanNo(); + filePath += fileResponse.getPlanNo() + File.separator; + } + + String finalFilePath = filePath; + String finalZipFilePath = zipFilePath; + List> listFile = + fileList.stream() + .map( + file -> { + Map map = new HashMap<>(); + map.put("directory", finalZipFilePath); + map.put("filename", finalFilePath + file.getFaileName()); + return map; + }) + .collect(Collectors.toList()); + + // zip 파일로 변환 + zipFileManager.createZipFile(response, strZipFileName, listFile); + } +} diff --git a/src/main/java/com/interplug/qcast/biz/file/dto/FileRequest.java b/src/main/java/com/interplug/qcast/biz/file/dto/FileRequest.java new file mode 100644 index 00000000..52f125b8 --- /dev/null +++ b/src/main/java/com/interplug/qcast/biz/file/dto/FileRequest.java @@ -0,0 +1,35 @@ +package com.interplug.qcast.biz.file.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +// @Data +@Getter +@Setter +public class FileRequest { + + @Schema(description = "물건 번호") + private String objectNo; + + @Schema(description = "PLAN 번호") + private String planNo; + + @Schema(description = "파일 번호") + private int no; + + @Schema(description = "카테고리") + private String category; + + @Schema(description = "파일 이름") + private String faileName; + + @Schema(description = "사용자(등록자,수정자) ID") + private String userId; + + @Schema(description = "zip 파일 이름") + private String zipFileName; + + @Schema(description = "planNo null 체크 Flag (NULL=1, NOT NULL=0)", defaultValue = "1") + private String planNoNullChkFlg = "1"; +} diff --git a/src/main/java/com/interplug/qcast/biz/file/dto/FileResponse.java b/src/main/java/com/interplug/qcast/biz/file/dto/FileResponse.java new file mode 100644 index 00000000..1de0e94b --- /dev/null +++ b/src/main/java/com/interplug/qcast/biz/file/dto/FileResponse.java @@ -0,0 +1,41 @@ +package com.interplug.qcast.biz.file.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +// @Data +@Getter +@Setter +public class FileResponse { + + @Schema(description = "물건 번호") + private String objectNo; + + @Schema(description = "PLAN 번호") + private String planNo; + + @Schema(description = "파일 번호") + private int no; + + @Schema(description = "카테고리") + private String category; + + @Schema(description = "파일 이름") + private String faileName; + + @Schema(description = "업로드 일시") + private String uploadDatetime; + + @Schema(description = "사용자 ID") + private String userId; + + @Schema(description = "삭제 플래그") + private String delFlg; + + @Schema(description = "마지막 수정 일시") + private String lastEditDatetime; + + @Schema(description = "마지막 수정자") + private String lastEditUser; +} diff --git a/src/main/resources/mappers/file/fileMapper.xml b/src/main/resources/mappers/file/fileMapper.xml new file mode 100644 index 00000000..811f8181 --- /dev/null +++ b/src/main/resources/mappers/file/fileMapper.xml @@ -0,0 +1,101 @@ + + + + + + + + + + /* sqlid : com.interplug.qcast.biz.file.insertFile */ + INSERT INTO T_UPLOAD ( + OBJECT_NO + , NO + , CATEGORY + , FAILE_NAME + , UPLOAD__DATETIME + , USER_ID + , DEL_FLG + , LAST_EDIT_DATETIME + , LAST_EDIT_USER + , PLAN_NO + ) VALUES ( + #{objectNo} + , (SELECT COALESCE(MAX(NO) + 1, 1) FROM T_UPLOAD WHERE OBJECT_NO = #{objectNo}) + , #{category} + , #{faileName} + , GETDATE() + , #{userId} + , 0 + , GETDATE() + , #{userId} + , #{planNo} + ) + + + + /* sqlid : com.interplug.qcast.biz.file.deleteFile */ + UPDATE T_UPLOAD + SET + DEL_FLG = 1 + , LAST_EDIT_DATETIME = GETDATE() + , LAST_EDIT_USER = #{userId} + WHERE OBJECT_NO = #{objectNo} + + AND PLAN_NO = #{planNo} + + + AND NO = #{no} + + + + \ No newline at end of file