From 40d8518a50b0fc70551a8887b0926158f82689d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Fri, 5 Dec 2025 13:06:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AF=B8=ED=95=84=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A7=84=ED=96=89=EC=A4=91....?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/VmisCarBassMatterInqireMapper.java | 4 +- .../api/mapper/VmisCarLedgerFrmbkMapper.java | 4 +- .../VmisCarBassMatterInqireLogService.java | 4 +- .../service/VmisCarLedgerFrmbkLogService.java | 4 +- ...VmisCarBassMatterInqireLogServiceImpl.java | 1 + .../VmisCarLedgerFrmbkLogServiceImpl.java | 1 + .../config/CarFfnlgPrnParseConfig.java | 109 +++ .../CarFfnlgTrgtIncmpController.java | 572 +++++++++++ .../CarFfnlgTrgtIncmpController.java.bak | 572 +++++++++++ .../mapper/CarFfnlgTrgtIncmpMapper.java | 90 ++ .../model/CarFfnlgTrgtIncmpExcelVO.java | 100 ++ .../CarFfnlgTrgtIncmpModifiedDataVO.java | 23 + .../model/CarFfnlgTrgtIncmpVO.java | 82 ++ .../service/CarFfnlgTrgtIncmpService.java | 99 ++ .../service/ComparisonOmService.java | 30 + .../impl/CarFfnlgTrgtIncmpServiceImpl.java | 743 +++++++++++++++ .../impl/ComparisonOmRemarkBuilder.java | 279 ++++++ .../service/impl/ComparisonOmServiceImpl.java | 94 ++ .../AbstractComparisonOmChecker.java | 98 ++ .../impl/om_checker/ComparisonOmChecker.java | 20 + .../OwnerCloseWithin31OmChecker.java | 236 +++++ .../om_checker/OwnerLevyOver31OmChecker.java | 236 +++++ .../ProductCloseWithin31OmChecker.java | 233 +++++ .../ProductLevyOver31OmChecker.java | 235 +++++ .../om_checker/ProductUseOmChangeChecker.java | 229 +++++ .../impl/om_checker/ProductUseOmChecker.java | 224 +++++ .../impl/om_checker/TransferOmChecker.java | 165 ++++ src/main/resources/application.yml | 38 + .../api/CarBassMatterInqireMapper_maria.xml | 2 +- .../mapper/api/CarLedgerFrmbkMapper_maria.xml | 2 +- .../CarFfnlgTrgtIncmpMapper_maria.xml | 309 ++++++ .../registration/list.jsp | 2 +- .../registrationOm/list.jsp | 901 ++++++++++++++++++ .../registrationOm/uploadPopup.jsp | 163 ++++ 34 files changed, 5893 insertions(+), 11 deletions(-) create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/config/CarFfnlgPrnParseConfig.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java.bak create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/mapper/CarFfnlgTrgtIncmpMapper.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpExcelVO.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpModifiedDataVO.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpVO.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/CarFfnlgTrgtIncmpService.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/ComparisonOmService.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/CarFfnlgTrgtIncmpServiceImpl.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmRemarkBuilder.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmServiceImpl.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/AbstractComparisonOmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ComparisonOmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerCloseWithin31OmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerLevyOver31OmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductCloseWithin31OmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductLevyOver31OmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChangeChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChecker.java create mode 100644 src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/TransferOmChecker.java create mode 100644 src/main/resources/mybatis/mapper/carInspectionPenalty/registrationOm/CarFfnlgTrgtIncmpMapper_maria.xml create mode 100644 src/main/webapp/WEB-INF/views/carInspectionPenalty/registrationOm/list.jsp create mode 100644 src/main/webapp/WEB-INF/views/carInspectionPenalty/registrationOm/uploadPopup.jsp diff --git a/src/main/java/go/kr/project/api/mapper/VmisCarBassMatterInqireMapper.java b/src/main/java/go/kr/project/api/mapper/VmisCarBassMatterInqireMapper.java index 55243d1..fe38396 100644 --- a/src/main/java/go/kr/project/api/mapper/VmisCarBassMatterInqireMapper.java +++ b/src/main/java/go/kr/project/api/mapper/VmisCarBassMatterInqireMapper.java @@ -53,10 +53,10 @@ public interface VmisCarBassMatterInqireMapper { CarBassMatterInqireVO selectCarBassMatterInqireById(String carBassMatterInqire); /** - * txId로 CAR_FFNLG_TRGT_ID를 업데이트합니다. + * txId로 CAR_FFNLG_TRGT_ID를 업데이트합니다. (지연/미필 공통) * * @param txId 트랜잭션 ID - * @param carFfnlgTrgtId 과태료 대상 ID + * @param carFfnlgTrgtId 과태료 대상 ID (지연 또는 미필) * @return 업데이트된 행 수 */ int updateCarFfnlgTrgtIdByTxId(String txId, String carFfnlgTrgtId); diff --git a/src/main/java/go/kr/project/api/mapper/VmisCarLedgerFrmbkMapper.java b/src/main/java/go/kr/project/api/mapper/VmisCarLedgerFrmbkMapper.java index 1a84531..c76952c 100644 --- a/src/main/java/go/kr/project/api/mapper/VmisCarLedgerFrmbkMapper.java +++ b/src/main/java/go/kr/project/api/mapper/VmisCarLedgerFrmbkMapper.java @@ -25,10 +25,10 @@ public interface VmisCarLedgerFrmbkMapper { // 편의: 상세 일괄 (MyBatis foreach를 XML에서 사용할 수도 있으나, 여기서는 단건 호출을 반복) /** - * txId로 CAR_FFNLG_TRGT_ID를 업데이트합니다. + * txId로 CAR_FFNLG_TRGT_ID를 업데이트합니다. (지연/미필 공통) * * @param txId 트랜잭션 ID - * @param carFfnlgTrgtId 과태료 대상 ID + * @param carFfnlgTrgtId 과태료 대상 ID (지연 또는 미필) * @return 업데이트된 행 수 */ int updateCarFfnlgTrgtIdByTxId(String txId, String carFfnlgTrgtId); diff --git a/src/main/java/go/kr/project/api/service/VmisCarBassMatterInqireLogService.java b/src/main/java/go/kr/project/api/service/VmisCarBassMatterInqireLogService.java index c6f7d6a..6a4b05f 100644 --- a/src/main/java/go/kr/project/api/service/VmisCarBassMatterInqireLogService.java +++ b/src/main/java/go/kr/project/api/service/VmisCarBassMatterInqireLogService.java @@ -25,9 +25,9 @@ public interface VmisCarBassMatterInqireLogService { void updateResponseNewTx(CarBassMatterInqireVO response); /** - * txId로 CAR_FFNLG_TRGT_ID를 업데이트한다. (REQUIRES_NEW) + * txId로 CAR_FFNLG_TRGT_ID를 업데이트한다. (REQUIRES_NEW, 지연/미필 공통) * @param response API 응답 객체 - * @param carFfnlgTrgtId 과태료 대상 ID + * @param carFfnlgTrgtId 과태료 대상 ID (지연 또는 미필) */ void updateCarFfnlgTrgtIdByTxIdNewTx(NewBasicResponse response, String carFfnlgTrgtId); } diff --git a/src/main/java/go/kr/project/api/service/VmisCarLedgerFrmbkLogService.java b/src/main/java/go/kr/project/api/service/VmisCarLedgerFrmbkLogService.java index 281023e..3ac5b95 100644 --- a/src/main/java/go/kr/project/api/service/VmisCarLedgerFrmbkLogService.java +++ b/src/main/java/go/kr/project/api/service/VmisCarLedgerFrmbkLogService.java @@ -33,9 +33,9 @@ public interface VmisCarLedgerFrmbkLogService { void saveDetailsNewTx(String masterId, List details); /** - * txId로 CAR_FFNLG_TRGT_ID를 업데이트한다. (REQUIRES_NEW) + * txId로 CAR_FFNLG_TRGT_ID를 업데이트한다. (REQUIRES_NEW, 지연/미필 공통) * @param response API 응답 객체 - * @param carFfnlgTrgtId 과태료 대상 ID + * @param carFfnlgTrgtId 과태료 대상 ID (지연 또는 미필) */ void updateCarFfnlgTrgtIdByTxIdNewTx(NewLedgerResponse response, String carFfnlgTrgtId); } diff --git a/src/main/java/go/kr/project/api/service/impl/VmisCarBassMatterInqireLogServiceImpl.java b/src/main/java/go/kr/project/api/service/impl/VmisCarBassMatterInqireLogServiceImpl.java index 05cb831..d1e38ed 100644 --- a/src/main/java/go/kr/project/api/service/impl/VmisCarBassMatterInqireLogServiceImpl.java +++ b/src/main/java/go/kr/project/api/service/impl/VmisCarBassMatterInqireLogServiceImpl.java @@ -82,4 +82,5 @@ public class VmisCarBassMatterInqireLogServiceImpl extends EgovAbstractServiceIm // 저장 실패해도 비교 로직은 계속 진행 } } + } diff --git a/src/main/java/go/kr/project/api/service/impl/VmisCarLedgerFrmbkLogServiceImpl.java b/src/main/java/go/kr/project/api/service/impl/VmisCarLedgerFrmbkLogServiceImpl.java index ef1011e..2e88bbd 100644 --- a/src/main/java/go/kr/project/api/service/impl/VmisCarLedgerFrmbkLogServiceImpl.java +++ b/src/main/java/go/kr/project/api/service/impl/VmisCarLedgerFrmbkLogServiceImpl.java @@ -88,4 +88,5 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im // 저장 실패해도 비교 로직은 계속 진행 } } + } diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/config/CarFfnlgPrnParseConfig.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/config/CarFfnlgPrnParseConfig.java new file mode 100644 index 0000000..47200c6 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/config/CarFfnlgPrnParseConfig.java @@ -0,0 +1,109 @@ +package go.kr.project.carInspectionPenalty.registrationOm.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 자동차 과태료 미필 PRN 파일 파싱 설정 + * application.yml의 car-ffnlg-prn-parse 설정을 바인딩 + * + * hangul-byte-size 값에 따라 적절한 바이트 길이 설정을 자동으로 선택합니다. + * - hangul-byte-size: 2 → byte-size-2 설정 사용 (EUC-KR, MS949) + * - hangul-byte-size: 3 → byte-size-3 설정 사용 (UTF-8) + * + * 각 항목별로 지정된 바이트 길이만 설정 (from-to 방식이 아님) + * 예: no: 6 (6바이트), vhclno: 14 (14바이트) + * -1 값은 "나머지 전체"를 의미 + */ +@Component +@ConfigurationProperties(prefix = "car-ffnlg-prn-parse") +@Data +public class CarFfnlgPrnParseConfig { + + /** + * 파일 인코딩 (UTF-8, EUC-KR, MS949 등) + */ + private String encoding; + + /** + * 한글 문자 바이트 크기 + * 2 = EUC-KR/MS949 (2바이트) + * 3 = UTF-8 (3바이트) + */ + private int hangulByteSize; + + /** + * 2바이트 환경 설정 (EUC-KR, MS949) + */ + private ByteSizeConfig byteSize2; + + /** + * 3바이트 환경 설정 (UTF-8) + */ + private ByteSizeConfig byteSize3; + + /** + * 바이트 크기별 설정 (first-line, second-line 포함) + */ + @Data + public static class ByteSizeConfig { + /** + * 첫째줄 필드별 바이트 길이 + */ + private Map firstLine; + + /** + * 둘째줄 필드별 바이트 길이 + */ + private Map secondLine; + } + + /** + * 현재 hangul-byte-size에 해당하는 설정 가져오기 + * + * @return ByteSizeConfig (2바이트 또는 3바이트 설정) + */ + private ByteSizeConfig getCurrentConfig() { + if (hangulByteSize == 3) { + return byteSize3; + } else { + // 기본값은 2바이트 (EUC-KR, MS949) + return byteSize2; + } + } + + /** + * 첫째줄 필드의 바이트 길이 가져오기 + * hangul-byte-size 값에 따라 적절한 설정에서 길이를 반환합니다. + * + * @param fieldKey 필드 키 (예: "no", "vhclno") + * @return 바이트 길이 (-1 = 나머지 전체) + */ + public int getFirstLineLength(String fieldKey) { + ByteSizeConfig config = getCurrentConfig(); + if (config == null || config.getFirstLine() == null) { + return 0; + } + Integer length = config.getFirstLine().get(fieldKey); + return length != null ? length : 0; + } + + /** + * 둘째줄 필드의 바이트 길이 가져오기 + * hangul-byte-size 값에 따라 적절한 설정에서 길이를 반환합니다. + * + * @param fieldKey 필드 키 (예: "skip", "rrno") + * @return 바이트 길이 (-1 = 나머지 전체) + */ + public int getSecondLineLength(String fieldKey) { + ByteSizeConfig config = getCurrentConfig(); + if (config == null || config.getSecondLine() == null) { + return 0; + } + Integer length = config.getSecondLine().get(fieldKey); + return length != null ? length : 0; + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java new file mode 100644 index 0000000..76b8439 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java @@ -0,0 +1,572 @@ +package go.kr.project.carInspectionPenalty.registrationOm.controller; + +import egovframework.constant.MessageConstants; +import egovframework.constant.TilesConstants; +import egovframework.util.ApiResponseUtil; +import egovframework.util.SessionUtil; +import egovframework.util.excel.ExcelSheetData; +import egovframework.util.excel.SxssfExcelFile; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpModifiedDataVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.CarFfnlgTrgtIncmpService; +import go.kr.project.common.model.CmmnCodeSearchVO; +import go.kr.project.common.service.CommonCodeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 자동차 미필 과태료 대상 등록 Controller + * 미필 과태료 대상 목록 조회, PRN 파일 업로드 기능 제공 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + */ +@Controller +@RequestMapping("/carInspectionPenalty/registration-om") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "자동차 미필 과태료 대상 등록", description = "자동차 미필 과태료 대상 등록 및 목록 조회 API") +public class CarFfnlgTrgtIncmpController { + + private final CarFfnlgTrgtIncmpService service; + private final CommonCodeService commonCodeService; + + /** + * 미필 과태료 대상 목록 화면 + * @param model 모델 + * @return 목록 화면 경로 + */ + @GetMapping("/list.do") + @Operation(summary = "미필 과태료 대상 목록 화면", description = "미필 과태료 대상 목록 조회 화면을 제공합니다.") + public String list(Model model) { + log.debug("미필 과태료 대상 목록 화면 요청"); + + // 업무 처리 상태 코드 조회 (공통코드) + CmmnCodeSearchVO taskPrcsSttsCdSearchVO = CmmnCodeSearchVO.builder() + .searchCdGroupId("TASK_PRCS_STTS_CD") + .searchUseYn("Y") + .sortColumn("SORT_ORDR") + .sortAscending(true) + .build(); + model.addAttribute("taskPrcsSttsCdList", commonCodeService.selectCodeDetailList(taskPrcsSttsCdSearchVO)); + + // 미필 과태료 대상 구분 코드 조회 (공통코드) + CmmnCodeSearchVO ffnlgTrgtSeCdSearchVO = CmmnCodeSearchVO.builder() + .searchCdGroupId("FFNLG_TRGT_SE_CD") + .searchUseYn("Y") + .sortColumn("SORT_ORDR") + .sortAscending(true) + .build(); + model.addAttribute("ffnlgTrgtSeCdList", commonCodeService.selectCodeDetailList(ffnlgTrgtSeCdSearchVO)); + + return "carInspectionPenalty/registrationOm/list" + TilesConstants.BASE; + } + + /** + * 미필 과태료 대상 목록 조회 AJAX + * @param paramVO 검색 조건 + * @return 목록 데이터 + */ + @PostMapping("/list.ajax") + @Operation(summary = "미필 과태료 대상 목록 조회", description = "미필 과태료 대상 목록을 조회하고 JSON 형식으로 반환합니다.") + public ResponseEntity listAjax(@ModelAttribute CarFfnlgTrgtIncmpVO paramVO) { + log.debug("미필 과태료 대상 목록 조회 AJAX - 검색조건: {}", paramVO); + + // 1. 총 개수 조회 + int totalCount = service.selectListTotalCount(paramVO); + + // 2. totalCount 설정 + paramVO.setTotalCount(totalCount); + + // 3. 페이징 활성화 + paramVO.setPagingYn("Y"); + + // 목록 조회 + List list = service.selectList(paramVO); + + return ApiResponseUtil.successWithGrid(list, paramVO); + } + + /** + * 미필 과태료 대상 목록 다운로드 (EUC-KR 텍스트) + * 샘플 파일과 동일한 고정폭 포맷으로 생성하여 다운로드 제공합니다. + * - 인코딩: EUC-KR (한글 2바이트) + */ + @GetMapping("/download.do") + @Operation(summary = "미필 과태료 대상 목록 다운로드", description = "EUC-KR 인코딩의 고정폭 텍스트로 목록을 샘플과 동일한 포맷으로 다운로드합니다.") + public void download( + @ModelAttribute CarFfnlgTrgtIncmpVO paramVO, + HttpServletResponse response + ) { + try { + + // 페이징 없이 전체 조회를 위해 페이징 비활성화 + paramVO.setPagingYn("N"); + + // 서비스에서 EUC-KR 텍스트 콘텐츠 생성 + byte[] fileBytes = service.generateEucKrDownloadBytes(paramVO); + + // EUC-KR 바이트를 UTF-8 바이트로 변환 (다운로드 시에만) + String content = new String(fileBytes, "EUC-KR"); + byte[] utfFileBytes = content.getBytes(StandardCharsets.UTF_8); + + String fileName = URLEncoder.encode("미필_유효기간경과_과태료부과대상_리스트.prn", "UTF-8"); + + // 응답 헤더 설정 (텍스트 파일, UTF-8 인코딩) + response.setContentType("text/plain; charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentLength(utfFileBytes.length); + + // UTF-8 바이트 스트림으로 전송 + response.getOutputStream().write(utfFileBytes); + response.getOutputStream().flush(); + } catch (Exception e) { + log.error("목록 다운로드 중 오류", e); + try { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("다운로드 처리 중 오류가 발생했습니다: " + e.getMessage()); + } catch (Exception ignored) { + + } + } + } + + /** + * 파일 업로드 팝업 화면 + * @return 팝업 화면 + */ + @GetMapping("/uploadPopup.do") + @Operation(summary = "파일 업로드 팝업", description = "PRN 파일 업로드 팝업 화면을 제공합니다.") + public ModelAndView uploadPopup() { + log.debug("파일 업로드 팝업 화면 요청"); + + ModelAndView mav = new ModelAndView("carInspectionPenalty/registrationOm/uploadPopup" + TilesConstants.POPUP); + + return mav; + } + + /** + * PRN 파일 업로드 및 처리 + * + * 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨 + * + * @param file 업로드된 PRN 파일 + * @return 처리 결과 + */ + @PostMapping("/upload.ajax") + @Operation(summary = "PRN 파일 업로드", description = "PRN 파일을 업로드하고 파싱하여 DB에 저장합니다. 한 건이라도 실패 시 전체 롤백됩니다.") + public ResponseEntity upload( + @Parameter(description = "PRN 파일") @RequestParam("file") MultipartFile file) { + + log.info("PRN 파일 업로드 요청 - 파일명: {}", file != null ? file.getOriginalFilename() : "null"); + + try { + // 세션에서 사용자 ID 가져오기 + String rgtr = SessionUtil.getUserId(); + if (rgtr == null || rgtr.isEmpty()) { + return ApiResponseUtil.error("로그인 정보가 없습니다."); + } + + // UTF-8 파일을 EUC-KR로 변환 (시스템은 EUC-KR 기준으로 처리) + MultipartFile convertedFile = convertUtf8ToEucKr(file); + + // 파일 업로드 및 파싱 (한 건이라도 실패 시 전체 롤백) + Map result = service.uploadAndParsePrnFile(convertedFile, rgtr); + + int successCount = (int) result.get("successCount"); + int failCount = (int) result.get("failCount"); + @SuppressWarnings("unchecked") + List errorMessages = (List) result.get("errorMessages"); + + if (failCount == 0 && successCount > 0) { + // 모든 데이터가 성공적으로 저장됨 + String message = String.format("파일 업로드가 완료되었습니다.\n\n성공: %d건", successCount); + return ApiResponseUtil.success(result, message); + } else if (successCount > 0) { + // 일부 성공, 일부 실패 + String message = String.format("파일 업로드가 완료되었습니다.\n\n성공: %d건, 실패: %d건", successCount, failCount); + return ApiResponseUtil.success(result, message); + } else { + // 모두 실패 + StringBuilder message = new StringBuilder(); + if (!errorMessages.isEmpty()) { + for (String errorMsg : errorMessages) { + message.append(errorMsg).append("\n"); + } + } else { + message.append("파일 업로드 중 오류가 발생했습니다."); + } + return ApiResponseUtil.error(message.toString()); + } + + } catch (RuntimeException e) { + // 데이터 처리 중 오류 발생 - 전체 롤백됨 + log.error("PRN 파일 업로드 중 오류 발생 - 전체 롤백", e); + return ApiResponseUtil.error(e.getMessage()); + } catch (Exception e) { + // 예상치 못한 오류 + log.error("PRN 파일 업로드 중 예상치 못한 오류 발생", e); + return ApiResponseUtil.error("파일 업로드 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 상세 조회 + * @param carFfnlgTrgtIncmpId 미필 과태료 대상 ID + * @return 상세 정보 + */ + @GetMapping("/selectOne.ajax") + @Operation(summary = "미필 과태료 대상 상세 조회", description = "미필 과태료 대상 상세 정보를 조회합니다.") + public ResponseEntity selectOne( + @Parameter(description = "미필 과태료 대상 ID") @RequestParam String carFfnlgTrgtIncmpId) { + + log.debug("미필 과태료 대상 상세 조회 - ID: {}", carFfnlgTrgtIncmpId); + + try { + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId); + + CarFfnlgTrgtIncmpVO result = service.selectOne(vo); + + if (result != null) { + return ApiResponseUtil.success(result, "조회 성공"); + } else { + return ApiResponseUtil.error("조회된 데이터가 없습니다."); + } + + } catch (Exception e) { + log.error("미필 과태료 대상 상세 조회 중 오류 발생", e); + return ApiResponseUtil.error("조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 삭제 (논리삭제) + * @param carFfnlgTrgtIncmpId 미필 과태료 대상 ID + * @return 삭제 결과 + */ + @PostMapping("/delete.ajax") + @Operation(summary = "미필 과태료 대상 삭제", description = "미필 과태료 대상을 삭제(논리삭제)합니다.") + public ResponseEntity delete( + @Parameter(description = "미필 과태료 대상 ID") @RequestParam String carFfnlgTrgtIncmpId) { + + log.info("미필 과태료 대상 삭제 요청 - ID: {}", carFfnlgTrgtIncmpId); + + try { + String dltr = SessionUtil.getUserId(); + if (dltr == null || dltr.isEmpty()) { + return ApiResponseUtil.error("로그인 정보가 없습니다."); + } + + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId); + vo.setDltr(dltr); + + int result = service.delete(vo); + + if (result > 0) { + return ApiResponseUtil.success(MessageConstants.Common.DELETE_SUCCESS); + } else { + return ApiResponseUtil.error(MessageConstants.Common.DELETE_ERROR); + } + + } catch (Exception e) { + log.error("미필 과태료 대상 삭제 중 오류 발생", e); + return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + * @param targetList 선택된 미필 과태료 대상 목록 + * @return 비교 결과 + */ + @PostMapping("/compareWithApi.ajax") + @ResponseBody + @Operation(summary = "API 호출 및 데이터 비교", description = "선택된 목록에 대해 차량 API를 호출하고 기본정보 및 등록원부와 비교합니다. 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일") + public ResponseEntity compareWithApi(@RequestBody List> targetList) { + log.info("API 호출 및 비교 요청 (미필) - 선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0); + + try { + Map resultData = service.compareWithApi(targetList); + + int successCount = (int) resultData.get("successCount"); + int failCount = (int) resultData.get("failCount"); + String message = String.format("API 호출 및 비교 완료\n성공: %d건, 실패: %d건", successCount, failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (IllegalArgumentException e) { + log.error("파라미터 검증 오류", e); + return ApiResponseUtil.error(e.getMessage()); + } catch (Exception e) { + log.error("API 호출 및 비교 중 오류 발생", e); + return ApiResponseUtil.error("처리 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 검색조건 전체 목록에 대해 API 호출 및 기본정보/등록원부 비교 (페이징 없이) + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + * @param searchParams 검색 조건 파라미터 + * @return 비교 결과 + */ + @PostMapping("/compareWithApiAll.ajax") + @ResponseBody + @Operation(summary = "검색조건 전체 API 호출 및 데이터 비교", description = "검색조건에 해당하는 전체 목록에 대해 차량 API를 호출하고 비교합니다. 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일") + public ResponseEntity compareWithApiAll(@RequestBody CarFfnlgTrgtIncmpVO searchParams) { + log.info("전체 API 호출 및 비교 요청 (미필) - 검색 조건: {}", searchParams); + + try { + // 페이징 비활성화 + searchParams.setPagingYn("N"); + + // 전체 목록 조회 + List allData = service.selectList(searchParams); + + // 목록을 Map 형태로 변환 + List> targetList = allData.stream() + .map(vo -> { + Map map = new HashMap<>(); + map.put("carFfnlgTrgtIncmpId", vo.getCarFfnlgTrgtIncmpId()); + map.put("vhclno", vo.getVhclno()); + map.put("inspVldPrd", vo.getInspVldPrd()); + map.put("ownrNm", vo.getOwnrNm()); + map.put("carNm", vo.getCarNm()); + return map; + }) + .collect(java.util.stream.Collectors.toList()); + + // API 호출 및 비교 + Map resultData = service.compareWithApi(targetList); + + int successCount = (int) resultData.get("successCount"); + int failCount = (int) resultData.get("failCount"); + String message = String.format("전체 API 호출 및 비교 완료\n대상: %d건, 성공: %d건, 실패: %d건", + allData.size(), successCount, failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (Exception e) { + log.error("전체 API 호출 및 비교 중 오류 발생", e); + return ApiResponseUtil.error("처리 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 일괄 삭제 + * @param deleteIds 삭제할 미필 과태료 대상 ID 목록 + * @return 삭제 결과 + */ + @PostMapping("/deleteBatch.ajax") + @ResponseBody + @Operation(summary = "미필 과태료 대상 일괄 삭제", description = "선택된 미필 과태료 대상 목록을 일괄 삭제합니다.") + public ResponseEntity deleteBatch(@RequestBody List deleteIds) { + log.info("일괄 삭제 요청 - 선택된 데이터 건수: {}", deleteIds != null ? deleteIds.size() : 0); + + try { + if (deleteIds == null || deleteIds.isEmpty()) { + return ApiResponseUtil.error("삭제할 데이터가 없습니다."); + } + + int successCount = 0; + int failCount = 0; + + for (String id : deleteIds) { + try { + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(id); + int result = service.delete(vo); + + if (result > 0) { + successCount++; + } else { + failCount++; + } + } catch (Exception e) { + log.error("삭제 실패 - ID: {}", id, e); + failCount++; + } + } + + String message = String.format("삭제 완료\n성공: %d건, 실패: %d건", successCount, failCount); + + Map resultData = new HashMap<>(); + resultData.put("successCount", successCount); + resultData.put("failCount", failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (Exception e) { + log.error("일괄 삭제 중 오류 발생", e); + return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 정보를 일괄 저장하는 AJAX 메소드 + * 생성, 수정, 삭제된 데이터를 처리합니다. + * + * @param modifyData 생성/수정/삭제할 데이터를 담은 VO 객체 + * @return 저장 결과 메시지와 성공/실패 상태를 담은 ResponseEntity 객체 + */ + @PostMapping("/saveAll.ajax") + @ResponseBody + @Operation(summary = "미필 과태료 대상 정보 일괄 저장", description = "생성, 수정, 삭제된 미필 과태료 대상 데이터를 일괄 처리합니다.") + public ResponseEntity saveAllAjax(@RequestBody CarFfnlgTrgtIncmpModifiedDataVO modifyData) { + log.info("미필 과태료 대상 일괄 저장 요청 - 수정: {}건, 생성: {}건, 삭제: {}건", + modifyData.getUpdatedRows() != null ? modifyData.getUpdatedRows().size() : 0, + modifyData.getCreatedRows() != null ? modifyData.getCreatedRows().size() : 0, + modifyData.getDeletedRows() != null ? modifyData.getDeletedRows().size() : 0); + + try { + int result = service.saveCarFfnlgTrgtIncmps(modifyData); + if (result > 0) { + return ApiResponseUtil.success("미필 과태료 대상 정보가 저장되었습니다."); + } else { + return ApiResponseUtil.error("저장할 데이터가 없습니다."); + } + } catch (Exception e) { + log.error("미필 과태료 대상 일괄 저장 중 오류 발생", e); + return ApiResponseUtil.error("저장 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * UTF-8 인코딩된 MultipartFile을 EUC-KR 인코딩으로 변환 + * + * @param utf8File UTF-8 인코딩된 원본 파일 + * @return EUC-KR 인코딩으로 변환된 파일 + * @throws IOException 파일 읽기/변환 중 오류 발생 시 + */ + private MultipartFile convertUtf8ToEucKr(MultipartFile utf8File) throws IOException { + // UTF-8로 파일 내용 읽기 + String content = new String(utf8File.getBytes(), StandardCharsets.UTF_8); + + // EUC-KR 바이트로 변환 + byte[] eucKrBytes = content.getBytes("EUC-KR"); + String eucKrContent = new String(eucKrBytes, "EUC-KR"); + + log.info("파일 인코딩 변환 - UTF-8({} bytes) → EUC-KR({} bytes)", + utf8File.getSize(), eucKrBytes.length); + + // EUC-KR 바이트로 변환된 새로운 MultipartFile 반환 + return new EucKrMultipartFile( + utf8File.getName(), + utf8File.getOriginalFilename(), + utf8File.getContentType(), + eucKrBytes + ); + } + + /** + * EUC-KR 인코딩 변환을 위한 커스텀 MultipartFile 구현체 + */ + private static class EucKrMultipartFile implements MultipartFile { + private final String name; + private final String originalFilename; + private final String contentType; + private final byte[] bytes; + + public EucKrMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) { + this.name = name; + this.originalFilename = originalFilename; + this.contentType = contentType; + this.bytes = bytes; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return bytes == null || bytes.length == 0; + } + + @Override + public long getSize() { + return bytes.length; + } + + @Override + public byte[] getBytes() { + return bytes; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public void transferTo(java.io.File dest) throws IOException, IllegalStateException { + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) { + fos.write(bytes); + } + } + } + + /** + * 미필 과태료 대상 목록 엑셀 다운로드 + * + * @param paramVO 검색 조건을 담은 VO 객체 + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + */ + @PostMapping("/excel.do") + @Operation(summary = "미필 과태료 대상 목록 엑셀 다운로드", description = "미필 과태료 대상 목록을 엑셀 파일로 다운로드합니다.") + public void downloadExcel( + @ModelAttribute CarFfnlgTrgtIncmpVO paramVO, + HttpServletRequest request, + HttpServletResponse response) { + try { + log.debug("미필 과태료 대상 목록 엑셀 다운로드 요청"); + + // 페이징 처리 없이 전체 데이터 조회 + paramVO.setPagingYn("N"); + + // 미필 과태료 대상 목록 조회 + List excelList = service.selectListForExcel(paramVO); + + // 엑셀 파일 생성 및 다운로드 + String filename = "미필_과태료대상목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; + new SxssfExcelFile(ExcelSheetData.of(excelList, CarFfnlgTrgtIncmpExcelVO.class, "미필 과태료 대상 목록 " + excelList.size() + "건"), request, response, filename); + + log.debug("미필 과태료 대상 목록 엑셀 다운로드 완료 - 파일명: {}, 건수: {}", filename, excelList.size()); + } catch (Exception e) { + log.error("엑셀 다운로드 중 오류 발생", e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java.bak b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java.bak new file mode 100644 index 0000000..76b8439 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/controller/CarFfnlgTrgtIncmpController.java.bak @@ -0,0 +1,572 @@ +package go.kr.project.carInspectionPenalty.registrationOm.controller; + +import egovframework.constant.MessageConstants; +import egovframework.constant.TilesConstants; +import egovframework.util.ApiResponseUtil; +import egovframework.util.SessionUtil; +import egovframework.util.excel.ExcelSheetData; +import egovframework.util.excel.SxssfExcelFile; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpModifiedDataVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.CarFfnlgTrgtIncmpService; +import go.kr.project.common.model.CmmnCodeSearchVO; +import go.kr.project.common.service.CommonCodeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 자동차 미필 과태료 대상 등록 Controller + * 미필 과태료 대상 목록 조회, PRN 파일 업로드 기능 제공 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + */ +@Controller +@RequestMapping("/carInspectionPenalty/registration-om") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "자동차 미필 과태료 대상 등록", description = "자동차 미필 과태료 대상 등록 및 목록 조회 API") +public class CarFfnlgTrgtIncmpController { + + private final CarFfnlgTrgtIncmpService service; + private final CommonCodeService commonCodeService; + + /** + * 미필 과태료 대상 목록 화면 + * @param model 모델 + * @return 목록 화면 경로 + */ + @GetMapping("/list.do") + @Operation(summary = "미필 과태료 대상 목록 화면", description = "미필 과태료 대상 목록 조회 화면을 제공합니다.") + public String list(Model model) { + log.debug("미필 과태료 대상 목록 화면 요청"); + + // 업무 처리 상태 코드 조회 (공통코드) + CmmnCodeSearchVO taskPrcsSttsCdSearchVO = CmmnCodeSearchVO.builder() + .searchCdGroupId("TASK_PRCS_STTS_CD") + .searchUseYn("Y") + .sortColumn("SORT_ORDR") + .sortAscending(true) + .build(); + model.addAttribute("taskPrcsSttsCdList", commonCodeService.selectCodeDetailList(taskPrcsSttsCdSearchVO)); + + // 미필 과태료 대상 구분 코드 조회 (공통코드) + CmmnCodeSearchVO ffnlgTrgtSeCdSearchVO = CmmnCodeSearchVO.builder() + .searchCdGroupId("FFNLG_TRGT_SE_CD") + .searchUseYn("Y") + .sortColumn("SORT_ORDR") + .sortAscending(true) + .build(); + model.addAttribute("ffnlgTrgtSeCdList", commonCodeService.selectCodeDetailList(ffnlgTrgtSeCdSearchVO)); + + return "carInspectionPenalty/registrationOm/list" + TilesConstants.BASE; + } + + /** + * 미필 과태료 대상 목록 조회 AJAX + * @param paramVO 검색 조건 + * @return 목록 데이터 + */ + @PostMapping("/list.ajax") + @Operation(summary = "미필 과태료 대상 목록 조회", description = "미필 과태료 대상 목록을 조회하고 JSON 형식으로 반환합니다.") + public ResponseEntity listAjax(@ModelAttribute CarFfnlgTrgtIncmpVO paramVO) { + log.debug("미필 과태료 대상 목록 조회 AJAX - 검색조건: {}", paramVO); + + // 1. 총 개수 조회 + int totalCount = service.selectListTotalCount(paramVO); + + // 2. totalCount 설정 + paramVO.setTotalCount(totalCount); + + // 3. 페이징 활성화 + paramVO.setPagingYn("Y"); + + // 목록 조회 + List list = service.selectList(paramVO); + + return ApiResponseUtil.successWithGrid(list, paramVO); + } + + /** + * 미필 과태료 대상 목록 다운로드 (EUC-KR 텍스트) + * 샘플 파일과 동일한 고정폭 포맷으로 생성하여 다운로드 제공합니다. + * - 인코딩: EUC-KR (한글 2바이트) + */ + @GetMapping("/download.do") + @Operation(summary = "미필 과태료 대상 목록 다운로드", description = "EUC-KR 인코딩의 고정폭 텍스트로 목록을 샘플과 동일한 포맷으로 다운로드합니다.") + public void download( + @ModelAttribute CarFfnlgTrgtIncmpVO paramVO, + HttpServletResponse response + ) { + try { + + // 페이징 없이 전체 조회를 위해 페이징 비활성화 + paramVO.setPagingYn("N"); + + // 서비스에서 EUC-KR 텍스트 콘텐츠 생성 + byte[] fileBytes = service.generateEucKrDownloadBytes(paramVO); + + // EUC-KR 바이트를 UTF-8 바이트로 변환 (다운로드 시에만) + String content = new String(fileBytes, "EUC-KR"); + byte[] utfFileBytes = content.getBytes(StandardCharsets.UTF_8); + + String fileName = URLEncoder.encode("미필_유효기간경과_과태료부과대상_리스트.prn", "UTF-8"); + + // 응답 헤더 설정 (텍스트 파일, UTF-8 인코딩) + response.setContentType("text/plain; charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentLength(utfFileBytes.length); + + // UTF-8 바이트 스트림으로 전송 + response.getOutputStream().write(utfFileBytes); + response.getOutputStream().flush(); + } catch (Exception e) { + log.error("목록 다운로드 중 오류", e); + try { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write("다운로드 처리 중 오류가 발생했습니다: " + e.getMessage()); + } catch (Exception ignored) { + + } + } + } + + /** + * 파일 업로드 팝업 화면 + * @return 팝업 화면 + */ + @GetMapping("/uploadPopup.do") + @Operation(summary = "파일 업로드 팝업", description = "PRN 파일 업로드 팝업 화면을 제공합니다.") + public ModelAndView uploadPopup() { + log.debug("파일 업로드 팝업 화면 요청"); + + ModelAndView mav = new ModelAndView("carInspectionPenalty/registrationOm/uploadPopup" + TilesConstants.POPUP); + + return mav; + } + + /** + * PRN 파일 업로드 및 처리 + * + * 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨 + * + * @param file 업로드된 PRN 파일 + * @return 처리 결과 + */ + @PostMapping("/upload.ajax") + @Operation(summary = "PRN 파일 업로드", description = "PRN 파일을 업로드하고 파싱하여 DB에 저장합니다. 한 건이라도 실패 시 전체 롤백됩니다.") + public ResponseEntity upload( + @Parameter(description = "PRN 파일") @RequestParam("file") MultipartFile file) { + + log.info("PRN 파일 업로드 요청 - 파일명: {}", file != null ? file.getOriginalFilename() : "null"); + + try { + // 세션에서 사용자 ID 가져오기 + String rgtr = SessionUtil.getUserId(); + if (rgtr == null || rgtr.isEmpty()) { + return ApiResponseUtil.error("로그인 정보가 없습니다."); + } + + // UTF-8 파일을 EUC-KR로 변환 (시스템은 EUC-KR 기준으로 처리) + MultipartFile convertedFile = convertUtf8ToEucKr(file); + + // 파일 업로드 및 파싱 (한 건이라도 실패 시 전체 롤백) + Map result = service.uploadAndParsePrnFile(convertedFile, rgtr); + + int successCount = (int) result.get("successCount"); + int failCount = (int) result.get("failCount"); + @SuppressWarnings("unchecked") + List errorMessages = (List) result.get("errorMessages"); + + if (failCount == 0 && successCount > 0) { + // 모든 데이터가 성공적으로 저장됨 + String message = String.format("파일 업로드가 완료되었습니다.\n\n성공: %d건", successCount); + return ApiResponseUtil.success(result, message); + } else if (successCount > 0) { + // 일부 성공, 일부 실패 + String message = String.format("파일 업로드가 완료되었습니다.\n\n성공: %d건, 실패: %d건", successCount, failCount); + return ApiResponseUtil.success(result, message); + } else { + // 모두 실패 + StringBuilder message = new StringBuilder(); + if (!errorMessages.isEmpty()) { + for (String errorMsg : errorMessages) { + message.append(errorMsg).append("\n"); + } + } else { + message.append("파일 업로드 중 오류가 발생했습니다."); + } + return ApiResponseUtil.error(message.toString()); + } + + } catch (RuntimeException e) { + // 데이터 처리 중 오류 발생 - 전체 롤백됨 + log.error("PRN 파일 업로드 중 오류 발생 - 전체 롤백", e); + return ApiResponseUtil.error(e.getMessage()); + } catch (Exception e) { + // 예상치 못한 오류 + log.error("PRN 파일 업로드 중 예상치 못한 오류 발생", e); + return ApiResponseUtil.error("파일 업로드 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 상세 조회 + * @param carFfnlgTrgtIncmpId 미필 과태료 대상 ID + * @return 상세 정보 + */ + @GetMapping("/selectOne.ajax") + @Operation(summary = "미필 과태료 대상 상세 조회", description = "미필 과태료 대상 상세 정보를 조회합니다.") + public ResponseEntity selectOne( + @Parameter(description = "미필 과태료 대상 ID") @RequestParam String carFfnlgTrgtIncmpId) { + + log.debug("미필 과태료 대상 상세 조회 - ID: {}", carFfnlgTrgtIncmpId); + + try { + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId); + + CarFfnlgTrgtIncmpVO result = service.selectOne(vo); + + if (result != null) { + return ApiResponseUtil.success(result, "조회 성공"); + } else { + return ApiResponseUtil.error("조회된 데이터가 없습니다."); + } + + } catch (Exception e) { + log.error("미필 과태료 대상 상세 조회 중 오류 발생", e); + return ApiResponseUtil.error("조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 삭제 (논리삭제) + * @param carFfnlgTrgtIncmpId 미필 과태료 대상 ID + * @return 삭제 결과 + */ + @PostMapping("/delete.ajax") + @Operation(summary = "미필 과태료 대상 삭제", description = "미필 과태료 대상을 삭제(논리삭제)합니다.") + public ResponseEntity delete( + @Parameter(description = "미필 과태료 대상 ID") @RequestParam String carFfnlgTrgtIncmpId) { + + log.info("미필 과태료 대상 삭제 요청 - ID: {}", carFfnlgTrgtIncmpId); + + try { + String dltr = SessionUtil.getUserId(); + if (dltr == null || dltr.isEmpty()) { + return ApiResponseUtil.error("로그인 정보가 없습니다."); + } + + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId); + vo.setDltr(dltr); + + int result = service.delete(vo); + + if (result > 0) { + return ApiResponseUtil.success(MessageConstants.Common.DELETE_SUCCESS); + } else { + return ApiResponseUtil.error(MessageConstants.Common.DELETE_ERROR); + } + + } catch (Exception e) { + log.error("미필 과태료 대상 삭제 중 오류 발생", e); + return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + * @param targetList 선택된 미필 과태료 대상 목록 + * @return 비교 결과 + */ + @PostMapping("/compareWithApi.ajax") + @ResponseBody + @Operation(summary = "API 호출 및 데이터 비교", description = "선택된 목록에 대해 차량 API를 호출하고 기본정보 및 등록원부와 비교합니다. 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일") + public ResponseEntity compareWithApi(@RequestBody List> targetList) { + log.info("API 호출 및 비교 요청 (미필) - 선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0); + + try { + Map resultData = service.compareWithApi(targetList); + + int successCount = (int) resultData.get("successCount"); + int failCount = (int) resultData.get("failCount"); + String message = String.format("API 호출 및 비교 완료\n성공: %d건, 실패: %d건", successCount, failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (IllegalArgumentException e) { + log.error("파라미터 검증 오류", e); + return ApiResponseUtil.error(e.getMessage()); + } catch (Exception e) { + log.error("API 호출 및 비교 중 오류 발생", e); + return ApiResponseUtil.error("처리 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 검색조건 전체 목록에 대해 API 호출 및 기본정보/등록원부 비교 (페이징 없이) + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + * @param searchParams 검색 조건 파라미터 + * @return 비교 결과 + */ + @PostMapping("/compareWithApiAll.ajax") + @ResponseBody + @Operation(summary = "검색조건 전체 API 호출 및 데이터 비교", description = "검색조건에 해당하는 전체 목록에 대해 차량 API를 호출하고 비교합니다. 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일") + public ResponseEntity compareWithApiAll(@RequestBody CarFfnlgTrgtIncmpVO searchParams) { + log.info("전체 API 호출 및 비교 요청 (미필) - 검색 조건: {}", searchParams); + + try { + // 페이징 비활성화 + searchParams.setPagingYn("N"); + + // 전체 목록 조회 + List allData = service.selectList(searchParams); + + // 목록을 Map 형태로 변환 + List> targetList = allData.stream() + .map(vo -> { + Map map = new HashMap<>(); + map.put("carFfnlgTrgtIncmpId", vo.getCarFfnlgTrgtIncmpId()); + map.put("vhclno", vo.getVhclno()); + map.put("inspVldPrd", vo.getInspVldPrd()); + map.put("ownrNm", vo.getOwnrNm()); + map.put("carNm", vo.getCarNm()); + return map; + }) + .collect(java.util.stream.Collectors.toList()); + + // API 호출 및 비교 + Map resultData = service.compareWithApi(targetList); + + int successCount = (int) resultData.get("successCount"); + int failCount = (int) resultData.get("failCount"); + String message = String.format("전체 API 호출 및 비교 완료\n대상: %d건, 성공: %d건, 실패: %d건", + allData.size(), successCount, failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (Exception e) { + log.error("전체 API 호출 및 비교 중 오류 발생", e); + return ApiResponseUtil.error("처리 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 일괄 삭제 + * @param deleteIds 삭제할 미필 과태료 대상 ID 목록 + * @return 삭제 결과 + */ + @PostMapping("/deleteBatch.ajax") + @ResponseBody + @Operation(summary = "미필 과태료 대상 일괄 삭제", description = "선택된 미필 과태료 대상 목록을 일괄 삭제합니다.") + public ResponseEntity deleteBatch(@RequestBody List deleteIds) { + log.info("일괄 삭제 요청 - 선택된 데이터 건수: {}", deleteIds != null ? deleteIds.size() : 0); + + try { + if (deleteIds == null || deleteIds.isEmpty()) { + return ApiResponseUtil.error("삭제할 데이터가 없습니다."); + } + + int successCount = 0; + int failCount = 0; + + for (String id : deleteIds) { + try { + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + vo.setCarFfnlgTrgtIncmpId(id); + int result = service.delete(vo); + + if (result > 0) { + successCount++; + } else { + failCount++; + } + } catch (Exception e) { + log.error("삭제 실패 - ID: {}", id, e); + failCount++; + } + } + + String message = String.format("삭제 완료\n성공: %d건, 실패: %d건", successCount, failCount); + + Map resultData = new HashMap<>(); + resultData.put("successCount", successCount); + resultData.put("failCount", failCount); + + return ApiResponseUtil.success(resultData, message); + + } catch (Exception e) { + log.error("일괄 삭제 중 오류 발생", e); + return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 미필 과태료 대상 정보를 일괄 저장하는 AJAX 메소드 + * 생성, 수정, 삭제된 데이터를 처리합니다. + * + * @param modifyData 생성/수정/삭제할 데이터를 담은 VO 객체 + * @return 저장 결과 메시지와 성공/실패 상태를 담은 ResponseEntity 객체 + */ + @PostMapping("/saveAll.ajax") + @ResponseBody + @Operation(summary = "미필 과태료 대상 정보 일괄 저장", description = "생성, 수정, 삭제된 미필 과태료 대상 데이터를 일괄 처리합니다.") + public ResponseEntity saveAllAjax(@RequestBody CarFfnlgTrgtIncmpModifiedDataVO modifyData) { + log.info("미필 과태료 대상 일괄 저장 요청 - 수정: {}건, 생성: {}건, 삭제: {}건", + modifyData.getUpdatedRows() != null ? modifyData.getUpdatedRows().size() : 0, + modifyData.getCreatedRows() != null ? modifyData.getCreatedRows().size() : 0, + modifyData.getDeletedRows() != null ? modifyData.getDeletedRows().size() : 0); + + try { + int result = service.saveCarFfnlgTrgtIncmps(modifyData); + if (result > 0) { + return ApiResponseUtil.success("미필 과태료 대상 정보가 저장되었습니다."); + } else { + return ApiResponseUtil.error("저장할 데이터가 없습니다."); + } + } catch (Exception e) { + log.error("미필 과태료 대상 일괄 저장 중 오류 발생", e); + return ApiResponseUtil.error("저장 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * UTF-8 인코딩된 MultipartFile을 EUC-KR 인코딩으로 변환 + * + * @param utf8File UTF-8 인코딩된 원본 파일 + * @return EUC-KR 인코딩으로 변환된 파일 + * @throws IOException 파일 읽기/변환 중 오류 발생 시 + */ + private MultipartFile convertUtf8ToEucKr(MultipartFile utf8File) throws IOException { + // UTF-8로 파일 내용 읽기 + String content = new String(utf8File.getBytes(), StandardCharsets.UTF_8); + + // EUC-KR 바이트로 변환 + byte[] eucKrBytes = content.getBytes("EUC-KR"); + String eucKrContent = new String(eucKrBytes, "EUC-KR"); + + log.info("파일 인코딩 변환 - UTF-8({} bytes) → EUC-KR({} bytes)", + utf8File.getSize(), eucKrBytes.length); + + // EUC-KR 바이트로 변환된 새로운 MultipartFile 반환 + return new EucKrMultipartFile( + utf8File.getName(), + utf8File.getOriginalFilename(), + utf8File.getContentType(), + eucKrBytes + ); + } + + /** + * EUC-KR 인코딩 변환을 위한 커스텀 MultipartFile 구현체 + */ + private static class EucKrMultipartFile implements MultipartFile { + private final String name; + private final String originalFilename; + private final String contentType; + private final byte[] bytes; + + public EucKrMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) { + this.name = name; + this.originalFilename = originalFilename; + this.contentType = contentType; + this.bytes = bytes; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return bytes == null || bytes.length == 0; + } + + @Override + public long getSize() { + return bytes.length; + } + + @Override + public byte[] getBytes() { + return bytes; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public void transferTo(java.io.File dest) throws IOException, IllegalStateException { + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) { + fos.write(bytes); + } + } + } + + /** + * 미필 과태료 대상 목록 엑셀 다운로드 + * + * @param paramVO 검색 조건을 담은 VO 객체 + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + */ + @PostMapping("/excel.do") + @Operation(summary = "미필 과태료 대상 목록 엑셀 다운로드", description = "미필 과태료 대상 목록을 엑셀 파일로 다운로드합니다.") + public void downloadExcel( + @ModelAttribute CarFfnlgTrgtIncmpVO paramVO, + HttpServletRequest request, + HttpServletResponse response) { + try { + log.debug("미필 과태료 대상 목록 엑셀 다운로드 요청"); + + // 페이징 처리 없이 전체 데이터 조회 + paramVO.setPagingYn("N"); + + // 미필 과태료 대상 목록 조회 + List excelList = service.selectListForExcel(paramVO); + + // 엑셀 파일 생성 및 다운로드 + String filename = "미필_과태료대상목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; + new SxssfExcelFile(ExcelSheetData.of(excelList, CarFfnlgTrgtIncmpExcelVO.class, "미필 과태료 대상 목록 " + excelList.size() + "건"), request, response, filename); + + log.debug("미필 과태료 대상 목록 엑셀 다운로드 완료 - 파일명: {}, 건수: {}", filename, excelList.size()); + } catch (Exception e) { + log.error("엑셀 다운로드 중 오류 발생", e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/mapper/CarFfnlgTrgtIncmpMapper.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/mapper/CarFfnlgTrgtIncmpMapper.java new file mode 100644 index 0000000..637cf77 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/mapper/CarFfnlgTrgtIncmpMapper.java @@ -0,0 +1,90 @@ +package go.kr.project.carInspectionPenalty.registrationOm.mapper; + +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 자동차 과태료 대상 미필 Mapper + */ +@Mapper +public interface CarFfnlgTrgtIncmpMapper { + + /** + * 과태료 대상 미필 목록 총 개수 조회 + * @param vo 검색 조건 + * @return 총 개수 + */ + int selectListTotalCount(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 목록 조회 + * @param vo 검색 조건 + * @return 목록 + */ + List selectList(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 상세 조회 + * @param vo 조회 조건 (carFfnlgTrgtIncmpId) + * @return 상세 정보 + */ + CarFfnlgTrgtIncmpVO selectOne(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 등록 + * @param vo 등록할 데이터 + * @return 등록 건수 + */ + int insert(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 수정 + * @param vo 수정할 데이터 + * @return 수정 건수 + */ + int update(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필의 처리상태와 비고만 수정 + * @param vo 수정할 데이터 (carFfnlgTrgtIncmpId, taskPrcsSttsCd, rmrk) + * @return 수정 건수 + */ + int updateTaskPrcsSttsCdAndRmrk(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 삭제 (논리삭제) + * @param vo 삭제할 데이터 (carFfnlgTrgtIncmpId, dltr) + * @return 삭제 건수 + */ + int delete(CarFfnlgTrgtIncmpVO vo); + + /** + * 차량번호와 검사유효기간으로 중복 체크 (삭제되지 않은 데이터 중) + * @param vo 조회 조건 (vhclno, inspVldPrd) + * @return 존재 개수 + */ + int checkDuplicateVhclno(CarFfnlgTrgtIncmpVO vo); + + /** + * 시군구 코드로 시군구명 조회 + * @param sggCd 시군구 코드 (5자리) + * @return 시군구명 + */ + String selectSggNmBySggCd(String sggCd); + + /** + * 과태료 대상 미필 목록 엑셀 다운로드용 조회 + * @param vo 검색 조건 + * @return 엑셀 다운로드용 목록 + */ + List selectListForExcel(CarFfnlgTrgtIncmpVO vo); + + /** + * 미필 부과일자 가산일 조회 (OM_DAY_CD 코드의 D값) + * @return 가산일 (예: 145) + */ + String selectOmDayPlusDay(); +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpExcelVO.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpExcelVO.java new file mode 100644 index 0000000..8a4eb55 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpExcelVO.java @@ -0,0 +1,100 @@ +package go.kr.project.carInspectionPenalty.registrationOm.model; + +import egovframework.util.excel.ExcelColumn; +import egovframework.util.excel.ExcelSheet; +import lombok.*; + +/** + * 과태료 대상 미필 목록 엑셀 다운로드용 VO 클래스 + * + *

엑셀 다운로드 시 사용되는 전용 VO로 @ExcelColumn 어노테이션을 포함

+ *

엑셀 샘플 순서에 맞게 필드를 정렬하고, 필요한 컬럼만 헤더 설정

+ */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@ExcelSheet(name = "미필과태료대상목록") +public class CarFfnlgTrgtIncmpExcelVO { + + /** 접수일자 */ + @ExcelColumn(headerName = "접수일자", headerWidth = 15, align = ExcelColumn.Align.CENTER) + private String rcptYmd; + + /** 프로그램ID */ + @ExcelColumn(headerName = "프로그램ID", headerWidth = 12, align = ExcelColumn.Align.CENTER) + private String prgrmId; + + /** 처리일자 */ + @ExcelColumn(headerName = "처리일자", headerWidth = 30, align = ExcelColumn.Align.CENTER) + private String prcsYmd; + + /** 번호 */ + @ExcelColumn(headerName = "번호", headerWidth = 8, align = ExcelColumn.Align.CENTER) + private Integer no; + + /** 차량번호 */ + @ExcelColumn(headerName = "차량번호", headerWidth = 15, align = ExcelColumn.Align.CENTER) + private String vhclno; + + /** 소유자명 */ + @ExcelColumn(headerName = "소유자명", headerWidth = 20, align = ExcelColumn.Align.CENTER) + private String ownrNm; + + /** 주민등록번호 */ + @ExcelColumn(headerName = "주민등록번호", headerWidth = 20, align = ExcelColumn.Align.CENTER) + private String rrno; + + /** 자동차명 */ + @ExcelColumn(headerName = "자동차명", headerWidth = 20, align = ExcelColumn.Align.LEFT) + private String carNm; + + /** 사용본거지주소 */ + @ExcelColumn(headerName = "사용본거지주소", headerWidth = 50, align = ExcelColumn.Align.LEFT) + private String useStrhldAddr; + + /** 검사유효기간 */ + @ExcelColumn(headerName = "검사유효기간", headerWidth = 25, align = ExcelColumn.Align.CENTER) + private String inspVldPrd; + + /** 처리상태 */ + @ExcelColumn(headerName = "처리상태", headerWidth = 15, align = ExcelColumn.Align.CENTER) + private String taskPrcsSttsCdNm; + + /** 처리일자 */ + @ExcelColumn(headerName = "업무처리일자", headerWidth = 15, align = ExcelColumn.Align.CENTER) + private String taskPrcsYmd; + + /** 비고 */ + @ExcelColumn(headerName = "비고", headerWidth = 30, align = ExcelColumn.Align.LEFT) + private String rmrk; + + /** 기본사항조회성명 */ + @ExcelColumn(headerName = "기본사항조회성명", headerWidth = 18, align = ExcelColumn.Align.CENTER) + private String carBscMttrInqFlnm; + + /** 기본사항조회시군구명 */ + @ExcelColumn(headerName = "기본사항조회시군구명", headerWidth = 20, align = ExcelColumn.Align.CENTER) + private String carBscMttrInqSggNm; + + /** 등록원부변경업무명 */ + @ExcelColumn(headerName = "등록원부변경업무명", headerWidth = 20, align = ExcelColumn.Align.CENTER) + private String carRegFrmbkChgTaskSeNm; + + /** 등록원부변경일자 */ + @ExcelColumn(headerName = "등록원부변경일자", headerWidth = 18, align = ExcelColumn.Align.CENTER) + private String carRegFrmbkChgYmd; + + /** 등록원부상세 */ + @ExcelColumn(headerName = "등록원부상세", headerWidth = 40, align = ExcelColumn.Align.LEFT) + private String carRegFrmbkDtl; + + /** 등록일시 */ + @ExcelColumn(headerName = "등록일시", headerWidth = 20, align = ExcelColumn.Align.CENTER) + private String regDt; + + /** 등록자 */ + @ExcelColumn(headerName = "등록자", headerWidth = 15, align = ExcelColumn.Align.CENTER) + private String rgtrNm; +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpModifiedDataVO.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpModifiedDataVO.java new file mode 100644 index 0000000..d7c1969 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpModifiedDataVO.java @@ -0,0 +1,23 @@ +package go.kr.project.carInspectionPenalty.registrationOm.model; + +import go.kr.project.common.model.PagingVO; +import lombok.*; + +import java.util.List; + +/** + * 과태료 대상 미필 수정 데이터를 담는 VO 클래스 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class CarFfnlgTrgtIncmpModifiedDataVO extends PagingVO { + + private List createdRows; + private List updatedRows; + private List deletedRows; + +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpVO.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpVO.java new file mode 100644 index 0000000..62a1d44 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/model/CarFfnlgTrgtIncmpVO.java @@ -0,0 +1,82 @@ +package go.kr.project.carInspectionPenalty.registrationOm.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import go.kr.project.common.model.PagingVO; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 자동차 과태료 대상 미필 VO + * 테이블: tb_car_ffnlg_trgt_incmp + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CarFfnlgTrgtIncmpVO extends PagingVO { + + // 기본키 + private String carFfnlgTrgtIncmpId; // 자동차 과태료 대상 미필 ID + + // 업무 필드 (헤더에서 파싱) + private String rcptYmd; // 접수 일자 + private String prgrmId; // 프로그램 ID + private String prcsYmd; // 처리 일자 + private String otptDt; // 출력 일시 + + // 업무 필드 (데이터 행에서 파싱) + private Integer no; // 번호 + private String vhclno; // 차량번호 + private String ownrNm; // 소유자 명 + private String rrno; // 주민등록번호 + private String carNm; // 자동차 명 + private String useStrhldAddr; // 사용 본거지 주소 + private String inspVldPrd; // 검사 유효 기간 + + // 업무 처리 필드 + private String taskPrcsSttsCd; // 업무 처리 상태 코드 (01=접수, 02=처리중, 03=완료) + private String taskPrcsYmd; // 업무 처리 일자 + private String rmrk; // 비고 + + // API 연동 필드 + private String carBassMatterInqireId; // 자동차 기본 사항 조회 ID + private String carLedgerFrmbkId; // 자동차 등록 원부 갑 ID + private String carBscMttrInqFlnm; // 자동차 기본 사항 조회 성명 (상품용일 때 저장) + private String carBscMttrInqSggCd; // 자동차 기본 사항 조회 시군구 코드 (이첩일 때 저장) + private String carBscMttrInqSggNm; // 자동차 기본 사항 조회 시군구 명 (이첩일 때 저장) + private String carRegFrmbkChgTaskSeCd; // 자동차 등록 원부갑 변경 업무 구분 코드 + private String carRegFrmbkChgTaskSeNm; // 자동차 등록 원부갑 변경 업무 구분 명 + private String carRegFrmbkChgYmd; // 자동차 등록 원부갑 변경 일자 + private String carRegFrmbkDtl; // 자동차 등록 원부갑 상세 + + // 감사 필드 + @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 regDt; // 등록 일시 + private String rgtr; // 등록자 + private String delYn; // 삭제 여부 + @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 delDt; // 삭제 일시 + private String dltr; // 삭제자 + + // 조회용 필드 + private String taskPrcsSttsCdNm; // 업무 처리 상태 코드명 + private String rgtrNm; // 등록자명 + + // 검색 조건 필드 + private String schRcptYmdStart; // 검색 시작 접수 일자 + private String schRcptYmdEnd; // 검색 종료 접수 일자 + private String schVhclno; // 검색 차량번호 + private String schOwnrNm; // 검색 소유자명 + private List schTaskPrcsSttsCd; // 검색 업무 처리 상태 코드 (다중 선택 가능) + private String schPrcsYmdStart; // 검색 시작 처리 일자 + private String schPrcsYmdEnd; // 검색 종료 처리 일자 + + // 부과일자 계산용 필드 (검사유효기간 종료일 + 145일) + private String levyCrtrYmd; // 부과기준일자 (API 호출 시 사용) +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/CarFfnlgTrgtIncmpService.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/CarFfnlgTrgtIncmpService.java new file mode 100644 index 0000000..7ff5740 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/CarFfnlgTrgtIncmpService.java @@ -0,0 +1,99 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service; + +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpModifiedDataVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +/** + * 자동차 과태료 대상 미필 Service + */ +public interface CarFfnlgTrgtIncmpService { + + /** + * 과태료 대상 미필 목록 총 개수 조회 + * @param vo 검색 조건 + * @return 총 개수 + */ + int selectListTotalCount(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 목록 조회 + * @param vo 검색 조건 + * @return 목록 + */ + List selectList(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 상세 조회 + * @param vo 조회 조건 (carFfnlgTrgtIncmpId) + * @return 상세 정보 + */ + CarFfnlgTrgtIncmpVO selectOne(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 등록 + * @param vo 등록할 데이터 + * @return 등록 건수 + */ + int insert(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 수정 + * @param vo 수정할 데이터 + * @return 수정 건수 + */ + int update(CarFfnlgTrgtIncmpVO vo); + + /** + * 과태료 대상 미필 삭제 (논리삭제) + * @param vo 삭제할 데이터 (carFfnlgTrgtIncmpId, dltr) + * @return 삭제 건수 + */ + int delete(CarFfnlgTrgtIncmpVO vo); + + /** + * PRN 파일 업로드 및 파싱하여 DB 저장 + * @param file 업로드된 PRN 파일 + * @param rgtr 등록자 ID + * @return 처리 결과 (성공 건수, 실패 건수, 오류 메시지 목록) + */ + Map uploadAndParsePrnFile(MultipartFile file, String rgtr); + + /** + * 과태료 대상 미필 목록을 EUC-KR 인코딩의 고정폭 텍스트 바이트로 생성 + * 샘플 텍스트와 동일한 포맷으로 출력합니다. + * + * @param vo 검색 조건 + * @return EUC-KR 인코딩 바이트 배열 + */ + byte[] generateEucKrDownloadBytes(CarFfnlgTrgtIncmpVO vo); + + /** + * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + 145일 + * + * @param targetList 선택된 과태료 대상 미필 목록 (carFfnlgTrgtIncmpId, vhclno, inspVldPrd 포함) + * @return 비교 결과 (compareResults, totalCount, successCount, failCount) + */ + Map compareWithApi(List> targetList); + + /** + * 과태료 대상 미필 정보를 일괄 저장 + * 생성, 수정, 삭제된 데이터를 처리합니다. + * + * @param modifyData 생성/수정/삭제할 데이터를 담은 VO 객체 + * @return 저장 건수 + */ + int saveCarFfnlgTrgtIncmps(CarFfnlgTrgtIncmpModifiedDataVO modifyData); + + /** + * 과태료 대상 미필 목록 엑셀 다운로드용 조회 + * @param vo 검색 조건 + * @return 엑셀 다운로드용 목록 + */ + List selectListForExcel(CarFfnlgTrgtIncmpVO vo); +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/ComparisonOmService.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/ComparisonOmService.java new file mode 100644 index 0000000..e9df9b2 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/ComparisonOmService.java @@ -0,0 +1,30 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service; + +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; + +/** + * 과태료 대상 미필 비교 서비스 + * + *

차량 정보를 외부 API와 비교하여 상태를 자동으로 분류합니다.

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +public interface ComparisonOmService { + + /** + * 비교 로직을 실행하고 업무 처리 상태를 업데이트합니다. + * + *

실행 순서:

+ *
    + *
  1. 상품용 체크
  2. + *
  3. 이첩 체크
  4. + *
  5. 향후 추가될 비교 로직들...
  6. + *
+ * + *

한 가지 조건이라도 만족하면 즉시 해당 상태로 업데이트하고 종료합니다.

+ *

미필의 경우 levyCrtrYmd(부과일자)가 미리 계산되어 전달됩니다.

+ * + * @param existingData 기존 과태료 대상 미필 데이터 (levyCrtrYmd 포함) + * @return 처리 상태 코드 (02=상품용, 03=이첩, null=해당없음) + */ + String executeComparison(CarFfnlgTrgtIncmpVO existingData); +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/CarFfnlgTrgtIncmpServiceImpl.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/CarFfnlgTrgtIncmpServiceImpl.java new file mode 100644 index 0000000..da90dad --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/CarFfnlgTrgtIncmpServiceImpl.java @@ -0,0 +1,743 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.SessionUtil; +import go.kr.project.carInspectionPenalty.registrationOm.config.CarFfnlgPrnParseConfig; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpModifiedDataVO; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.CarFfnlgTrgtIncmpService; +import go.kr.project.carInspectionPenalty.registrationOm.service.ComparisonOmService; +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 java.io.BufferedReader; +import java.io.InputStreamReader; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 자동차 과태료 대상 미필 Service 구현체 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implements CarFfnlgTrgtIncmpService { + + private final CarFfnlgTrgtIncmpMapper mapper; + private final CarFfnlgPrnParseConfig parseConfig; + private final ComparisonOmService comparisonOmService; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Override + public int selectListTotalCount(CarFfnlgTrgtIncmpVO vo) { + return mapper.selectListTotalCount(vo); + } + + @Override + public List selectList(CarFfnlgTrgtIncmpVO vo) { + return mapper.selectList(vo); + } + + @Override + public CarFfnlgTrgtIncmpVO selectOne(CarFfnlgTrgtIncmpVO vo) { + return mapper.selectOne(vo); + } + + @Override + @Transactional + public int insert(CarFfnlgTrgtIncmpVO vo) { + return mapper.insert(vo); + } + + @Override + @Transactional + public int update(CarFfnlgTrgtIncmpVO vo) { + return mapper.update(vo); + } + + @Override + @Transactional + public int delete(CarFfnlgTrgtIncmpVO vo) { + return mapper.delete(vo); + } + + /** + * PRN 파일 업로드 및 파싱하여 DB 저장 + * + * 파일 형식: 고정폭 PRN 파일 (헤더 3줄 + 데이터 2줄씩) + * - 헤더 1줄: 프로그램ID + * - 헤더 2줄: 처리일자 + * - 헤더 3줄: 출력일시 + * - 데이터: 4번째 라인부터 시작, 2줄 1세트 + * 1) 첫 번째 줄: 번호/차량번호/소유자명/차명/사용본거지주소/검사유효기간 + * 2) 두 번째 줄: 주민등록번호/사용본거지주소(나머지) + * + * 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨 + */ + @Override + @Transactional + public Map uploadAndParsePrnFile(MultipartFile file, String rgtr) { + Map result = new HashMap<>(); + List errorMessages = new ArrayList<>(); + int successCount = 0; + int dataLineNumber = 0; + + try { + // 파일 검증 + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("파일이 선택되지 않았습니다."); + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".prn") && !originalFilename.toLowerCase().endsWith(".txt"))) { + throw new IllegalArgumentException("PRN, TXT 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename); + } + + if (file.getSize() > 50 * 1024 * 1024) { + throw new IllegalArgumentException("파일 크기는 50MB를 초과할 수 없습니다. 파일 크기: " + (file.getSize() / 1024 / 1024) + "MB"); + } + + log.info("PRN 파일 업로드 시작 - 파일명: {}, 크기: {} bytes", originalFilename, file.getSize()); + + String encoding = parseConfig.getEncoding(); + log.info("파일 인코딩: {}, 한글 바이트 크기: {}", encoding, parseConfig.getHangulByteSize()); + + List allLines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(file.getInputStream(), encoding))) { + String line; + while ((line = reader.readLine()) != null) { + allLines.add(line); + } + } + + // 파일 최소 라인 검증 (헤더 7라인 + 데이터 최소 2라인) + if (allLines.size() < 9) { + throw new IllegalArgumentException("파일 형식이 올바르지 않습니다. 최소 9라인 이상이어야 합니다. 현재 라인 수: " + allLines.size()); + } + + log.info("파일 읽기 완료 - 총 라인 수: {}", allLines.size()); + + // 헤더 파싱 (3, 4번째 줄에서 추출) + // Line 3: " 프로그램 ID : VGD01B" + // Line 4: " 처 리 일 자 : 2025년07월11일~2025년07월11일 출 력 일 시 : 2025년12월04일17시01분" + String line3 = allLines.get(2).trim(); + String line4 = allLines.get(3).trim(); + + String prgrmId = ""; + String prcsYmd = ""; + String otptDt = ""; + + // 프로그램 ID 추출 (: 기준으로 split) + if (line3.contains(":")) { + String[] parts = line3.split(":", 2); + if (parts.length > 1) { + prgrmId = parts[1].trim(); + } + } + + // 처리일자, 출력일시 추출 + if (line4.contains(":")) { + String[] parts = line4.split(":", 2); + if (parts.length > 1) { + String rest = parts[1].trim(); + // "2025년07월11일~2025년07월11일 출 력 일 시 : 2025년12월04일17시01분" 형태 + if (rest.contains("출")) { + int idx = rest.indexOf("출"); + prcsYmd = rest.substring(0, idx).trim(); + String rest2 = rest.substring(idx); + if (rest2.contains(":")) { + String[] parts2 = rest2.split(":", 2); + if (parts2.length > 1) { + otptDt = parts2[1].trim(); + } + } + } else { + prcsYmd = rest; + } + } + } + + log.info("헤더 정보 - 프로그램ID: {}, 처리일자: {}, 출력일시: {}", prgrmId, prcsYmd, otptDt); + + // 8번째 라인부터 데이터 처리 (인덱스 7부터 시작) + for (int i = 7; i < allLines.size(); i++) { + String firstLine = allLines.get(i); + + if (firstLine.trim().isEmpty()) { + continue; + } + + if (firstLine.trim().startsWith("---")) { + continue; + } + + if (i + 1 >= allLines.size()) { + String errorMsg = String.format("[라인 %d] 데이터가 불완전합니다. 2줄 1세트 형식이 필요합니다.", i + 1); + errorMessages.add(errorMsg); + throw new MessageException(buildErrorMessage(errorMessages)); + } + + String secondLine = allLines.get(i + 1); + dataLineNumber++; + + // 고정폭 파싱 + CarFfnlgTrgtIncmpVO vo = parseFixedWidthData(firstLine, secondLine, dataLineNumber, errorMessages, prgrmId, prcsYmd, otptDt); + + if (vo == null) { + throw new MessageException(buildErrorMessage(errorMessages)); + } + + // 필수 필드 검증 + List validationErrors = validateParsedData(dataLineNumber, vo); + + if (!validationErrors.isEmpty()) { + errorMessages.addAll(validationErrors); + throw new MessageException(buildErrorMessage(errorMessages)); + } + + // 차량번호+검사유효기간 중복 체크 + CarFfnlgTrgtIncmpVO checkVO = new CarFfnlgTrgtIncmpVO(); + checkVO.setVhclno(vo.getVhclno()); + checkVO.setInspVldPrd(vo.getInspVldPrd()); + int duplicateCount = mapper.checkDuplicateVhclno(checkVO); + if (duplicateCount > 0) { + String errorMsg = String.format("[데이터 %d] 중복된 차량번호+검사유효기간입니다. 차량번호: %s, 검사유효기간: %s", + dataLineNumber, vo.getVhclno(), vo.getInspVldPrd()); + errorMessages.add(errorMsg); + throw new MessageException(buildErrorMessage(errorMessages)); + } + + // 업무 처리 상태 및 등록자 설정 + vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT); + vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + vo.setRcptYmd(LocalDate.now().format(DATE_FORMATTER)); + vo.setRgtr(rgtr); + + // DB 저장 + int insertResult = mapper.insert(vo); + + if (insertResult > 0) { + successCount++; + log.debug("데이터 저장 성공 [데이터 {}] - 차량번호: {}", dataLineNumber, vo.getVhclno()); + } else { + String errorMsg = String.format("[데이터 %d] 데이터 저장 실패 - 차량번호: %s", dataLineNumber, vo.getVhclno()); + errorMessages.add(errorMsg); + throw new MessageException(buildErrorMessage(errorMessages)); + } + + // 2줄 1세트이므로 다음 줄 건너뛰기 + i++; + } + + log.info("PRN 파일 처리 완료 - 성공: {}건", successCount); + + result.put("success", true); + result.put("successCount", successCount); + result.put("failCount", 0); + result.put("errorMessages", errorMessages); + + } catch (IllegalArgumentException e) { + log.error("파일 검증 중 오류 발생: {}", e.getMessage()); + errorMessages.add(e.getMessage()); + result.put("success", false); + result.put("successCount", 0); + result.put("failCount", 0); + result.put("errorMessages", errorMessages); + } catch (MessageException e) { + log.error("PRN 파일 업로드 중 오류 발생 - 전체 롤백 처리", e); + throw e; + } catch (Exception e) { + log.error("PRN 파일 업로드 중 예상치 못한 오류 발생", e); + errorMessages.add("파일 업로드 중 오류가 발생했습니다: " + e.getMessage()); + throw new MessageException(buildErrorMessage(errorMessages), e); + } + + return result; + } + + /** + * 목록을 EUC-KR 텍스트로 생성하여 다운로드용 바이트 배열을 반환 + */ + @Override + public byte[] generateEucKrDownloadBytes(CarFfnlgTrgtIncmpVO vo) { + try { + final String encoding = parseConfig.getEncoding() == null || parseConfig.getEncoding().trim().isEmpty() + ? "EUC-KR" : parseConfig.getEncoding().trim(); + + List list = mapper.selectList(vo); + + StringBuilder sb = new StringBuilder(); + + // 헤더 구성 + sb.append("검사미필 과태료부과대상 리스트\r\n"); + sb.append("------------------------------------\r\n"); + sb.append("\r\n"); + + // 데이터 라인 생성 + for (CarFfnlgTrgtIncmpVO row : list) { + String firstLine = + padRightBytes(nvl(row.getNo()), 6, encoding) + + padRightBytes(nvl(row.getVhclno()), 14, encoding) + + padRightBytes(nvl(row.getOwnrNm()), 16, encoding) + + padRightBytes(nvl(row.getCarNm()), 22, encoding) + + padRightBytes(nvl(row.getUseStrhldAddr()), 62, encoding) + + padRightBytes(nvl(row.getInspVldPrd()), 23, encoding); + + sb.append(firstLine).append("\r\n"); + + String secondLine = + padRightBytes("", 38, encoding) + + padRightBytes(nvl(row.getRrno()), 16, encoding) + + padRightBytes(nvl(row.getUseStrhldAddr()), 62, encoding); + + sb.append(secondLine).append("\r\n"); + sb.append("\r\n"); + } + + return sb.toString().getBytes(encoding); + } catch (Exception e) { + throw new MessageException("다운로드 파일 생성 중 오류: " + e.getMessage(), e); + } + } + + /** + * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 + * 미필의 경우 부과일자 = 검사유효기간 종료일 + OM_DAY_CD의 D 코드값(145일) + */ + @Override + @Transactional + public Map compareWithApi(List> targetList) { + log.info("========== 미필 API 호출 및 비교 시작 =========="); + log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0); + + if (targetList == null || targetList.isEmpty()) { + throw new IllegalArgumentException("선택된 데이터가 없습니다."); + } + + List> compareResults = new ArrayList<>(); + int successCount = 0; + int productUseCount = 0; + int transferCount = 0; + int normalCount = 0; + + // 가산일 조회 (OM_DAY_CD 코드의 D값) + String plusDayStr = mapper.selectOmDayPlusDay(); + int plusDay = 145; // 기본값 + if (plusDayStr != null && !plusDayStr.isEmpty()) { + try { + plusDay = Integer.parseInt(plusDayStr); + } catch (NumberFormatException e) { + log.warn("가산일 파싱 실패, 기본값 145 사용: {}", plusDayStr); + } + } + log.info("부과일자 가산일: {}일", plusDay); + + for (Map target : targetList) { + String carFfnlgTrgtIncmpId = target.get("carFfnlgTrgtIncmpId"); + String vhclno = target.get("vhclno"); + String inspVldPrd = target.get("inspVldPrd"); + + log.info("처리 중 - 차량번호: {}, 검사유효기간: {}", vhclno, inspVldPrd); + + Map compareResult = new HashMap<>(); + compareResult.put("carFfnlgTrgtIncmpId", carFfnlgTrgtIncmpId); + compareResult.put("vhclno", vhclno); + + try { + // 1. 기존 데이터 조회 + CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO(); + existingData.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId); + existingData = mapper.selectOne(existingData); + + if (existingData == null) { + String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno); + log.error(errorMsg); + throw new MessageException(errorMsg); + } + + // 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가 + if (!TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT.equals(existingData.getTaskPrcsSttsCd())) { + String errorMsg = String.format("접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s", + vhclno, existingData.getTaskPrcsSttsCd()); + log.error(errorMsg); + throw new MessageException(errorMsg); + } + + // 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일) + String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd(inspVldPrd, plusDay); + existingData.setLevyCrtrYmd(levyCrtrYmd); + log.info("부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}", inspVldPrd, levyCrtrYmd); + + // 4. 비교 로직 실행 + String statusCode = comparisonOmService.executeComparison(existingData); + + // 결과 처리 + if (statusCode != null) { + if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) { + productUseCount++; + compareResult.put("processStatus", "상품용"); + compareResult.put("message", "상품용으로 처리되었습니다."); + } else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) { + transferCount++; + compareResult.put("processStatus", "이첩"); + compareResult.put("message", "이첩으로 처리되었습니다."); + } else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) { + normalCount++; + compareResult.put("processStatus", "내사종결"); + compareResult.put("message", "내사종결로 처리되었습니다."); + } else { + normalCount++; + compareResult.put("processStatus", "기타"); + compareResult.put("message", "기타 상태로 처리되었습니다."); + } + compareResult.put("success", true); + successCount++; + } else { + normalCount++; + compareResult.put("success", true); + compareResult.put("message", "정상 처리되었습니다."); + compareResult.put("processStatus", "정상"); + successCount++; + } + + } catch (Exception e) { + log.error("데이터 비교 중 오류 발생 - 차량번호: {}, 전체 롤백 처리", vhclno, e); + throw new MessageException(e.getMessage(), e); + } + + compareResults.add(compareResult); + } + + Map resultData = new HashMap<>(); + resultData.put("compareResults", compareResults); + resultData.put("totalCount", targetList.size()); + resultData.put("successCount", successCount); + resultData.put("failCount", 0); + resultData.put("productUseCount", productUseCount); + resultData.put("transferCount", transferCount); + resultData.put("normalCount", normalCount); + + log.info("========== 미필 API 호출 및 비교 완료 =========="); + log.info("성공: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건", + successCount, productUseCount, transferCount, normalCount); + + return resultData; + } + + /** + * 과태료 대상 미필 정보를 일괄 저장 + */ + @Override + @Transactional + public int saveCarFfnlgTrgtIncmps(CarFfnlgTrgtIncmpModifiedDataVO modifyData) { + int result = 0; + + // 1. 삭제된 행 처리 + List deletedRows = modifyData.getDeletedRows(); + if (deletedRows != null && !deletedRows.isEmpty()) { + for (CarFfnlgTrgtIncmpVO vo : deletedRows) { + String dltr = SessionUtil.getUserId(); + if (dltr == null || dltr.isEmpty()) { + throw new MessageException("로그인 정보가 없습니다."); + } + vo.setDltr(dltr); + result += mapper.delete(vo); + } + } + + // 2. 추가된 행 처리 + List createdRows = modifyData.getCreatedRows(); + if (createdRows != null && !createdRows.isEmpty()) { + for (CarFfnlgTrgtIncmpVO vo : createdRows) { + String rgtr = SessionUtil.getUserId(); + if (rgtr == null || rgtr.isEmpty()) { + throw new MessageException("로그인 정보가 없습니다."); + } + vo.setRgtr(rgtr); + + if (vo.getTaskPrcsSttsCd() == null || vo.getTaskPrcsSttsCd().isEmpty()) { + vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT); + } + + result += mapper.insert(vo); + } + } + + // 3. 수정된 행 처리 + List updatedRows = modifyData.getUpdatedRows(); + if (updatedRows != null && !updatedRows.isEmpty()) { + for (CarFfnlgTrgtIncmpVO vo : updatedRows) { + vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + result += mapper.updateTaskPrcsSttsCdAndRmrk(vo); + } + } + + log.info("과태료 대상 미필 일괄 저장 완료 - 처리 건수: {}", result); + return result; + } + + @Override + public List selectListForExcel(CarFfnlgTrgtIncmpVO vo) { + log.debug("과태료 대상 미필 목록 엑셀 다운로드용 조회 - 검색조건: {}", vo); + return mapper.selectListForExcel(vo); + } + + // ================== 내부 유틸 메서드 ================== + + private static String nvl(Object o) { + if (o == null) return ""; + return o.toString(); + } + + private static String padRightBytes(String s, int byteLen, String encoding) throws Exception { + if (byteLen <= 0) return nvl(s); + String v = nvl(s); + byte[] b = v.getBytes(encoding); + if (b.length == byteLen) return v; + if (b.length > byteLen) { + return truncateToBytes(v, byteLen, encoding); + } + StringBuilder sb = new StringBuilder(v); + while (sb.toString().getBytes(encoding).length < byteLen) { + sb.append(' '); + } + return sb.toString(); + } + + private static String truncateToBytes(String s, int byteLen, String encoding) throws Exception { + if (s == null) return ""; + byte[] b = s.getBytes(encoding); + if (b.length <= byteLen) return s; + byte[] cut = new byte[byteLen]; + System.arraycopy(b, 0, cut, 0, byteLen); + for (int len = byteLen; len > 0; len--) { + try { + return new String(cut, 0, len, encoding); + } catch (Exception ignore) { + } + } + return ""; + } + + private String buildErrorMessage(List errorMessages) { + if (errorMessages.isEmpty()) { + return "파일 업로드 중 오류가 발생했습니다."; + } + + StringBuilder sb = new StringBuilder(); + sb.append("파일 업로드 실패 - 전체 롤백 처리되었습니다.\n\n"); + sb.append("[오류 상세 내역]\n"); + + int displayCount = Math.min(errorMessages.size(), 10); + for (int i = 0; i < displayCount; i++) { + sb.append(errorMessages.get(i)).append("\n"); + } + + if (errorMessages.size() > 10) { + sb.append("... 외 ").append(errorMessages.size() - 10).append("건\n"); + } + + return sb.toString(); + } + + /** + * 설정 기반 바이트 단위 고정폭 데이터 파싱 (2줄 1세트) + */ + private CarFfnlgTrgtIncmpVO parseFixedWidthData(String firstLine, String secondLine, + int dataLineNumber, List errorMessages, + String prgrmId, String prcsYmd, String otptDt) { + try { + CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO(); + + String encoding = parseConfig.getEncoding(); + log.debug("[데이터 {}] 파싱 시작 - 인코딩: {}, 한글바이트: {}", dataLineNumber, encoding, parseConfig.getHangulByteSize()); + + // 첫 번째 줄 파싱 + byte[] firstBytes = firstLine.getBytes(encoding); + int pos = 0; + + // 번호 + int len = parseConfig.getFirstLineLength("no"); + String no = extractByteLength(firstBytes, pos, len, encoding).trim(); + pos += len; + + // 차량번호 + len = parseConfig.getFirstLineLength("vhclno"); + String vhclno = extractByteLength(firstBytes, pos, len, encoding).trim(); + pos += len; + + // 소유자명 (주민번호 빈칸 포함 32바이트) + len = parseConfig.getFirstLineLength("ownr-nm"); + String ownrNm = extractByteLength(firstBytes, pos, len, encoding).trim(); + pos += len; + + // 자동차명 + len = parseConfig.getFirstLineLength("car-nm"); + String carNm = extractByteLength(firstBytes, pos, len, encoding).trim(); + pos += len; + + // 사용본거지주소 + len = parseConfig.getFirstLineLength("use-strhld-addr"); + String useStrhldAddr = extractByteLength(firstBytes, pos, len, encoding).trim(); + pos += len; + + // 검사유효기간 + len = parseConfig.getFirstLineLength("insp-vld-prd"); + String inspVldPrd = extractByteLength(firstBytes, pos, len, encoding).trim(); + + // 두 번째 줄 파싱 + byte[] secondBytes = secondLine.getBytes(encoding); + pos = 0; + + // 공백 스킵 + len = parseConfig.getSecondLineLength("skip"); + pos += len; + + // 주민등록번호 + len = parseConfig.getSecondLineLength("rrno"); + String rrno = extractByteLength(secondBytes, pos, len, encoding).trim(); + pos += len; + + // 사용본거지주소 (나머지) + len = parseConfig.getSecondLineLength("use-strhld-addr"); + String useStrhldAddr2 = extractByteLength(secondBytes, pos, len, encoding).trim(); + + // 주소 합치기 + if (!useStrhldAddr2.isEmpty()) { + useStrhldAddr = useStrhldAddr + " " + useStrhldAddr2; + } + + // VO 설정 + if (!no.isEmpty()) { + try { + vo.setNo(Integer.parseInt(no)); + } catch (NumberFormatException e) { + log.warn("번호 파싱 실패: {}", no); + } + } + vo.setPrgrmId(prgrmId); + vo.setPrcsYmd(prcsYmd); + vo.setOtptDt(otptDt); + vo.setVhclno(vhclno); + vo.setOwnrNm(ownrNm); + vo.setRrno(rrno); + vo.setCarNm(carNm); + vo.setUseStrhldAddr(useStrhldAddr); + vo.setInspVldPrd(inspVldPrd); + + log.debug("[데이터 {}] 파싱 완료", dataLineNumber); + + return vo; + + } catch (Exception e) { + String errorMsg = String.format("[데이터 %d] 파싱 중 오류 발생 - %s", dataLineNumber, e.getMessage()); + errorMessages.add(errorMsg); + log.error("데이터 {} 파싱 중 오류", dataLineNumber, e); + return null; + } + } + + private String extractByteLength(byte[] bytes, int pos, int length, String encoding) { + try { + if (pos < 0) pos = 0; + if (pos >= bytes.length) return ""; + + int actualLength; + if (length < 0) { + actualLength = bytes.length - pos; + } else { + actualLength = Math.min(length, bytes.length - pos); + } + + if (actualLength <= 0) return ""; + + byte[] extracted = new byte[actualLength]; + System.arraycopy(bytes, pos, extracted, 0, actualLength); + + return new String(extracted, encoding); + + } catch (Exception e) { + log.error("바이트 추출 중 오류 발생 - pos: {}, length: {}, encoding: {}", pos, length, encoding, e); + return ""; + } + } + + /** + * 파싱된 데이터 유효성 검증 + */ + private List validateParsedData(int dataLineNumber, CarFfnlgTrgtIncmpVO vo) { + List errors = new ArrayList<>(); + + String vhclno = vo.getVhclno() != null ? vo.getVhclno() : "알 수 없음"; + + // 1. 차량번호 검증 + if (vo.getVhclno() == null || vo.getVhclno().isEmpty()) { + errors.add(String.format("[데이터 %d] 차량번호가 누락되었습니다.", dataLineNumber)); + } else if (vo.getVhclno().length() > 30) { + errors.add(String.format("[데이터 %d] 차량번호가 너무 깁니다. 차량번호: %s (최대 30자)", dataLineNumber, vo.getVhclno())); + } + + // 2. 소유자명 검증 + if (vo.getOwnrNm() == null || vo.getOwnrNm().isEmpty()) { + errors.add(String.format("[데이터 %d] 소유자명이 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno)); + } else if (vo.getOwnrNm().length() > 75) { + errors.add(String.format("[데이터 %d] 소유자명이 너무 깁니다. 소유자명: %s (최대 75자), 차량번호: %s", + dataLineNumber, vo.getOwnrNm(), vhclno)); + } + + // 3. 주민등록번호 검증 + if (vo.getRrno() == null || vo.getRrno().isEmpty()) { + errors.add(String.format("[데이터 %d] 주민등록번호가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno)); + } else if (vo.getRrno().length() > 100) { + errors.add(String.format("[데이터 %d] 주민등록번호가 너무 깁니다. 주민번호 길이: %d (최대 100자), 차량번호: %s", + dataLineNumber, vo.getRrno().length(), vhclno)); + } + + // 4. 검사유효기간 검증 + if (vo.getInspVldPrd() == null || vo.getInspVldPrd().isEmpty()) { + errors.add(String.format("[데이터 %d] 검사유효기간이 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno)); + } + + return errors; + } + + /** + * 검사유효기간에서 부과일자 계산 + * 예: "2023-07-12~2025-07-11" -> 종료일(2025-07-11) + 가산일(145일) = 20251203 + */ + private String calculateLevyCrtrYmdFromInspVldPrd(String inspVldPrd, int plusDay) { + if (inspVldPrd == null || inspVldPrd.isEmpty()) { + throw new IllegalArgumentException("검사유효기간이 없습니다."); + } + + // "2023-07-12~2025-07-11" 형식에서 종료일 추출 + String[] parts = inspVldPrd.split("~"); + if (parts.length != 2) { + throw new IllegalArgumentException("검사유효기간 형식이 올바르지 않습니다: " + inspVldPrd); + } + + String endDateStr = parts[1].trim().replace("-", ""); + + try { + LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + LocalDate levyDate = endDate.plusDays(plusDay); + return levyDate.format(DATE_FORMATTER); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("검사유효기간 종료일 파싱 실패: " + endDateStr, e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmRemarkBuilder.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmRemarkBuilder.java new file mode 100644 index 0000000..e0a5e9b --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmRemarkBuilder.java @@ -0,0 +1,279 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl; + +import egovframework.util.DateUtil; +import egovframework.util.StringUtil; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; + +/** + * 과태료 대상 미필 비교 비고(Remark) 생성 유틸리티 클래스 + * + *

각 비교 로직별 비고 문자열을 생성하는 메서드를 제공합니다.

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +public class ComparisonOmRemarkBuilder { + + /** + * 상품용 비고 생성 - 미필 + * + * @param step1Record Step 1 API 응답 (부과일자 기준 소유자명) + * @param step4Record Step 4 API 응답 (명의이전 시점 소유자명) + * @param ledgerRecord 조건에 맞는 갑부 레코드 + * @param inspVldPrdEnd 검사유효기간 종료일 + * @param levyCrtrYmd 부과일자 (검사유효기간 종료일 + 145일) + * @return 비고 문자열 + */ + public static String buildProductUseRemark( + NewBasicResponse.Record step1Record, + NewBasicResponse.Record step4Record, + NewLedgerResponse.Record ledgerRecord, + String inspVldPrdEnd, + String levyCrtrYmd) { + + StringBuilder sb = new StringBuilder(); + sb.append("상품용 - 미필검사\n"); + + // 1. 부과일자 기준 소유자 정보 + sb.append("\n■ 부과일자 기준 소유자정보\n"); + sb.append(" - 소유자명: ").append(StringUtil.nvl(step1Record.getRprsOwnrNm())).append("\n"); + sb.append(" - 차대번호: ").append(StringUtil.nvl(step1Record.getVin())).append("\n"); + sb.append(" - 부과일자: ").append(DateUtil.formatDateString(levyCrtrYmd)).append("\n"); + + // 2. 명의이전 시점 소유자 정보 + sb.append("\n■ 명의이전 시점 소유자정보\n"); + sb.append(" - 소유자명: ").append(StringUtil.nvl(step4Record.getRprsOwnrNm())).append("\n"); + sb.append(" - 조회일자: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + + // 3. 갑부 상세 정보 (명의이전 이력) + sb.append("\n■ 갑부 상세 (명의이전 이력)\n"); + sb.append(" - 변경일자: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + sb.append(" - 변경업무코드: ").append(StringUtil.nvl(ledgerRecord.getChgTaskSeCd())).append("\n"); + + // 4. 검사유효기간 정보 + sb.append("\n■ 검사유효기간 정보\n"); + sb.append(" - 검사유효기간 종료일: ").append(DateUtil.formatDateString(inspVldPrdEnd)).append("\n"); + + return sb.toString(); + } + + /** + * 상품용-변경등록 비고 생성 - 미필 + * + * @param step1Record Step 1 API 응답 (부과일자 기준 소유자명) + * @param step4Record Step 4 API 응답 (변경등록 시점 소유자명) + * @param ledgerRecord 조건에 맞는 갑부 레코드 (변경등록 레코드) + * @param inspVldPrdEnd 검사유효기간 종료일 + * @param levyCrtrYmd 부과일자 (검사유효기간 종료일 + 145일) + * @return 비고 문자열 + */ + public static String buildProductUseChangeRemark( + NewBasicResponse.Record step1Record, + NewBasicResponse.Record step4Record, + NewLedgerResponse.Record ledgerRecord, + String inspVldPrdEnd, + String levyCrtrYmd) { + + StringBuilder sb = new StringBuilder(); + sb.append("상품용 - 변경등록 - 미필\n"); + + // 1. 부과일자 기준 소유자 정보 + sb.append("\n■ 부과일자 기준 소유자정보\n"); + sb.append(" - 소유자명: ").append(StringUtil.nvl(step1Record.getRprsOwnrNm())).append("\n"); + sb.append(" - 차대번호: ").append(StringUtil.nvl(step1Record.getVin())).append("\n"); + + // 2. 변경등록 시점 소유자 정보 + sb.append("\n■ 변경등록 시점 소유자정보\n"); + sb.append(" - 소유자명: ").append(StringUtil.nvl(step4Record.getRprsOwnrNm())).append("\n"); + sb.append(" - 조회일자: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + + // 3. 갑부 상세 정보 (변경등록 이력) + sb.append("\n■ 갑부 상세 (변경등록 이력)\n"); + sb.append(" - 변경일자: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + sb.append(" - 변경업무코드: ").append(StringUtil.nvl(ledgerRecord.getChgTaskSeCd())).append("\n"); + sb.append(" - 변경업무명: ").append(StringUtil.nvl(ledgerRecord.getChgTaskSeNm())).append("\n"); + sb.append(" - 특별사항: ").append(StringUtil.nvl(ledgerRecord.getSpcablMttr())).append("\n"); + + return sb.toString(); + } + + /** + * 상품용 비고 생성 - 미필 (부과일자 소유자가 상품용이 아니지만, 명의이전 후 상품용인 경우) + * + * 비고 형식: + * 명의이전(25.9.3.) 이전소유자 상품용 + * 22루2283 + * 검사유효기간 시작일 - 종료일 + * 부과일자 일자 + * 명의이전 일자 + * 상품용 일자 + * + * @param step1Record Step 1 API 응답 (부과일자 기준 소유자명) + * @param step4Record Step 4 API 응답 (명의이전 시점 소유자명 = 상품용) + * @param ledgerRecord 조건에 맞는 갑부 레코드 (명의이전 레코드) + * @param vhclno 차량번호 + * @param levyCrtrYmd 부과일자 (검사유효기간 종료일 + 145일) + * @param inspVldPrdStart 검사유효기간 시작일 + * @param inspVldPrdEnd 검사유효기간 종료일 + * @param daysBetween 일수차이 + * @return 비고 문자열 + */ + public static String buildProductCloseLevyRemark( + NewBasicResponse.Record step1Record, + NewBasicResponse.Record step4Record, + NewLedgerResponse.Record ledgerRecord, + String vhclno, + String levyCrtrYmd, + String inspVldPrdStart, + String inspVldPrdEnd, + long daysBetween) { + + // 날짜 포맷 변환 (YYYYMMDD -> YY.M.D) + String chgYmdFormatted = DateUtil.formatToShortDate(ledgerRecord.getChgYmd()); + String step1wnerName = StringUtil.nvl(step1Record.getRprsOwnrNm()); + + StringBuilder sb = new StringBuilder(); + + // 첫 줄: 명의이전(25.9.3.) 이전소유자 상품용 + sb.append("명의이전(").append(chgYmdFormatted).append(") 이전소유자 상품용").append("\n"); + + // 둘째 줄: 차량번호 + sb.append(StringUtil.nvl(vhclno)).append("\n"); + + // 셋째 줄: 검사유효기간 시작일 - 종료일 + sb.append(" - 검사유효기간: ").append(DateUtil.formatDateString(inspVldPrdStart)) + .append(" - ").append(DateUtil.formatDateString(inspVldPrdEnd)).append("\n"); + + // 넷째 줄: 부과일자 일자 + sb.append(" - 부과일자: ").append(DateUtil.formatDateString(levyCrtrYmd)).append("\n"); + + // 다섯째 줄: 명의이전 일자 + sb.append(" - 명의이전: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + + // 여섯째 줄: 상품용 일자 (명의이전 일자와 동일) + sb.append(" - 상품용: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + + // 일곱째 줄: 일수차이 + sb.append("일수차이: ").append(daysBetween).append("일"); + + return sb.toString(); + } + + + /** + * 명의이전 비고 생성 - 미필 (내사종결 또는 날짜 수정 후 부과) + * + * 비고 형식: + * 명의이전(25.9.3.) + * 22루2283 + * 검사유효기간 시작일 - 종료일 + * 부과일자 일자 + * 명의이전 일자 + * + * @param step1Record Step 1 API 응답 (부과일자 기준 소유자명) + * @param step4Record Step 4 API 응답 (명의이전 시점 소유자명) + * @param ledgerRecord 조건에 맞는 갑부 레코드 (명의이전 레코드) + * @param vhclno 차량번호 + * @param levyCrtrYmd 부과일자 (검사유효기간 종료일 + 145일) + * @param inspVldPrdStart 검사유효기간 시작일 + * @param inspVldPrdEnd 검사유효기간 종료일 + * @param daysBetween 일수차이 + * @return 비고 문자열 + */ + public static String buildOwnerChangeRemark(NewBasicResponse.Record step1Record, + NewBasicResponse.Record step4Record, + NewLedgerResponse.Record ledgerRecord, + String vhclno, + String levyCrtrYmd, + String inspVldPrdStart, + String inspVldPrdEnd, + long daysBetween) { + + // 날짜 포맷 변환 (YYYYMMDD -> YY.M.D) + String chgYmdFormatted = DateUtil.formatToShortDate(ledgerRecord.getChgYmd()); + String step1wnerName = StringUtil.nvl(step1Record.getRprsOwnrNm()); + + StringBuilder sb = new StringBuilder(); + + // 첫 줄: 명의이전(25.9.3.) + sb.append("명의이전(").append(chgYmdFormatted).append(")").append("\n"); + + // 둘째 줄: 차량번호 + sb.append(StringUtil.nvl(vhclno)).append("\n"); + + // 셋째 줄: 검사유효기간 시작일 - 종료일 + sb.append(" - 검사유효기간: ").append(DateUtil.formatDateString(inspVldPrdStart)) + .append(" - ").append(DateUtil.formatDateString(inspVldPrdEnd)).append("\n"); + + // 넷째 줄: 부과일자 일자 + sb.append(" - 부과일자: ").append(DateUtil.formatDateString(levyCrtrYmd)).append("\n"); + + // 다섯째 줄: 명의이전 일자 + sb.append(" - 명의이전: ").append(DateUtil.formatDateString(ledgerRecord.getChgYmd())).append("\n"); + + // 일곱째 줄: 일수차이 + sb.append("일수차이: ").append(daysBetween).append("일"); + + return sb.toString(); + } + + /** + * 이첩 비고 생성 - Case 이첩-1 (부과일자 사용본거지) + * + * @param sggNm 시군구명 + * @param userOrg4 사용자 조직코드 앞 4자리 + * @return 비고 문자열 + */ + public static String buildTransferCase1Remark(String sggNm, String userOrg4) { + return String.format("%s, 부과일자사용본거지, [사용자 조직코드 앞 4자리: %s, 법정동명: %s]", + sggNm, userOrg4, sggNm); + } + + /** + * 이첩 비고 생성 - Case 이첩-2 (145일 도래지) + * + * @param sggNm 시군구명 + * @param legalDong4 법정동코드 앞 4자리 + * @return 비고 문자열 + */ + public static String buildTransferCase2Remark(String sggNm, String legalDong4) { + return String.format("%s, 145일 도래지, [법정동코드: %s, 법정동명: %s]", + sggNm, legalDong4, sggNm); + } + + /** + * 등록원부 갑부 레코드 상세 정보 생성 + * + * @param record 갑부 레코드 + * @return 상세 정보 문자열 + */ + public static String buildLedgerRecordDetail(NewLedgerResponse.Record record) { + if (record == null) { + return ""; + } + + StringBuilder detail = new StringBuilder(); + + // 변경 정보 + StringUtil.appendIfNotEmpty(detail, "변경업무구분코드", record.getChgTaskSeCd()); + StringUtil.appendIfNotEmpty(detail, "변경업무구분명", record.getChgTaskSeNm()); + StringUtil.appendIfNotEmpty(detail, "변경일자", DateUtil.formatDateString(record.getChgYmd())); + + // 주요 정보 + StringUtil.appendIfNotEmpty(detail, "주요번호", record.getMainNo()); + StringUtil.appendIfNotEmpty(detail, "일련번호", record.getSno()); + StringUtil.appendIfNotEmpty(detail, "특별사항", record.getSpcablMttr()); + + // 명의자 정보 + StringUtil.appendIfNotEmpty(detail, "명의자명", record.getHshldrNm()); + StringUtil.appendIfNotEmpty(detail, "명의자식별번호", StringUtil.maskIdecno(record.getHshldrIdecno())); + + // 기타 + StringUtil.appendIfNotEmpty(detail, "신청접수번호", record.getAplyRcptNo()); + StringUtil.appendIfNotEmpty(detail, "차량관리번호", record.getVhmno()); + StringUtil.appendIfNotEmpty(detail, "원부그룹번호", record.getLedgerGroupNo()); + StringUtil.appendIfNotEmpty(detail, "원부개별번호", record.getLedgerIndivNo()); + StringUtil.appendIfNotEmpty(detail, "상세일련번호", record.getDtlSn()); + + return detail.toString(); + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmServiceImpl.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmServiceImpl.java new file mode 100644 index 0000000..71c1533 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/ComparisonOmServiceImpl.java @@ -0,0 +1,94 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl; + +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.ComparisonOmService; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; +import org.springframework.stereotype.Service; + +/** + * 과태료 대상 미필 비교 서비스 구현체 + * + *

각 비교 로직을 독립적인 체커 클래스로 분리하여 관리합니다.

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ComparisonOmServiceImpl extends EgovAbstractServiceImpl implements ComparisonOmService { + + private final ProductUseOmChecker productUseOmChecker; + private final ProductUseOmChangeChecker productUseOmChangeChecker; + private final ProductCloseWithin31OmChecker productCloseWithin31OmChecker; + private final OwnerCloseWithin31OmChecker ownerCloseWithin31OmChecker; + private final ProductLevyOver31OmChecker productLevyOver31OmChecker; + private final OwnerLevyOver31OmChecker ownerLevyOver31OmChecker; + private final TransferOmChecker transferOmChecker; + + /** + * 비교 로직 메인 메서드 + * + *

순차적으로 각 비교 체커를 실행하고, 하나라도 적용되면 즉시 종료합니다.

+ *

미필의 경우 부과일자(levyCrtrYmd)가 미리 계산되어 existingData에 포함됩니다.

+ */ + @Override + public String executeComparison(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); + log.info("========== 미필 비교 로직 시작: {}, 부과일자: {} ==========", vhclno, levyCrtrYmd); + + // ========== 1. 상품용 체크 - api-1번호출.소유자명.contains("상품용") ========== + String productUseResult = productUseOmChecker.check(existingData); + if (productUseResult != null) { + log.info("========== 미필 비교 로직 종료 (상품용): {} ==========", vhclno); + return productUseResult; + } + + // ========== 2. 상품용 체크 - api-1번호출.소유자명.contains("상품용-변경등록") ========== + String productUseChangeResult = productUseOmChangeChecker.check(existingData); + if (productUseChangeResult != null) { + log.info("========== 미필 비교 로직 종료 (상품용-변경등록): {} ==========", vhclno); + return productUseChangeResult; + } + + // ========== 3. 내사종결 체크 - 명의이전 이전소유자 상품용, 31일 이내 ========== + String investigationClosedByProductResult = productCloseWithin31OmChecker.check(existingData); + if (investigationClosedByProductResult != null) { + log.info("========== 미필 비교 로직 종료 (내사종결 - 명의이전 이전소유자 상품용, 31일 이내): {} ==========", vhclno); + return investigationClosedByProductResult; + } + + // ========== 4. 내사종결 체크 - 명의이전, 31일 이내 ========== + String investigationClosedByOwnerChangeResult = ownerCloseWithin31OmChecker.check(existingData); + if (investigationClosedByOwnerChangeResult != null) { + log.info("========== 미필 비교 로직 종료 (내사종결 - 명의이전, 31일 이내): {} ==========", vhclno); + return investigationClosedByOwnerChangeResult; + } + + // ========== 5. 날짜 수정 후 부과 체크 - 명의이전 이전소유자 상품용, 31일 초과 ========== + String dateModifiedLevyByProductResult = productLevyOver31OmChecker.check(existingData); + if (dateModifiedLevyByProductResult != null) { + log.info("========== 미필 비교 로직 종료 (날짜 수정 후 부과 - 명의이전 이전소유자, 31일 초과): {} ==========", vhclno); + return dateModifiedLevyByProductResult; + } + + // ========== 6. 날짜 수정 후 부과 체크 - 명의이전, 31일 초과 ========== + String dateModifiedLevyByOwnerChangeOverResult = ownerLevyOver31OmChecker.check(existingData); + if (dateModifiedLevyByOwnerChangeOverResult != null) { + log.info("========== 미필 비교 로직 종료 (날짜 수정 후 부과 - 명의이전, 31일 초과): {} ==========", vhclno); + return dateModifiedLevyByOwnerChangeOverResult; + } + + // ========== 7. 이첩 체크 ========== + String transferResult = transferOmChecker.check(existingData); + if (transferResult != null) { + log.info("========== 미필 비교 로직 종료 (이첩): {} ==========", vhclno); + return transferResult; + } + + log.info("========== 미필 비교 로직 종료 (미적용): {} ==========", vhclno); + return null; + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/AbstractComparisonOmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/AbstractComparisonOmChecker.java new file mode 100644 index 0000000..67ca878 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/AbstractComparisonOmChecker.java @@ -0,0 +1,98 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.format.DateTimeFormatter; + +/** + * 미필 비교 로직 추상 클래스 + * + *

공통 의존성 및 헬퍼 메서드를 제공합니다.

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractComparisonOmChecker implements ComparisonOmChecker { + + protected final CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper; + protected final ExternalVehicleApiService apiService; + protected final VmisCarBassMatterInqireLogService bassMatterLogService; + protected final VmisCarLedgerFrmbkLogService ledgerLogService; + + protected static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** + * 명의이전 또는 상품용 관련 일수 기준값 (일) + * 명의이전일자 ~ 검사일 사이의 일수가 이 값 이하면 내사종결, 초과하면 날짜 수정 후 부과 + */ + protected static final int DAYS_THRESHOLD = 31; + + /** + * 자동차기본정보 요청 객체 생성 + * 미필의 경우 levyCrtrYmd = 검사유효기간 종료일 + 145일 + * + * @param vhrno 차량번호 + * @param vin 차대번호 + * @param levyCrtrYmd 부과일자 (미필: 검사유효기간 종료일 + 145일) + * @return NewBasicRequest + */ + protected NewBasicRequest createBasicRequest(String vhrno, String vin, String levyCrtrYmd) { + NewBasicRequest request = new NewBasicRequest(); + + NewBasicRequest.Record record = new NewBasicRequest.Record(); + record.setLevyCrtrYmd(levyCrtrYmd); + + if (vhrno != null) { + record.setVhrno(vhrno); + record.setInqSeCd("3"); // 3: 자동차번호 + } else if (vin != null) { + record.setVin(vin); + record.setInqSeCd("2"); // 2: 차대번호 + } + + request.setRecord(java.util.Arrays.asList(record)); + return request; + } + + /** + * 자동차등록원부(갑) 요청 객체 생성 + * + * @param vhrno 차량번호 + * @param ownerNm 성명 + * @param idecno 주민번호 + * @param legalDongCd 법정동코드 + * @return NewLedgerRequest + */ + protected NewLedgerRequest createLedgerRequest(String vhrno, String ownerNm, String idecno, String legalDongCd) { + NewLedgerRequest request = new NewLedgerRequest(); + + // 차량번호 + request.setVhrno(vhrno); + + // 민원인 정보 + request.setCvlprNm(ownerNm); + request.setCvlprIdecno(idecno); + request.setCvlprStdgCd(legalDongCd); + + // 개인정보공개 (1:소유자공개) + request.setPrvcRls("1"); + + // 경로구분코드 (고정값 3) + request.setPathSeCd("3"); + + // 내역표시 (1:전체내역) + request.setDsctnIndct("1"); + + // 조회구분코드 (1:열람) + request.setInqSeCd("1"); + + return request; + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ComparisonOmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ComparisonOmChecker.java new file mode 100644 index 0000000..ed7edd4 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ComparisonOmChecker.java @@ -0,0 +1,20 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; + +/** + * 미필 비교 로직 인터페이스 + * + *

각 비교 로직 체커는 이 인터페이스를 구현합니다.

+ *

미필의 경우 부과일자(levyCrtrYmd)가 existingData에 포함되어 전달됩니다.

+ */ +public interface ComparisonOmChecker { + + /** + * 비교 로직 실행 + * + * @param existingData 과태료 대상 미필 데이터 (levyCrtrYmd 포함) + * @return 처리상태코드 또는 null (미적용) + */ + String check(CarFfnlgTrgtIncmpVO existingData); +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerCloseWithin31OmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerCloseWithin31OmChecker.java new file mode 100644 index 0000000..8be5dac --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerCloseWithin31OmChecker.java @@ -0,0 +1,236 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 4. 내사종결 검증 - 순수 명의이전 (31일 이내) (미필) + * + *

부과일자 소유자가 상품용이 아니고, 명의이전 전 소유자도 상품용이 아닌 경우

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class OwnerCloseWithin31OmChecker extends AbstractComparisonOmChecker { + + public OwnerCloseWithin31OmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[내사종결-명의이전-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + String step1RprsvOwnrIdecno = step1Record.getRprsvOwnrIdecno(); // 부과일자 기준 대표소유자 회원번호 + + log.info("[내사종결-명의이전-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 부과일자 소유자가 상품용 아님 + if (step1OwnerName != null && step1OwnerName.contains("상품용")) { + log.debug("[내사종결-명의이전-미필] 부과일자 소유자가 상품용 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[내사종결-명의이전-미필] 부과일자 소유자가 상품용 아님 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[내사종결-명의이전-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[내사종결-명의이전-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[내사종결-명의이전-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[내사종결-명의이전-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[내사종결-명의이전-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 검사유효기간 내 명의이전 레코드 찾기 (가장 최근 일자) ========== + log.info("[내사종결-명의이전-미필] 갑부 상세 레코드 검색 시작 - 검사유효기간 시작일: {}, 검사유효기간 종료일: {}", inspVldPrdStart, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate inspVldPrdStartDate = DateUtil.parseDate(inspVldPrdStart); + LocalDate inspVldPrdEndDate = DateUtil.parseDate(inspVldPrdEnd); + LocalDate latestChgDate = null; + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "11" (명의이전) + if (!"11".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: 검사유효기간 시작일 <= CHG_YMD <= 검사유효기간 종료일 + if ((chgDate.isEqual(inspVldPrdStartDate) || chgDate.isAfter(inspVldPrdStartDate)) && + (chgDate.isEqual(inspVldPrdEndDate) || chgDate.isBefore(inspVldPrdEndDate))) { + + // 가장 최근 일자 선택 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + targetRecord = record; + latestChgDate = chgDate; + log.debug("[내사종결-명의이전-미필] 검사유효기간 내 명의이전 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + } + + if (targetRecord == null) { + log.debug("[내사종결-명의이전-미필] 검사유효기간 내 명의이전 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[내사종결-명의이전-미필] 검사유효기간 내 명의이전 발견! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== 명의이전일자 ~ 부과일자 사이의 일수 계산 ========== + LocalDate chgDate = DateUtil.parseDate(targetChgYmd); + LocalDate levyDate = DateUtil.parseDate(levyCrtrYmd); + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(chgDate, levyDate); + + if (daysBetween < 0 || daysBetween > DAYS_THRESHOLD) { + log.debug("[내사종결-명의이전-미필] 명의이전일자가 부과일자의 {}일 이내가 아님 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetChgYmd, levyCrtrYmd, daysBetween); + return null; + } + + log.info("[내사종결-명의이전-미필] 명의이전일자가 부과일자의 {}일 이내 확인 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetChgYmd, levyCrtrYmd, daysBetween); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD) ========== + LocalDate targetDate = DateUtil.parseDate(targetChgYmd); + NewBasicRequest step4Request = createBasicRequest(null, vin, targetDate.format(DATE_FORMATTER)); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetDate.format(DATE_FORMATTER)); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + String step4RprsvOwnrIdecno = step4Record.getRprsvOwnrIdecno(); // 대표소유자 회원번호 + + log.info("[내사종결-명의이전-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // 검사유효기간내 명의변경 소유자와 부과일자의 소유자가 같냐 + if (step4OwnerName == null || !step4RprsvOwnrIdecno.contains(step1RprsvOwnrIdecno)) { + log.debug("[내사종결-명의이전-미필] 명의이전 전 소유자가 상품용 - Step4 소유자명: {}", step4OwnerName); + return null; + } + + log.info("[내사종결-명의이전-미필] 명의이전 전 소유자가 상품용 아님 - 소유자명: {}", step4OwnerName); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildOwnerChangeRemark( + step1Record, step4Record, targetRecord, + vhclno, levyCrtrYmd, inspVldPrdStart, inspVldPrdEnd, daysBetween + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[내사종결-명의이전-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[내사종결-명의이전-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; + + } catch (Exception e) { + log.error("[내사종결-명의이전-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[내사종결-명의이전-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerLevyOver31OmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerLevyOver31OmChecker.java new file mode 100644 index 0000000..83c0b53 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/OwnerLevyOver31OmChecker.java @@ -0,0 +1,236 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 6. 날짜 수정 후 부과 검증 - 순수 명의이전 (31일 초과) (미필) + * + *

부과일자 소유자가 상품용이 아니고, 명의이전 전 소유자도 상품용이 아니며, 31일 초과인 경우

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class OwnerLevyOver31OmChecker extends AbstractComparisonOmChecker { + + public OwnerLevyOver31OmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[날짜수정후부과-명의이전-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + String step1RprsvOwnrIdecno = step1Record.getRprsvOwnrIdecno(); // 부과일자 기준 대표소유자 회원번호 + + log.info("[날짜수정후부과-명의이전-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 부과일자 소유자가 상품용 아님 + if (step1OwnerName != null && step1OwnerName.contains("상품용")) { + log.debug("[날짜수정후부과-명의이전-미필] 부과일자 소유자가 상품용 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[날짜수정후부과-명의이전-미필] 부과일자 소유자가 상품용 아님 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[날짜수정후부과-명의이전-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[날짜수정후부과-명의이전-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[날짜수정후부과-명의이전-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[날짜수정후부과-명의이전-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[날짜수정후부과-명의이전-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 검사유효기간 내 명의이전 레코드 찾기 ========== + log.info("[날짜수정후부과-명의이전-미필] 갑부 상세 레코드 검색 시작 - 검사유효기간 시작일: {}, 검사유효기간 종료일: {}", inspVldPrdStart, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate inspVldPrdStartDate = DateUtil.parseDate(inspVldPrdStart); + LocalDate inspVldPrdEndDate = DateUtil.parseDate(inspVldPrdEnd); + LocalDate latestChgDate = null; + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "11" (명의이전) + if (!"11".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: 검사유효기간 시작일 <= CHG_YMD <= 검사유효기간 종료일 + if ((chgDate.isEqual(inspVldPrdStartDate) || chgDate.isAfter(inspVldPrdStartDate)) && + (chgDate.isEqual(inspVldPrdEndDate) || chgDate.isBefore(inspVldPrdEndDate))) { + + // 가장 최근 일자 선택 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + targetRecord = record; + latestChgDate = chgDate; + log.debug("[내사종결-명의이전-미필] 검사유효기간 내 명의이전 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + } + + if (targetRecord == null) { + log.debug("[날짜수정후부과-명의이전-미필] 검사유효기간 내 명의이전 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[날짜수정후부과-명의이전-미필] 검사유효기간 내 명의이전 발견! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== 명의이전일자 ~ 부과일자 사이의 일수 계산 ========== + LocalDate chgDate = DateUtil.parseDate(targetChgYmd); + LocalDate levyDate = DateUtil.parseDate(levyCrtrYmd); + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(chgDate, levyDate); + + if (daysBetween <= DAYS_THRESHOLD) { + log.debug("[날짜수정후부과-명의이전-미필] 명의이전일자가 부과일자의 {}일 이내임 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetChgYmd, levyCrtrYmd, daysBetween); + return null; + } + + log.info("[날짜수정후부과-명의이전-미필] 명의이전일자가 부과일자의 {}일 초과 확인 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetChgYmd, levyCrtrYmd, daysBetween); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD) ========== + LocalDate targetDate = DateUtil.parseDate(targetChgYmd); + NewBasicRequest step4Request = createBasicRequest(null, vin, targetDate.format(DATE_FORMATTER)); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetDate.format(DATE_FORMATTER)); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + String step4RprsvOwnrIdecno = step4Record.getRprsvOwnrIdecno(); // 대표소유자 회원번호 + + log.info("[날짜수정후부과-명의이전-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // 검사유효기간내 명의변경 소유자와 부과일자의 소유자가 같냐 + if (step4OwnerName == null || !step4RprsvOwnrIdecno.contains(step1RprsvOwnrIdecno)) { + log.debug("[내사종결-명의이전-미필] 명의이전 전 소유자가 상품용 - Step4 소유자명: {}", step4OwnerName); + return null; + } + + log.info("[날짜수정후부과-명의이전-미필] 명의이전 전 소유자가 상품용 아님 - 소유자명: {}", step4OwnerName); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildOwnerChangeRemark( + step1Record, step4Record, targetRecord, + vhclno, levyCrtrYmd, inspVldPrdStart, inspVldPrdEnd, daysBetween + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[날짜수정후부과-명의이전-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[날짜수정후부과-명의이전-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY; + + } catch (Exception e) { + log.error("[날짜수정후부과-명의이전-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[날짜수정후부과-명의이전-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductCloseWithin31OmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductCloseWithin31OmChecker.java new file mode 100644 index 0000000..de0d54f --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductCloseWithin31OmChecker.java @@ -0,0 +1,233 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 3. 내사종결 검증 - 명의이전 이전소유자 상품용, 31일 이내 (미필) + * + *

명의이전 이전 소유자가 상품용이고, 명의이전일자가 부과일자의 31일 이내인 경우

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class ProductCloseWithin31OmChecker extends AbstractComparisonOmChecker { + + public ProductCloseWithin31OmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[내사종결-명의이전 상품용-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전 상품용-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + + log.info("[내사종결-명의이전 상품용-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 조건 1: 소유자명에 "상품용" 포함 여부 확인 + if (step1OwnerName == null || step1OwnerName.contains("상품용")) { + log.debug("[내사종결-명의이전 상품용-미필] 소유자명에 '상품용' 미포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[내사종결-명의이전 상품용-미필] 소유자명에 '상품용' 포함 확인! - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[내사종결-명의이전 상품용-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전 상품용-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[내사종결-명의이전 상품용-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[내사종결-명의이전 상품용-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[내사종결-명의이전 상품용-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[내사종결-명의이전 상품용-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 조건에 맞는 레코드 찾기 ========== + log.info("[내사종결-명의이전 상품용-미필] 갑부 상세 레코드 검색 시작 - 부과일자: {}, 검사유효기간 종료일: {}", levyCrtrYmd, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate latestChgDate = null; + LocalDate levyDate = DateUtil.parseDate(levyCrtrYmd); + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "11" (명의이전) + if (!"11".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: CHG_YMD <= 부과일자 + if (chgDate.isAfter(levyDate)) { + log.debug("[내사종결-명의이전 상품용-미필] CHG_YMD > 부과일자 - 변경일자: {}, 부과일자: {}", chgYmd, levyCrtrYmd); + continue; + } + + // 가장 마지막 일자 찾기 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + latestChgDate = chgDate; + targetRecord = record; + log.debug("[내사종결-명의이전 상품용-미필] 조건 충족 레코드 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + + if (targetRecord == null) { + log.debug("[내사종결-명의이전 상품용-미필] 조건에 맞는 명의이전 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + // 조건: 가장 마지막 명의이전일자가 부과일자의 기준일수 이내인지 확인 + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(latestChgDate, levyDate); + if (daysBetween < 0 || daysBetween > DAYS_THRESHOLD) { + log.debug("[내사종결-명의이전 상품용-미필] 명의이전일자가 부과일자의 {}일 이내가 아님 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetRecord.getChgYmd(), levyCrtrYmd, daysBetween); + return null; + } + log.info("[내사종결-명의이전 상품용-미필] 명의이전일자가 부과일자의 {}일 이내 확인 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetRecord.getChgYmd(), levyCrtrYmd, daysBetween); + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[내사종결-명의이전 상품용-미필] 조건 충족 레코드 선택! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD -1일) ========== + LocalDate targetDate = DateUtil.parseDate(targetChgYmd); + String targetChgYmdMinus1 = targetDate.minusDays(1).format(DATE_FORMATTER); + log.info("[내사종결-명의이전 상품용-미필] Step 4: 자동차기본정보 조회 - 차대번호: {}, 부과일자: {} (원본: {})", vin, targetChgYmdMinus1, targetChgYmd); + + NewBasicRequest step4Request = createBasicRequest(null, vin, targetChgYmdMinus1); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[내사종결-명의이전 상품용-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetChgYmdMinus1); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + + log.info("[내사종결-명의이전 상품용-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // ========== 소유자명 비교 - 상품용 포함 여부 확인 ========== + if (step4OwnerName == null || !step4OwnerName.contains("상품용")) { + log.debug("[내사종결-명의이전 상품용-미필] 소유자명에 '상품용' 미포함 - Step4 소유자명: {}", step4OwnerName); + return null; + } + + log.info("[내사종결-명의이전 상품용-미필] 모든 조건 충족! 차량번호: {}, 변경일자: {}", vhclno, targetChgYmd); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildProductCloseLevyRemark( + step1Record, step4Record, targetRecord, + vhclno, levyCrtrYmd, inspVldPrdStart, inspVldPrdEnd, daysBetween + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[내사종결-명의이전 상품용-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[내사종결-명의이전 상품용-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; + + } catch (Exception e) { + log.error("[내사종결-명의이전 상품용-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[내사종결-명의이전 상품용-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductLevyOver31OmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductLevyOver31OmChecker.java new file mode 100644 index 0000000..fd8eefe --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductLevyOver31OmChecker.java @@ -0,0 +1,235 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 5. 날짜 수정 후 부과 검증 - 명의이전 이전소유자 상품용, 31일 초과 (미필) + * + *

명의이전 이전 소유자가 상품용이고, 명의이전일자가 부과일자의 31일 초과인 경우

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class ProductLevyOver31OmChecker extends AbstractComparisonOmChecker { + + public ProductLevyOver31OmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전 상품용-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 조건 1: 소유자명에 "상품용" 포함 여부 확인 + if (step1OwnerName == null || step1OwnerName.contains("상품용")) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] 소유자명에 '상품용' 미포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[날짜수정후부과-명의이전 상품용-미필] 소유자명에 '상품용' 포함 확인! - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전 상품용-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[날짜수정후부과-명의이전 상품용-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 조건에 맞는 레코드 찾기 ========== + log.info("[날짜수정후부과-명의이전 상품용-미필] 갑부 상세 레코드 검색 시작 - 부과일자: {}, 검사유효기간 종료일: {}", levyCrtrYmd, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate latestChgDate = null; + LocalDate levyDate = DateUtil.parseDate(levyCrtrYmd); + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "11" (명의이전) + if (!"11".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: CHG_YMD <= 부과일자 + if (chgDate.isAfter(levyDate)) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] CHG_YMD > 부과일자 - 변경일자: {}, 부과일자: {}", chgYmd, levyCrtrYmd); + continue; + } + + // 가장 마지막 일자 찾기 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + latestChgDate = chgDate; + targetRecord = record; + log.debug("[날짜수정후부과-명의이전 상품용-미필] 조건 충족 레코드 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + + if (targetRecord == null) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] 조건에 맞는 명의이전 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + // 조건: 가장 마지막 명의이전일자가 부과일자의 기준일수 초과인지 확인 + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(latestChgDate, levyDate); + if (daysBetween <= DAYS_THRESHOLD) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] 명의이전일자가 부과일자의 {}일 이내임 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetRecord.getChgYmd(), levyCrtrYmd, daysBetween); + return null; + } + log.info("[날짜수정후부과-명의이전 상품용-미필] 명의이전일자가 부과일자의 {}일 초과 확인 - 변경일자: {}, 부과일자: {}, 일수차이: {}일", + DAYS_THRESHOLD, targetRecord.getChgYmd(), levyCrtrYmd, daysBetween); + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[날짜수정후부과-미필] 조건 충족 레코드 선택! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD -1일) ========== + LocalDate targetDate = DateUtil.parseDate(targetChgYmd); + String targetChgYmdMinus1 = targetDate.minusDays(1).format(DATE_FORMATTER); + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 4: 자동차기본정보 조회 - 차대번호: {}, 부과일자: {} (원본: {})", vin, targetChgYmdMinus1, targetChgYmd); + + NewBasicRequest step4Request = createBasicRequest(null, vin, targetChgYmdMinus1); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[날짜수정후부과-명의이전 상품용-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetChgYmdMinus1); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + + log.info("[날짜수정후부과-명의이전 상품용-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // ========== 소유자명 비교 - 상품용 포함 여부 확인 ========== + if (step4OwnerName == null || !step4OwnerName.contains("상품용")) { + log.debug("[날짜수정후부과-명의이전 상품용-미필] 소유자명에 '상품용' 미포함 - Step4 소유자명: {}", step4OwnerName); + return null; + } + + log.info("[날짜수정후부과-명의이전 상품용-미필] 명의이전 시점 소유자명에 '상품용' 확인! - 소유자명: {}", step4OwnerName); + + log.info("[날짜수정후부과-명의이전 상품용-미필] 모든 조건 충족! 차량번호: {}, 변경일자: {}", vhclno, targetChgYmd); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildProductCloseLevyRemark( + step1Record, step4Record, targetRecord, + vhclno, levyCrtrYmd, inspVldPrdStart, inspVldPrdEnd, daysBetween + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[날짜수정후부과-명의이전 상품용-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[날짜수정후부과-명의이전 상품용-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY; + + } catch (Exception e) { + log.error("[날짜수정후부과-명의이전 상품용-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[날짜수정후부과-명의이전 상품용-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChangeChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChangeChecker.java new file mode 100644 index 0000000..eefdaad --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChangeChecker.java @@ -0,0 +1,229 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 2. 상품용-변경등록 체크 (미필) + * + *

api-1번호출.소유자명.contains("상품용")

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class ProductUseOmChangeChecker extends AbstractComparisonOmChecker { + + public ProductUseOmChangeChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[상품용-변경등록-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[상품용-변경등록-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + String step1RprsvOwnrIdecno = step1Record.getRprsvOwnrIdecno(); // 부과일자 기준 대표소유자 회원번호 + + log.info("[상품용-변경등록-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 조건 1: 소유자명에 "상품용" 포함 여부 확인 + if (step1OwnerName == null || !step1OwnerName.contains("상품용")) { + log.debug("[상품용-변경등록-미필] 소유자명에 '상품용' 미포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[상품용-변경등록-미필] 소유자명에 '상품용' 포함 확인! - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[상품용-변경등록-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[상품용-변경등록-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[상품용-변경등록-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[상품용-변경등록-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[상품용-변경등록-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[상품용-변경등록-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 조건에 맞는 레코드 찾기 ========== + log.info("[상품용-변경등록-미필] 갑부 상세 레코드 검색 시작 - 부과일자: {}, 검사유효기간 종료일: {}", levyCrtrYmd, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate latestChgDate = null; + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "21" (변경등록) + if (!"21".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: CHG_YMD <= 검사유효기간 종료일 + LocalDate inspVldPrdEndDate = DateUtil.parseDate(inspVldPrdEnd); + if (chgDate.isAfter(inspVldPrdEndDate)) { + log.debug("[상품용-변경등록-미필] CHG_YMD > 검사유효기간 종료일 - 변경일자: {}, 검사유효기간 종료일: {}", chgYmd, inspVldPrdEnd); + continue; + } + + String spcablMttr = record.getSpcablMttr(); + if (!spcablMttr.contains("성명")) { + continue; + } + + // 가장 마지막 일자 찾기 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + latestChgDate = chgDate; + targetRecord = record; + log.debug("[상품용-변경등록-미필] 조건 충족 레코드 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + + if (targetRecord == null) { + log.debug("[상품용-변경등록-미필] 조건에 맞는 변경등록 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[상품용-변경등록-미필] 조건 충족 레코드 선택! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD) ========== + log.info("[상품용-변경등록-미필] Step 4: 자동차기본정보 조회 - 차대번호: {}, 부과일자: {}", vin, targetChgYmd); + + NewBasicRequest step4Request = createBasicRequest(null, vin, targetChgYmd.replace("-", "")); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[상품용-변경등록-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetChgYmd); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + String step4RprsvOwnrIdecno = step4Record.getRprsvOwnrIdecno(); // 대표소유자 회원번호 + + log.info("[상품용-변경등록-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // ========== 소유자 회원번호 비교 ========== + if (step4OwnerName == null || !step4RprsvOwnrIdecno.equals(step1RprsvOwnrIdecno)) { + log.debug("[상품용-변경등록-미필] 소유자 불일치 - Step1 소유자: {}, Step4 소유자: {}", step1RprsvOwnrIdecno, step4RprsvOwnrIdecno); + return null; + } + + log.info("[상품용-변경등록-미필] 소유자 일치 확인! - 소유자명: {}", step1OwnerName); + log.info("[상품용-변경등록-미필] 모든 조건 충족! 차량번호: {}, 변경일자: {}", vhclno, targetChgYmd); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildProductUseChangeRemark( + step1Record, step4Record, targetRecord, + inspVldPrdEnd, levyCrtrYmd + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[상품용-변경등록-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[상품용-변경등록-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE; + + } catch (Exception e) { + log.error("[상품용-변경등록-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[상품용-변경등록-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChecker.java new file mode 100644 index 0000000..942965f --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/ProductUseOmChecker.java @@ -0,0 +1,224 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.request.NewLedgerRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.model.response.NewLedgerResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 1. 상품용 체크 (미필) + * + *

api-1번호출.소유자명.contains("상품용")

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class ProductUseOmChecker extends AbstractComparisonOmChecker { + + public ProductUseOmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + String levyCrtrYmd = existingData.getLevyCrtrYmd(); // 미필: 검사유효기간 종료일 + 145일 + String inspVldPrd = existingData.getInspVldPrd(); // 검사유효기간 + + // 검사유효기간에서 시작일과 종료일 추출 + String inspVldPrdStart = null; + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdStart = dates[0].trim().replace("-", ""); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + try { + // ========== Step 1: 자동차기본정보 조회 (차량번호, 부과일자=검사유효기간 종료일+145일) ========== + log.info("[상품용-미필] Step 1: 자동차기본정보 조회 - 차량번호: {}, 부과일자: {}", vhclno, levyCrtrYmd); + + NewBasicRequest step1Request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse step1Response = apiService.getBasicInfo(step1Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step1Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step1Response == null || step1Response.getRecord() == null || step1Response.getRecord().isEmpty()) { + log.warn("[상품용-미필] Step 1 응답 없음 - 차량번호: {}", vhclno); + return null; + } + + NewBasicResponse.Record step1Record = step1Response.getRecord().get(0); + String vin = step1Record.getVin(); // 차대번호 + String step1OwnerName = step1Record.getRprsOwnrNm(); // 부과일자 기준 소유자명 + String step1RprsvOwnrIdecno = step1Record.getRprsvOwnrIdecno(); // 부과일자 기준 대표소유자 회원번호 + + log.info("[상품용-미필] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); + + // 조건 1: 소유자명에 "상품용" 포함 여부 확인 + if (step1OwnerName == null || !step1OwnerName.contains("상품용")) { + log.debug("[상품용-미필] 소유자명에 '상품용' 미포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + return null; + } + + log.info("[상품용-미필] 소유자명에 '상품용' 포함 확인! - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + + // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== + String today = LocalDate.now().format(DATE_FORMATTER); + log.info("[상품용-미필] Step 2: 자동차기본정보 조회 - 차대번호: {}, 오늘일자: {}", vin, today); + + NewBasicRequest step2Request = createBasicRequest(null, vin, today); + NewBasicResponse step2Response = apiService.getBasicInfo(step2Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step2Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step2Response == null || step2Response.getRecord() == null || step2Response.getRecord().isEmpty()) { + log.warn("[상품용-미필] Step 2 응답 없음 - 차대번호: {}", vin); + return null; + } + + NewBasicResponse.Record step2Record = step2Response.getRecord().get(0); + String currentVhclno = step2Record.getVhrno(); + String currentOwnerName = step2Record.getRprsOwnrNm(); + String currentIdecno = step2Record.getRprsvOwnrIdecno(); + String currentLegalDongCode = step2Record.getUsgsrhldStdgCd(); + + log.info("[상품용-미필] Step 2 결과 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + // ========== Step 3: 자동차등록원부(갑) 조회 ========== + log.info("[상품용-미필] Step 3: 자동차등록원부(갑) 조회 - 차량번호: {}, 성명: {}, 주민번호: {}, 법정동코드: {}", + currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + + NewLedgerRequest step3Request = createLedgerRequest(currentVhclno, currentOwnerName, currentIdecno, currentLegalDongCode); + NewLedgerResponse step3Response = apiService.getLedgerInfo(step3Request); + ledgerLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step3Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step3Response == null) { + log.warn("[상품용-미필] Step 3 응답 없음 - 차량번호: {}", currentVhclno); + return null; + } + + List ledgerRecords = step3Response.getRecord(); + if (ledgerRecords == null || ledgerRecords.isEmpty()) { + log.debug("[상품용-미필] 갑부 상세 내역 없음 - 차량번호: {}", vhclno); + return null; + } + + // ========== 갑부 상세에서 조건에 맞는 레코드 찾기 ========== + log.info("[상품용-미필] 갑부 상세 레코드 검색 시작 - 부과일자: {}, 검사유효기간 종료일: {}", levyCrtrYmd, inspVldPrdEnd); + + NewLedgerResponse.Record targetRecord = null; + LocalDate latestChgDate = null; + + for (NewLedgerResponse.Record record : ledgerRecords) { + String chgYmd = record.getChgYmd(); + String chgTaskSeCd = record.getChgTaskSeCd(); + + // 조건: CHG_TASK_SE_CD == "11" (명의이전) + if (!"11".equals(chgTaskSeCd) || chgYmd == null) { + continue; + } + + LocalDate chgDate = DateUtil.parseDate(chgYmd); + if (chgDate == null) { + continue; + } + + // 조건: CHG_YMD <= 검사유효기간 종료일 + LocalDate inspVldPrdEndDate = DateUtil.parseDate(inspVldPrdEnd); + if (chgDate.isAfter(inspVldPrdEndDate)) { + log.debug("[상품용-미필] CHG_YMD > 검사유효기간 종료일 - 변경일자: {}, 검사유효기간 종료일: {}", chgYmd, inspVldPrdEnd); + continue; + } + + // 가장 마지막 일자 찾기 + if (latestChgDate == null || chgDate.isAfter(latestChgDate)) { + latestChgDate = chgDate; + targetRecord = record; + log.debug("[상품용-미필] 조건 충족 레코드 발견 - 변경일자: {}, 변경업무: {}", chgYmd, chgTaskSeCd); + } + } + + if (targetRecord == null) { + log.debug("[상품용-미필] 조건에 맞는 명의이전 레코드 없음 - 차량번호: {}", vhclno); + return null; + } + + String targetChgYmd = targetRecord.getChgYmd(); + log.info("[상품용-미필] 조건 충족 레코드 선택! 변경일자: {}, 변경업무: {}", targetChgYmd, targetRecord.getChgTaskSeNm()); + + // ========== Step 4: 자동차기본정보 조회 (차대번호, 부과일자=CHG_YMD) ========== + log.info("[상품용-미필] Step 4: 자동차기본정보 조회 - 차대번호: {}, 부과일자: {}", vin, targetChgYmd); + + NewBasicRequest step4Request = createBasicRequest(null, vin, targetChgYmd.replace("-", "")); + NewBasicResponse step4Response = apiService.getBasicInfo(step4Request); + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(step4Response, existingData.getCarFfnlgTrgtIncmpId()); + + if (step4Response == null || step4Response.getRecord() == null || step4Response.getRecord().isEmpty()) { + log.warn("[상품용-미필] Step 4 응답 없음 - 차대번호: {}, 부과일자: {}", vin, targetChgYmd); + return null; + } + + NewBasicResponse.Record step4Record = step4Response.getRecord().get(0); + String step4OwnerName = step4Record.getRprsOwnrNm(); // CHG_YMD 시점의 소유자명 + String step4RprsvOwnrIdecno = step4Record.getRprsvOwnrIdecno(); // 대표소유자 회원번호 + + log.info("[상품용-미필] Step 4 결과 - 소유자명: {}", step4OwnerName); + + // ========== 소유자 회원번호 비교 ========== + if (step4OwnerName == null || !step4RprsvOwnrIdecno.equals(step1RprsvOwnrIdecno)) { + log.debug("[상품용-미필] 소유자 불일치 - Step1 소유자: {}, Step4 소유자: {}", step1RprsvOwnrIdecno, step4RprsvOwnrIdecno); + return null; + } + + log.info("[상품용-미필] 소유자 일치 확인! - 소유자명: {}", step1OwnerName); + log.info("[상품용-미필] 모든 조건 충족! 차량번호: {}, 변경일자: {}", vhclno, targetChgYmd); + + // ========== 비고 생성 ========== + String rmrk = ComparisonOmRemarkBuilder.buildProductUseRemark( + step1Record, step4Record, targetRecord, + inspVldPrdEnd, levyCrtrYmd + ); + + // ========== DB 업데이트 ========== + existingData.setCarBassMatterInqireId(step1Response.getGeneratedId()); + existingData.setCarLedgerFrmbkId(step3Response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(step4OwnerName); + existingData.setCarRegFrmbkChgTaskSeCd(targetRecord.getChgTaskSeCd()); + existingData.setCarRegFrmbkChgTaskSeNm(targetRecord.getChgTaskSeNm()); + existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd().replace("-", "")); + existingData.setCarRegFrmbkDtl(ComparisonOmRemarkBuilder.buildLedgerRecordDetail(targetRecord)); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[상품용-미필] 업데이트 실패: %s", vhclno)); + } + + log.info("[상품용-미필] 처리 완료! 차량번호: {}", vhclno); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE; + + } catch (Exception e) { + log.error("[상품용-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[상품용-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/TransferOmChecker.java b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/TransferOmChecker.java new file mode 100644 index 0000000..cda2a33 --- /dev/null +++ b/src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/TransferOmChecker.java @@ -0,0 +1,165 @@ +package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker; + +import egovframework.constant.TaskPrcsSttsConstants; +import egovframework.exception.MessageException; +import egovframework.util.DateUtil; +import egovframework.util.SessionUtil; +import go.kr.project.api.model.request.NewBasicRequest; +import go.kr.project.api.model.response.NewBasicResponse; +import go.kr.project.api.service.ExternalVehicleApiService; +import go.kr.project.api.service.VmisCarBassMatterInqireLogService; +import go.kr.project.api.service.VmisCarLedgerFrmbkLogService; +import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper; +import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO; +import go.kr.project.carInspectionPenalty.registrationOm.service.impl.ComparisonOmRemarkBuilder; +import go.kr.project.login.model.LoginUserVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +/** + * 7. 이첩 검증 (미필) + * + *

DAYCNT 기반 부과기준일 계산 및 법정동코드 비교

+ *

미필의 경우 부과일자 = 검사유효기간 종료일 + 145일

+ */ +@Slf4j +@Component +public class TransferOmChecker extends AbstractComparisonOmChecker { + + public TransferOmChecker(CarFfnlgTrgtIncmpMapper carFfnlgTrgtIncmpMapper, + ExternalVehicleApiService apiService, + VmisCarBassMatterInqireLogService bassMatterLogService, + VmisCarLedgerFrmbkLogService ledgerLogService) { + super(carFfnlgTrgtIncmpMapper, apiService, bassMatterLogService, ledgerLogService); + } + + @Override + public String check(CarFfnlgTrgtIncmpVO existingData) { + String vhclno = existingData.getVhclno(); + + try { + // TODO : DAYCNT 는 어떻게 가져옴? + // DAYCNT 가져오기 + //String daycntStr = existingData.getDaycnt(); + String daycntStr = "111"; + if (daycntStr == null || daycntStr.isEmpty()) { + log.debug("[이첩-미필] DAYCNT 없음 - 차량번호: {}", vhclno); + return null; + } + + int daycnt = Integer.parseInt(daycntStr); + log.info("[이첩-미필] DAYCNT: {} - 차량번호: {}", daycnt, vhclno); + + // 부과기준일 계산 + String levyCrtrYmd; + String transferType; + String inspVldPrd = existingData.getInspVldPrd(); + + // 검사유효기간에서 종료일 추출 + String inspVldPrdEnd = null; + if (inspVldPrd != null && inspVldPrd.contains("~")) { + String[] dates = inspVldPrd.split("~"); + inspVldPrdEnd = dates.length > 1 ? dates[1].trim().replace("-", "") : null; + } + + if (daycnt > 115) { + // 이첩-2: 부과기준일 = 검사유효기간 종료일 + 115일 + LocalDate inspVldPrdEndDate = DateUtil.parseDate(inspVldPrdEnd); + LocalDate levyCrtrDate = inspVldPrdEndDate.plusDays(115); + levyCrtrYmd = levyCrtrDate.format(DATE_FORMATTER); + transferType = "이첩-2"; + log.info("[이첩-2-미필] 부과기준일 = 검사유효기간 종료일({}) + 115일 = {}", inspVldPrdEnd, levyCrtrYmd); + } else { + // 이첩-1: 부과기준일 = 부과일자 (검사유효기간 종료일 + 145일) + levyCrtrYmd = existingData.getLevyCrtrYmd(); + transferType = "이첩-1"; + log.info("[이첩-1-미필] 부과기준일 = 부과일자 = {}", levyCrtrYmd); + } + + // 자동차기본정보 API 호출 (부과기준일 기준) + log.info("[{}-미필] 자동차기본정보 조회 - 차량번호: {}, 부과기준일: {}", transferType, vhclno, levyCrtrYmd); + + NewBasicRequest request = createBasicRequest(vhclno, null, levyCrtrYmd); + NewBasicResponse response = apiService.getBasicInfo(request); + + // API 응답에 CAR_FFNLG_TRGT_INCMP_ID 업데이트 + bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx(response, existingData.getCarFfnlgTrgtIncmpId()); + + if (response == null || response.getRecord() == null || response.getRecord().isEmpty()) { + log.warn("[{}-미필] 응답 없음 - 차량번호: {}", transferType, vhclno); + return null; + } + + NewBasicResponse.Record record = response.getRecord().get(0); + String usgsrhldStdgCd = record.getUsgsrhldStdgCd(); // 사용본거지법정동코드 + + log.info("[{}-미필] API 응답 - 사용본거지법정동코드: {}", transferType, usgsrhldStdgCd); + + // 법정동코드 유효성 검사 + if (usgsrhldStdgCd == null || usgsrhldStdgCd.length() < 4) { + log.debug("[{}-미필] 법정동코드 없음 - 차량번호: {}", transferType, vhclno); + return null; + } + + // 세션에서 사용자 정보 조회 + LoginUserVO userInfo = SessionUtil.getLoginUser(); + if (userInfo == null || userInfo.getOrgCd() == null) { + log.debug("[{}-미필] 사용자 정보 없음", transferType); + return null; + } + + // 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 비교 + String legalDong4 = usgsrhldStdgCd.substring(0, 4); + String userOrgCd = userInfo.getOrgCd(); + String userOrg4 = userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : userOrgCd; + + if (legalDong4.equals(userOrg4)) { + log.debug("[{}-미필] 법정동코드 일치 - 차량번호: {}, 법정동: {}, 조직: {}", + transferType, vhclno, legalDong4, userOrg4); + return null; + } + + log.info("[{}-미필] 법정동코드 불일치! 차량번호: {}, 법정동: {}, 조직: {}", + transferType, vhclno, legalDong4, userOrg4); + + // 시군구 코드 및 시군구명 조회 + String sggCd = usgsrhldStdgCd.length() >= 5 ? usgsrhldStdgCd.substring(0, 5) : usgsrhldStdgCd; + String sggNm = carFfnlgTrgtIncmpMapper.selectSggNmBySggCd(sggCd); + if (sggNm == null || sggNm.isEmpty()) { + log.warn("[{}-미필] 시군구명 조회 실패 - 시군구코드: {}", transferType, sggCd); + sggNm = ""; + } + + // 비고 생성 + String rmrk; + if ("이첩-1".equals(transferType)) { // 5번 + rmrk = ComparisonOmRemarkBuilder.buildTransferCase1Remark(sggNm, userOrg4); + } else { // 7번 + rmrk = ComparisonOmRemarkBuilder.buildTransferCase2Remark(sggNm, legalDong4); + } + + // DB 업데이트 + existingData.setCarBassMatterInqireId(response.getGeneratedId()); + existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER); + existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER)); + existingData.setCarBscMttrInqFlnm(existingData.getOwnrNm()); + existingData.setCarBscMttrInqSggCd(sggCd); + existingData.setCarBscMttrInqSggNm(sggNm); + existingData.setRmrk(rmrk); + + int updateCount = carFfnlgTrgtIncmpMapper.update(existingData); + if (updateCount == 0) { + throw new MessageException(String.format("[%s-미필] 업데이트 실패: %s", transferType, vhclno)); + } + + log.info("[{}-미필] 처리 완료! 차량번호: {}, 시군구: {}({})", transferType, vhclno, sggNm, sggCd); + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER; + + } catch (Exception e) { + log.error("[이첩-미필] 검증 중 오류 발생 - 차량번호: {}", vhclno, e); + throw new MessageException(String.format("[이첩-미필] 검증 중 오류 발생 - 차량번호: %s", vhclno), e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9490ed0..d5be76b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -145,3 +145,41 @@ car-ffnlg-txt-parse: addr: 86 # 주소 vld-prd-expry-ymd: 12 # 유효기간만료일자 trd-gds: 11 # 매매상품 + +# ===== 자동차 과태료 미필 PRN 파일 파싱 설정 ===== +# hangul-byte-size에 따라 자동으로 적절한 바이트 길이 설정을 선택합니다. +# - hangul-byte-size: 2 → byte-size-2 설정 사용 (EUC-KR, MS949) +# - hangul-byte-size: 3 → byte-size-3 설정 사용 (UTF-8) +# 미필 PRN 파일은 헤더에 프로그램ID, 처리일자, 출력일시가 있고 +# 데이터는 2줄씩 구성됨 +car-ffnlg-prn-parse: + encoding: EUC-KR # 파일 인코딩 (UTF-8, EUC-KR, MS949 등) + hangul-byte-size: 2 # 한글 문자 바이트 크기 (2=EUC-KR/MS949, 3=UTF-8) + + # ===== 2바이트 환경 설정 (EUC-KR, MS949) ===== + byte-size-2: + first-line: # 첫째줄 필드별 바이트 길이 (2바이트 기준) + no: 6 # 번호 + vhclno: 14 # 차량번호 + ownr-nm: 32 # 소유자명 (주민번호 빈칸 포함) + car-nm: 22 # 자동차명 + use-strhld-addr: 62 # 사용본거지주소 + insp-vld-prd: 23 # 검사유효기간 + second-line: # 둘째줄 필드별 바이트 길이 (2바이트 기준) + skip: 38 # 공백 (스킵) + rrno: 16 # 주민등록번호 + use-strhld-addr: -1 # 사용본거지주소 (나머지 전체) + + # ===== 3바이트 환경 설정 (UTF-8) ===== + byte-size-3: + first-line: # 첫째줄 필드별 바이트 길이 (3바이트 기준) + no: 6 # 번호 + vhclno: 14 # 차량번호 + ownr-nm: 32 # 소유자명 (주민번호 빈칸 포함) + car-nm: 22 # 자동차명 + use-strhld-addr: 62 # 사용본거지주소 + insp-vld-prd: 23 # 검사유효기간 + second-line: # 둘째줄 필드별 바이트 길이 (3바이트 기준) + skip: 38 # 공백 (스킵) + rrno: 16 # 주민등록번호 + use-strhld-addr: -1 # 사용본거지주소 (나머지 전체) diff --git a/src/main/resources/mybatis/mapper/api/CarBassMatterInqireMapper_maria.xml b/src/main/resources/mybatis/mapper/api/CarBassMatterInqireMapper_maria.xml index 8cebffd..89f2dfc 100644 --- a/src/main/resources/mybatis/mapper/api/CarBassMatterInqireMapper_maria.xml +++ b/src/main/resources/mybatis/mapper/api/CarBassMatterInqireMapper_maria.xml @@ -149,7 +149,7 @@ WHERE CAR_BASS_MATTER_INQIRE_ID = #{carBassMatterInqireId} - + UPDATE tb_car_bass_matter_inqire SET CAR_FFNLG_TRGT_ID = #{param2} diff --git a/src/main/resources/mybatis/mapper/api/CarLedgerFrmbkMapper_maria.xml b/src/main/resources/mybatis/mapper/api/CarLedgerFrmbkMapper_maria.xml index 35f13c1..585117b 100644 --- a/src/main/resources/mybatis/mapper/api/CarLedgerFrmbkMapper_maria.xml +++ b/src/main/resources/mybatis/mapper/api/CarLedgerFrmbkMapper_maria.xml @@ -184,7 +184,7 @@ WHERE CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId} - + UPDATE tb_car_ledger_frmbk SET CAR_FFNLG_TRGT_ID = #{param2} diff --git a/src/main/resources/mybatis/mapper/carInspectionPenalty/registrationOm/CarFfnlgTrgtIncmpMapper_maria.xml b/src/main/resources/mybatis/mapper/carInspectionPenalty/registrationOm/CarFfnlgTrgtIncmpMapper_maria.xml new file mode 100644 index 0000000..9ddcec6 --- /dev/null +++ b/src/main/resources/mybatis/mapper/carInspectionPenalty/registrationOm/CarFfnlgTrgtIncmpMapper_maria.xml @@ -0,0 +1,309 @@ + + + + + + + + + AND t.RCPT_YMD >= #{schRcptYmdStart} + + + AND t.RCPT_YMD <= #{schRcptYmdEnd} + + + AND t.VHCLNO LIKE CONCAT('%', #{schVhclno}, '%') + + + AND t.OWNR_NM LIKE CONCAT('%', #{schOwnrNm}, '%') + + + AND t.TASK_PRCS_STTS_CD IN + + #{item} + + + + AND t.PRCS_YMD >= #{schPrcsYmdStart} + + + AND t.PRCS_YMD <= #{schPrcsYmdEnd} + + + + + + + + + + + + + + + + + + INSERT INTO tb_car_ffnlg_trgt_incmp ( + CAR_FFNLG_TRGT_INCMP_ID, + RCPT_YMD, + PRGRM_ID, + PRCS_YMD, + OTPT_DT, + NO, + VHCLNO, + OWNR_NM, + RRNO, + CAR_NM, + USE_STRHLD_ADDR, + INSP_VLD_PRD, + TASK_PRCS_STTS_CD, + TASK_PRCS_YMD, + RMRK, + CAR_BASS_MATTER_INQIRE_ID, + CAR_LEDGER_FRMBK_ID, + CAR_BSC_MTTR_INQ_FLNM, + CAR_BSC_MTTR_INQ_SGG_CD, + CAR_BSC_MTTR_INQ_SGG_NM, + CAR_REG_FRMBK_CHG_TASK_SE_CD, + CAR_REG_FRMBK_CHG_TASK_SE_NM, + CAR_REG_FRMBK_CHG_YMD, + CAR_REG_FRMBK_DTL, + REG_DT, + RGTR, + DEL_YN + ) VALUES ( + CONCAT('CFI', LPAD(NEXTVAL(seq_car_ffnlg_trgt_incmp_id), 17, '0')), + #{rcptYmd}, + #{prgrmId}, + #{prcsYmd}, + #{otptDt}, + #{no}, + #{vhclno}, + #{ownrNm}, + ECL_ENCRYPT(#{rrno}), + #{carNm}, + #{useStrhldAddr}, + #{inspVldPrd}, + #{taskPrcsSttsCd}, + #{taskPrcsYmd}, + #{rmrk}, + #{carBassMatterInqireId}, + #{carLedgerFrmbkId}, + #{carBscMttrInqFlnm}, + #{carBscMttrInqSggCd}, + #{carBscMttrInqSggNm}, + #{carRegFrmbkChgTaskSeCd}, + #{carRegFrmbkChgTaskSeNm}, + #{carRegFrmbkChgYmd}, + #{carRegFrmbkDtl}, + NOW(), + #{rgtr}, + 'N' + ) + + + + + UPDATE tb_car_ffnlg_trgt_incmp + SET TASK_PRCS_STTS_CD = #{taskPrcsSttsCd}, + TASK_PRCS_YMD = #{taskPrcsYmd}, + RMRK = #{rmrk}, + CAR_BASS_MATTER_INQIRE_ID = #{carBassMatterInqireId}, + CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId}, + CAR_BSC_MTTR_INQ_FLNM = #{carBscMttrInqFlnm}, + CAR_BSC_MTTR_INQ_SGG_CD = #{carBscMttrInqSggCd}, + CAR_BSC_MTTR_INQ_SGG_NM = #{carBscMttrInqSggNm}, + CAR_REG_FRMBK_CHG_TASK_SE_CD = #{carRegFrmbkChgTaskSeCd}, + CAR_REG_FRMBK_CHG_TASK_SE_NM = #{carRegFrmbkChgTaskSeNm}, + CAR_REG_FRMBK_CHG_YMD = #{carRegFrmbkChgYmd}, + CAR_REG_FRMBK_DTL = #{carRegFrmbkDtl} + WHERE CAR_FFNLG_TRGT_INCMP_ID = #{carFfnlgTrgtIncmpId} + AND DEL_DT IS NULL + + + + + UPDATE tb_car_ffnlg_trgt_incmp + SET TASK_PRCS_STTS_CD = #{taskPrcsSttsCd}, + TASK_PRCS_YMD = #{taskPrcsYmd}, + RMRK = #{rmrk} + WHERE CAR_FFNLG_TRGT_INCMP_ID = #{carFfnlgTrgtIncmpId} + AND DEL_DT IS NULL + + + + + UPDATE tb_car_ffnlg_trgt_incmp + SET DEL_DT = NOW(), + DLTR = #{dltr} + WHERE CAR_FFNLG_TRGT_INCMP_ID = #{carFfnlgTrgtIncmpId} + AND DEL_DT IS NULL + + + + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp index eb1e667..353c386 100644 --- a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp +++ b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp @@ -9,7 +9,7 @@
-
> 지연 과태료 대상 목록
+
+ + +
+
+
+
+
+
    +
  • 접수일자
  • +
  • + ~ + +
  • +
  • 처리일자
  • +
  • + ~ + +
  • +
  • 차량번호
  • +
  • + +
  • +
  • 소유자명
  • +
  • + +
  • +
  • 처리상태
  • +
  • + + + +
  • +
+
    +
  • +
  • +
+
+
+
+
+
+
    +
  • 미필 과태료 대상 목록
  • +
  • + + + + + + 총 0건 + + / Pages +
  • +
+
+
+
+
+
+
+
+
+ + + + + + + diff --git a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registrationOm/uploadPopup.jsp b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registrationOm/uploadPopup.jsp new file mode 100644 index 0000000..eccffdc --- /dev/null +++ b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registrationOm/uploadPopup.jsp @@ -0,0 +1,163 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + +
+

미필 과태료 대상 PRN 파일 업로드

+
+ +
+
+ + +

* PRN 또는 TXT 파일만 업로드 가능합니다.

+

* 업로드 시 UTF-8 → EUC-KR 변환됩니다.

+
+ + +
+ + + +
+ + +
+
+ +