diff --git a/DB-DDL/maria/ddl/ibmsdb/seq_crdn_file_id.sql b/DB-DDL/maria/ddl/ibmsdb/seq_crdn_file_id.sql new file mode 100644 index 0000000..b6cfd9f --- /dev/null +++ b/DB-DDL/maria/ddl/ibmsdb/seq_crdn_file_id.sql @@ -0,0 +1,7 @@ +CREATE SEQUENCE seq_crdn_file_id + START WITH 1 + INCREMENT BY 1 + MINVALUE 1 + MAXVALUE 9999999999999999 + CACHE 1000 + NOCYCLE; diff --git a/DB-DDL/maria/ddl/ibmsdb/tb_crdn_file.sql b/DB-DDL/maria/ddl/ibmsdb/tb_crdn_file.sql new file mode 100644 index 0000000..0d15a05 --- /dev/null +++ b/DB-DDL/maria/ddl/ibmsdb/tb_crdn_file.sql @@ -0,0 +1,16 @@ +create table tb_crdn_file +( + FILE_ID varchar(20) not null comment '파일 ID' + primary key, + CRDN_YR char(4) null comment '단속 연도', + CRDN_NO varchar(6) null comment '단속 번호', + ORIGINAL_FILE_NM varchar(200) not null comment '원본 파일명', + STORED_FILE_NM varchar(200) not null comment '저장 파일명', + FILE_PATH varchar(200) not null comment '파일 경로', + FILE_SIZE bigint not null comment '파일 크기', + FILE_EXT varchar(10) not null comment '파일 확장자', + REG_DTTM datetime null comment '등록 일시', + RGTR varchar(20) null comment '등록자' +) + comment '단속관련 파일 파일'; + diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java index 4acc776..6ea063f 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java @@ -1,14 +1,19 @@ package go.kr.project.crdn.crndRegistAndView.main.controller; +import egovframework.constant.FileContentTypeConstants; import egovframework.constant.MessageConstants; import egovframework.constant.TilesConstants; import egovframework.exception.MessageException; import egovframework.util.ApiResponseUtil; +import egovframework.util.FileUtil; import egovframework.util.SessionUtil; import egovframework.util.excel.ExcelSheetData; import egovframework.util.excel.SxssfExcelFile; import go.kr.project.common.model.CmmnCodeSearchVO; +import go.kr.project.common.model.FileVO; import go.kr.project.common.service.CommonCodeService; +import go.kr.project.crdn.crndRegistAndView.main.mapper.CrdnFileMapper; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnFileVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelGridVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; @@ -21,14 +26,18 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.util.List; /** @@ -50,6 +59,9 @@ import java.util.List; @Tag(name = "단속 등록/조회", description = "단속 등록/조회 관련 API") public class CrdnRegistAndViewController { + @Value("${file.upload.max-files:10}") + private int maxFiles; + /** 단속 서비스 */ private final CrdnRegistAndViewService service; @@ -58,6 +70,10 @@ public class CrdnRegistAndViewController { private final CommonCodeService commonCodeService; + private final CrdnFileMapper crdnFileMapper; + + private final FileUtil fileUtil; + /** * 단속 목록 화면을 제공하고, 검색 조건에 사용될 공통코드를 모델에 추가한다. * @param model 뷰에 전달할 데이터를 담는 모델 객체 @@ -208,7 +224,16 @@ public class CrdnRegistAndViewController { .sortAscending(true) .build(); model.addAttribute("dsclMthdCdList", commonCodeService.selectCodeDetailList(dsclMthdCdSearchVO)); - + + CrdnFileVO paramVO = CrdnFileVO.builder() + .crdnYr(crdnYr) + .crdnNo(crdnNo) + .build(); + model.addAttribute("crdnFileList", crdnFileMapper.selectCrdnFileList(paramVO)); + + // 최대 파일 개수 전달 + model.addAttribute("maxFiles", 5); + return "crdn/crndRegistAndView/main/detailView-main" + TilesConstants.BASE; } @@ -242,8 +267,9 @@ public class CrdnRegistAndViewController { /** * 단속 정보를 수정합니다. - * + * * @param paramVO 수정할 단속 정보를 담은 VO 객체 + * @param request 파일 업로드를 위한 MultipartHttpServletRequest 객체 * @return 수정 결과와 성공 상태를 담은 ResponseEntity 객체 */ @Operation(summary = "단속 수정", description = "기존 단속 정보를 수정합니다.") @@ -253,11 +279,14 @@ public class CrdnRegistAndViewController { @ApiResponse(description = "오류로 인한 실패") }) @PostMapping("/update.ajax") - public ResponseEntity update(@ModelAttribute CrdnRegistAndViewVO paramVO) { + public ResponseEntity update(@ModelAttribute CrdnRegistAndViewVO paramVO, MultipartHttpServletRequest request) { log.debug("단속 정보 수정 요청 - 단속연도: {}, 단속번호: {}", paramVO.getCrdnYr(), paramVO.getCrdnNo()); - - int result = service.update(paramVO); - + + // 파일 목록 가져오기 + List fileList = request.getFiles("files"); + + int result = service.updateWithFiles(paramVO, fileList); + if (result > 0) { return ApiResponseUtil.success(MessageConstants.Common.UPDATE_SUCCESS); } else { @@ -467,5 +496,70 @@ public class CrdnRegistAndViewController { } } + /** + * 첨부파일 목록을 조회합니다. + * + * @param crdnYr 단속 연도 + * @param crdnNo 단속 번호 + * @return 첨부파일 목록과 성공 상태를 담은 ResponseEntity 객체 + */ + @Operation(summary = "첨부파일 목록 조회", description = "단속의 첨부파일 목록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "첨부파일 목록 조회 성공"), + @ApiResponse(responseCode = "400", description = "첨부파일 목록 조회 실패"), + @ApiResponse(description = "오류로 인한 실패") + }) + @GetMapping("/fileList.ajax") + public ResponseEntity fileListAjax(@RequestParam String crdnYr, @RequestParam String crdnNo) { + CrdnFileVO paramVO = CrdnFileVO.builder() + .crdnYr(crdnYr) + .crdnNo(crdnNo) + .build(); + List fileList = crdnFileMapper.selectCrdnFileList(paramVO); + + return ApiResponseUtil.success(fileList, "첨부파일 목록 조회가 완료되었습니다."); + } + + /** + * 파일을 삭제합니다. + * + * @param fileId 삭제할 파일의 ID + * @return 삭제 결과와 성공 상태를 담은 ResponseEntity 객체 + */ + @Operation(summary = "첨부 파일 삭제", description = "단속의 첨부 파일을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "첨부 파일 삭제 성공"), + @ApiResponse(responseCode = "400", description = "첨부 파일 삭제 실패"), + @ApiResponse(description = "오류로 인한 실패") + }) + @PostMapping("/deleteFile.ajax") + public ResponseEntity deleteFileAjax(@RequestParam String fileId) { + int result = service.deleteCrdnFile(fileId); + + if (result > 0) { + return ApiResponseUtil.success("파일이 성공적으로 삭제되었습니다."); + } else { + return ApiResponseUtil.error("파일 삭제에 실패했습니다."); + } + } + + /** + * 파일을 다운로드합니다. + * + * @param fileId 다운로드할 파일의 ID + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + */ + @Operation(summary = "첨부 파일 다운로드", description = "단속의 첨부 파일을 다운로드합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "첨부 파일 다운로드 성공"), + @ApiResponse(responseCode = "400", description = "첨부 파일 다운로드 실패"), + @ApiResponse(description = "오류로 인한 실패") + }) + @GetMapping("/download.do") + public void downloadFile(@RequestParam String fileId, HttpServletRequest request, HttpServletResponse response) { + // 파일 다운로드 처리 - 서비스 계층으로 위임 + service.downloadCrdnFile(fileId, request, response); + } } \ No newline at end of file diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnFileMapper.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnFileMapper.java new file mode 100644 index 0000000..9f81e9a --- /dev/null +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnFileMapper.java @@ -0,0 +1,68 @@ +package go.kr.project.crdn.crndRegistAndView.main.mapper; + +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnFileVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * packageName : go.kr.project.crdn.crndRegistAndView.main.mapper + * fileName : CrdnFileMapper + * author : 시스템 관리자 + * date : 2025-11-14 + * description : 단속 첨부파일 Mapper 인터페이스 + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 2025-11-14 시스템 관리자 최초 생성 + */ +@Mapper +public interface CrdnFileMapper { + + /** + * 파일 정보를 조회합니다. + * + * @param fileId 파일 ID + * @return 파일 정보 + */ + CrdnFileVO selectCrdnFile(String fileId); + + /** + * 단속에 첨부된 파일 목록을 조회합니다. + * + * @param vo 단속 정보 (crdnYr, crdnNo) + * @return 파일 목록 + */ + List selectCrdnFileList(CrdnFileVO vo); + + /** + * 파일 ID를 생성합니다. + * + * @return CRDF00000001 형태의 파일 ID + */ + String generateFileId(); + + /** + * 파일 정보를 등록합니다. + * + * @param vo 등록할 파일 정보를 담은 VO 객체 + * @return 등록된 행의 수 + */ + int insertCrdnFile(CrdnFileVO vo); + + /** + * 단속에 첨부된 파일 정보를 삭제합니다. + * + * @param vo 단속 정보 (crdnYr, crdnNo) + * @return 삭제된 행의 수 + */ + int deleteCrdnFileByCrdn(CrdnFileVO vo); + + /** + * 파일 정보를 삭제합니다. + * + * @param fileId 파일 ID + * @return 삭제된 행의 수 + */ + int deleteCrdnFile(String fileId); +} diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnFileVO.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnFileVO.java new file mode 100644 index 0000000..9e6a902 --- /dev/null +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnFileVO.java @@ -0,0 +1,83 @@ +package go.kr.project.crdn.crndRegistAndView.main.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +/** + * packageName : go.kr.project.crdn.crndRegistAndView.main.model + * fileName : CrdnFileVO + * author : 시스템 관리자 + * date : 2025-11-14 + * description : 단속 첨부파일 VO 클래스 + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 2025-11-14 시스템 관리자 최초 생성 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class CrdnFileVO { + + /** 파일 ID */ + @Size(max = 20) + private String fileId; + + /** 단속 연도 */ + @Size(max = 4) + @NotNull + private String crdnYr; + + /** 단속 번호 */ + @Size(max = 10) + @NotNull + private String crdnNo; + + /** 원본 파일명 */ + @Size(max = 200) + @NotNull + private String originalFileNm; + + /** 저장 파일명 */ + @Size(max = 200) + @NotNull + private String storedFileNm; + + /** 파일 경로 */ + @Size(max = 200) + @NotNull + private String filePath; + + /** 파일 크기 */ + @NotNull + private Long fileSize; + + /** 파일 확장자 */ + @Size(max = 10) + @NotNull + private String fileExt; + + /** 등록 일시 */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime regDttm; + + /** 등록자 */ + @Size(max = 10) + private String rgtr; + + // ==================== 추가 필드 (표시용) ==================== + + /** 파일 크기 표시용 (KB, MB 등) */ + private String fileSizeStr; + + /** 다운로드 URL */ + private String downloadUrl; +} diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java index 729901c..35220a3 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java @@ -108,4 +108,30 @@ public interface CrdnRegistAndViewService { */ List selectListForExcel(CrdnRegistAndViewVO vo); + /** + * 단속 정보를 첨부파일과 함께 수정합니다. + * + * @param vo 수정할 단속 정보를 담은 VO 객체 + * @param files 업로드할 파일 목록 + * @return 수정된 행의 수 + */ + int updateWithFiles(CrdnRegistAndViewVO vo, java.util.List files); + + /** + * 단속 첨부파일을 삭제합니다. + * + * @param fileId 삭제할 파일 ID + * @return 삭제된 행의 수 + */ + int deleteCrdnFile(String fileId); + + /** + * 파일을 다운로드합니다. + * + * @param fileId 다운로드할 파일의 ID + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + */ + void downloadCrdnFile(String fileId, javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response); + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java index c697751..7e6f236 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java @@ -1,10 +1,15 @@ package go.kr.project.crdn.crndRegistAndView.main.service.impl; import egovframework.constant.CrdnPrcsSttsConstants; +import egovframework.constant.FileContentTypeConstants; import egovframework.exception.MessageException; +import egovframework.util.FileUtil; import egovframework.util.SessionUtil; import egovframework.util.StringUtil; +import go.kr.project.common.model.FileVO; +import go.kr.project.crdn.crndRegistAndView.main.mapper.CrdnFileMapper; import go.kr.project.crdn.crndRegistAndView.main.mapper.CrdnRegistAndViewMapper; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnFileVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelGridVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; @@ -13,8 +18,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import static egovframework.constant.SEQConstants.SEQ_CRDN; @@ -38,6 +49,8 @@ import static egovframework.constant.SEQConstants.SEQ_CRDN; public class CrdnRegistAndViewServiceImpl extends EgovAbstractServiceImpl implements CrdnRegistAndViewService { private final CrdnRegistAndViewMapper mapper; + private final CrdnFileMapper crdnFileMapper; + private final FileUtil fileUtil; /** * 단속 목록을 조회합니다. @@ -218,18 +231,18 @@ public class CrdnRegistAndViewServiceImpl extends EgovAbstractServiceImpl implem * @throws MessageException 필수값 누락 시 발생 */ private void validateRequiredFields(CrdnRegistAndViewVO vo) { - if (vo.getRgnSeCd() == null || vo.getRgnSeCd().trim().isEmpty()) { + /*if (vo.getRgnSeCd() == null || vo.getRgnSeCd().trim().isEmpty()) { log.warn("단속 작업 실패 - 지역구분코드 미입력"); throw new MessageException("지역구분은 필수값입니다."); - } + }*/ if (vo.getDsclMthdCd() == null || vo.getDsclMthdCd().trim().isEmpty()) { log.warn("단속 작업 실패 - 적발방법코드 미입력"); throw new MessageException("적발방법은 필수값입니다."); } - if (vo.getRelevyYn() == null || vo.getRelevyYn().trim().isEmpty()) { + /*if (vo.getRelevyYn() == null || vo.getRelevyYn().trim().isEmpty()) { log.warn("단속 작업 실패 - 재부과여부 미입력"); throw new MessageException("재부과여부는 필수값입니다."); - } + }*/ } /** @@ -316,4 +329,158 @@ public class CrdnRegistAndViewServiceImpl extends EgovAbstractServiceImpl implem log.debug("엑셀 다운로드용 단속 목록 조회 완료 - 조회 건수: {}", list.size()); return list; } + + /** + * 단속 정보를 첨부파일과 함께 수정합니다. + * + * @param vo 수정할 단속 정보를 담은 VO 객체 + * @param files 업로드할 파일 목록 + * @return 수정된 행의 수 + */ + @Override + @Transactional + public int updateWithFiles(CrdnRegistAndViewVO vo, List files) { + log.debug("단속 정보 파일 포함 수정 - 단속연도: {}, 단속번호: {}", vo.getCrdnYr(), vo.getCrdnNo()); + + // 단속 정보 수정 + int result = update(vo); + + // 파일 업로드 처리 + if (files != null && !files.isEmpty()) { + uploadCrdnFiles(files, vo.getCrdnYr(), vo.getCrdnNo(), SessionUtil.getUserId()); + } + + return result; + } + + /** + * 여러 파일을 업로드하고 DB에 정보를 등록합니다. (내부 사용 메소드) + * + * @param files 업로드할 파일 목록 + * @param crdnYr 단속 연도 + * @param crdnNo 단속 번호 + * @param rgtr 등록자 + * @return 등록된 파일 정보 목록 + */ + private List uploadCrdnFiles(List files, String crdnYr, String crdnNo, String rgtr) { + List result = new ArrayList<>(); + + if (files == null || files.isEmpty()) { + return result; + } + + // 빈 파일 제거 + List validFiles = new ArrayList<>(); + for (MultipartFile file : files) { + if (!file.isEmpty()) { + validFiles.add(file); + } + } + + if (validFiles.isEmpty()) { + return result; + } + + // FileUtil을 사용하여 파일 업로드 + List uploadedFiles; + try { + // 설정 파일에서 하위 디렉토리 경로 가져오기 + String subDir = fileUtil.getSubDir("crdn-file"); + uploadedFiles = fileUtil.uploadFiles(validFiles, subDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + + for (FileVO uploadedFile : uploadedFiles) { + // 파일 ID 생성 + String fileId = crdnFileMapper.generateFileId(); + + // 파일 정보 VO 생성 + CrdnFileVO fileVO = CrdnFileVO.builder() + .fileId(fileId) + .crdnYr(crdnYr) + .crdnNo(crdnNo) + .originalFileNm(uploadedFile.getOriginalFileNm()) + .storedFileNm(uploadedFile.getStoredFileNm()) + .filePath(uploadedFile.getFilePath()) + .fileSize(uploadedFile.getFileSize()) + .fileExt(uploadedFile.getFileExt()) + .rgtr(rgtr) + .build(); + + // 파일 정보 DB 등록 + crdnFileMapper.insertCrdnFile(fileVO); + + result.add(fileVO); + } + + return result; + } + + /** + * 단속 첨부파일을 삭제합니다. + * + * @param fileId 삭제할 파일 ID + * @return 삭제된 행의 수 + */ + @Override + @Transactional + public int deleteCrdnFile(String fileId) { + // 파일 정보 조회 + CrdnFileVO fileVO = crdnFileMapper.selectCrdnFile(fileId); + int result = crdnFileMapper.deleteCrdnFile(fileId); + + if (result > 0 && fileVO != null) { + // FileVO로 변환 + FileVO fileInfo = new FileVO(); + fileInfo.setOriginalFileNm(fileVO.getOriginalFileNm()); + fileInfo.setStoredFileNm(fileVO.getStoredFileNm()); + fileInfo.setFilePath(fileVO.getFilePath()); + fileInfo.setFileSize(fileVO.getFileSize()); + fileInfo.setFileExt(fileVO.getFileExt()); + + // 실제 파일 삭제 + fileUtil.deleteFile(fileInfo); + } + + return result; + } + + /** + * 파일을 다운로드합니다. + * + * @param fileId 다운로드할 파일의 ID + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + */ + @Override + public void downloadCrdnFile(String fileId, HttpServletRequest request, HttpServletResponse response) { + // 파일 정보 조회 + CrdnFileVO fileVO = crdnFileMapper.selectCrdnFile(fileId); + + if (fileVO == null) { + throw new RuntimeException("파일 정보를 찾을 수 없습니다: " + fileId); + } + + // FileVO로 변환 + FileVO fileInfo = new FileVO(); + fileInfo.setOriginalFileNm(fileVO.getOriginalFileNm()); + fileInfo.setStoredFileNm(fileVO.getStoredFileNm()); + fileInfo.setFilePath(fileVO.getFilePath()); + fileInfo.setFileSize(fileVO.getFileSize()); + fileInfo.setFileExt(fileVO.getFileExt()); + + // ContentType 설정 (파일 확장자에 따라 MIME 타입 추정) + String fileExt = fileVO.getFileExt().toLowerCase(); + String contentType = FileContentTypeConstants.getContentType(fileExt); + + fileInfo.setContentType(contentType); + + // FileUtil을 사용하여 파일 다운로드 + try { + fileUtil.downloadFile(fileInfo, request, response); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 596b166..13e2057 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -145,7 +145,7 @@ file: path: d:/data/@projectName@/file max-size: 10 # 단일 파일 최대 크기 (MB) max-total-size: 100 # 총 파일 최대 크기 (MB) - max-files: 20 # 최대 파일 개수 + max-files: 10 # 최대 파일 개수 allowed-extensions: hwp,jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip real-file-delete: true # 실제 파일 삭제 여부 sub-dirs: @@ -154,6 +154,7 @@ file: html-editor: common/html_editor # HTML 에디터 파일 저장 경로 crdn-act-photo: crdn-act-photo # 단속행위 사진 crdn-actn-photo: crdn-actn-photo # 단속행위 조치 사진 + crdn-file: crdn-file # 단속 첨부파일 # Juso API configuration juso: diff --git a/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnFileMapper_maria.xml b/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnFileMapper_maria.xml new file mode 100644 index 0000000..461b665 --- /dev/null +++ b/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnFileMapper_maria.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + INSERT INTO tb_crdn_file ( + FILE_ID, + CRDN_YR, + CRDN_NO, + ORIGINAL_FILE_NM, + STORED_FILE_NM, + FILE_PATH, + FILE_SIZE, + FILE_EXT, + REG_DTTM, + RGTR + ) VALUES ( + #{fileId}, + #{crdnYr}, + #{crdnNo}, + #{originalFileNm}, + #{storedFileNm}, + #{filePath}, + #{fileSize}, + #{fileExt}, + NOW(), + #{rgtr} + ) + + + + + DELETE FROM + tb_crdn_file + WHERE + CRDN_YR = #{crdnYr} + AND CRDN_NO = #{crdnNo} + + + + + DELETE FROM + tb_crdn_file + WHERE + FILE_ID = #{fileId} + + + diff --git a/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/detailView-main.jsp b/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/detailView-main.jsp index 384abb1..3e18d80 100644 --- a/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/detailView-main.jsp +++ b/src/main/webapp/WEB-INF/views/crdn/crndRegistAndView/main/detailView-main.jsp @@ -22,7 +22,7 @@
  • 기본정보
  • -
    + @@ -84,10 +84,50 @@ 비고 - + + + 첨부파일 추가 + +
    +
    +
    + attach_file + 최대 ${maxFiles}개의 파일을 첨부할 수 있습니다. +
    + +
    +
    + +
    + + +
    +

    첨부된 파일 목록

    +
      + +
    • +
      + insert_drive_file + ${file.originalFileNm} + (${file.fileSizeStr != null ? file.fileSizeStr : file.fileSize += ' bytes'}) +
      + +
    • +
      +
    +
    +
    +
    + +
    @@ -261,30 +301,24 @@ var self = this; if (!this.validate()) return; - - // 폼 데이터 수집 - var formData = { - crdnYr: $('#crdnYr').val(), - crdnNo: $('#crdnNo').val(), - rgnSeCd: $('#rgnSeCd').val(), - dsclMthdCd: $('#dsclMthdCd').val(), - dsclYmd: $('#dsclYmd').val().replace(/-/g, ''), - exmnr: $('#exmnr').val(), - relevyYn: $('#relevyYn').val(), - agrvtnLevyTrgtYn: $('#agrvtnLevyTrgtYn').val(), - rmrk: $('#rmrk').val() - }; - + + // FormData 사용 (파일 업로드 지원) + var formData = new FormData(document.getElementById('crdnForm')); + if (confirm('단속 정보를 저장하시겠습니까?')) { $.ajax({ url: '', type: 'POST', data: formData, + processData: false, + contentType: false, success: function(response) { if (response && response.success) { alert('단속 정보가 성공적으로 저장되었습니다.'); - // 현재 데이터 업데이트 - self.currentData = formData; + // 동적으로 추가된 파일 입력 필드 초기화 + $('#dynamic_file_list').empty(); + // 파일 목록 새로고침 + self.loadFileList(); // 그리드 새로고침 self.refreshAllGrids(); } else { @@ -306,12 +340,12 @@ if (isValid) { // 지역구분 검증 - if (!$.trim($('#rgnSeCd').val())) { + /*if (!$.trim($('#rgnSeCd').val())) { var rgnSeElement = document.getElementById('rgnSeCd'); errorElementCreate(rgnSeElement, '지역구분을 선택하세요.', false); $('#rgnSeCd').focus(); return false; - } + }*/ // 적발방법 검증 if (!$.trim($('#dsclMthdCd').val())) { @@ -323,12 +357,12 @@ // 재부과여부 검증 - if (!$.trim($('#relevyYn').val())) { + /*if (!$.trim($('#relevyYn').val())) { var relevyElement = document.getElementById('relevyYn'); errorElementCreate(relevyElement, '재부과여부를 선택하세요.', false); $('#relevyYn').focus(); return false; - } + }*/ // 비고 글자수 검증 (varchar(1000) 제한) var rmrk = $.trim($('#rmrk').val()); @@ -496,6 +530,39 @@ } }); + // 파일 삭제 버튼 클릭 이벤트 (기존 파일) + $(document).on('click', '.btn_delete_file', function(event) { + // 이벤트 버블링 방지 - 파일 입력 필드 클릭 이벤트가 실행되지 않도록 함 + event.stopPropagation(); + event.preventDefault(); + + var fileId = $(this).data('file-id'); + if (fileId) { + // 기존 파일 삭제 + self.deleteFile(fileId); + } else { + // 새로 추가된 파일 입력 필드 삭제 + $(this).closest('.file-input-row').remove(); + self.updateFileCount(); + } + }); + + // 파일 추가 버튼 클릭 이벤트 + $('#btnAddFile').on('click', function() { + self.addFileInput(); + }); + + // 파일 다운로드 클릭 이벤트 (동적으로 추가된 요소에 대응) + $(document).on('click', '.file-download-link', function(event) { + event.preventDefault(); + event.stopPropagation(); + + var fileId = $(this).data('file-id'); + if (fileId) { + self.downloadFile(fileId); + } + }); + }, /** @@ -550,21 +617,231 @@ } }, + /** + * 파일 입력 필드 추가 + */ + addFileInput: function() { + var self = this; + + // 현재 파일 입력 필드 개수 확인 + var currentFileCount = $('.file-input-row').length, + // 기존 첨부 파일 개수 확인 (수정 모드일 경우) + existingFileCount = $('.file-item').length, + maxFiles = parseInt('${maxFiles}', 5); + // 최대 파일 개수 제한 (기존 파일 + 새 파일) + if (currentFileCount + existingFileCount >= maxFiles) { + alert('최대 ' + maxFiles + '개의 파일만 첨부할 수 있습니다. (현재 기존 파일 ' + existingFileCount + '개)'); + return; + } + + // 파일 입력 필드 생성 + var fileInputHtml = + '
    ' + + ' ' + + ' 파일을 선택하세요' + + ' ' + + '
    '; + + // 파일 입력 필드 추가 + var $newFileRow = $(fileInputHtml); + $('#dynamic_file_list').append($newFileRow); + + // 파일 선택 이벤트 핸들러 추가 + $newFileRow.find('.file_input').on('change', function() { + self.handleFileSelect(this); + }); + + // 파일 개수 업데이트 + this.updateFileCount(); + }, + + /** + * 파일 개수 업데이트 + */ + updateFileCount: function() { + var currentFileCount = $('.file-input-row').length, + // 기존 첨부 파일 개수 확인 (수정 모드일 경우) + existingFileCount = $('.file-item').length, + maxFiles = parseInt('${maxFiles}', 5); + + // 파일 추가 버튼 활성화/비활성화 (기존 파일 + 새 파일) + if (currentFileCount + existingFileCount >= maxFiles) { + $('#btnAddFile').addClass('disabled').prop('disabled', true); + } else { + $('#btnAddFile').removeClass('disabled').prop('disabled', false); + } + }, + + /** + * 파일 선택 처리 + * 선택된 파일명을 화면에 표시합니다. + * + * @param {HTMLInputElement} fileInput 파일 입력 요소 + */ + handleFileSelect: function(fileInput) { + var $fileInput = $(fileInput); + var $fileRow = $fileInput.closest('.file-input-row'); + var $fileText = $fileRow.find('.file-input-text'); + + // 선택된 파일이 있는지 확인 + if (fileInput.files && fileInput.files.length > 0) { + var fileName = fileInput.files[0].name; + var fileSize = fileInput.files[0].size; + + // 파일 크기를 읽기 쉬운 형태로 변환 + var fileSizeText = this.formatFileSize(fileSize); + + // 파일명과 크기를 표시 + $fileText.html('insert_drive_file' + + '' + fileName + '' + + '(' + fileSizeText + ')'); + $fileText.attr('title', fileName); + } else { + // 파일이 선택되지 않은 경우 기본 텍스트 표시 + $fileText.text('파일을 선택하세요'); + $fileText.removeAttr('title'); + } + }, + + /** + * 파일 크기를 읽기 쉬운 형태로 변환 + * + * @param {number} bytes 파일 크기 (바이트) + * @returns {string} 변환된 파일 크기 문자열 + */ + formatFileSize: function(bytes) { + if (bytes === 0) return '0 Bytes'; + + var k = 1024; + var sizes = ['Bytes', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + /** + * 파일 목록 로딩 + */ + loadFileList: function() { + var self = this; + var crdnYr = '${crdnYr}'; + var crdnNo = '${crdnNo}'; + + $.ajax({ + url: '', + type: 'GET', + data: { + crdnYr: crdnYr, + crdnNo: crdnNo + }, + success: function(response) { + if (response && response.success) { + self.renderFileList(response.data); + } + } + }); + }, + + /** + * 파일 목록 렌더링 + */ + renderFileList: function(fileList) { + var $container = $('.file-list-container'); + + if (!fileList || fileList.length === 0) { + $container.remove(); + return; + } + + var html = '
    ' + + '

    첨부된 파일 목록

    ' + + '
      '; + + for (var i = 0; i < fileList.length; i++) { + var file = fileList[i]; + html += '
    • ' + + '
      ' + + ' insert_drive_file' + + ' ' + file.originalFileNm + '' + + ' (' + (file.fileSizeStr || (file.fileSize + ' bytes')) + ')' + + '
      ' + + ' ' + + '
    • '; + } + + html += '
    '; + + // 기존 파일 목록 제거 후 새로 추가 + $container.remove(); + $('.file-upload-list').after(html); + + // 파일 개수 업데이트 + this.updateFileCount(); + }, + + /** + * 파일 삭제 + * + * @param {string} fileId 삭제할 파일 ID + */ + deleteFile: function(fileId) { + var self = this; + + if (confirm('파일을 삭제하시겠습니까?')) { + $.ajax({ + url: '', + type: 'POST', + data: { fileId: fileId }, + success: function(response) { + if (response.success) { + alert('파일이 성공적으로 삭제되었습니다.'); + // 파일 목록에서 해당 파일 항목 제거 + $('[data-file-id="' + fileId + '"]').closest('li').remove(); + // 파일 개수 업데이트 및 파일 추가 버튼 상태 갱신 + self.updateFileCount(); + } else { + alert(response.message || '파일 삭제에 실패했습니다.'); + } + }, + error: function(xhr, status, error) { + // 에러 처리는 xit-common.js의 ajaxError에서 처리됨 + } + }); + } + }, + + /** + * 파일 다운로드 + * + * @param {string} fileId 다운로드할 파일 ID + */ + downloadFile: function(fileId) { + var downloadUrl = '?fileId=' + encodeURIComponent(fileId); + window.location.href = downloadUrl; + }, + /** * 모듈 초기화 */ init: function() { var self = this; - + // 중요로직: 소유자 선택/제거 버튼 초기 비활성화 설정 this.updateOwnrButtonsState(false); - + // 이벤트 핸들러 설정 this.eventBindEvents(); - + // 단속 데이터 로딩 this.loadCrdnData(); + // 파일 목록 로딩 + this.loadFileList(); + // 팝업에서 선택된 조사원 정보 수신 콜백 설정 (한글 주석: 팝업 → 부모창 데이터 전달 수신) window.onExmnrSelected = function(selectedExmnrs) { if (selectedExmnrs && selectedExmnrs.length > 0) {