미필 초기 작업 진행중....

main
박성영 1 week ago
parent fdcf38bb31
commit 40d8518a50

@ -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);

@ -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);

@ -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);
}

@ -33,9 +33,9 @@ public interface VmisCarLedgerFrmbkLogService {
void saveDetailsNewTx(String masterId, List<CarLedgerFrmbkDtlVO> 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);
}

@ -82,4 +82,5 @@ public class VmisCarBassMatterInqireLogServiceImpl extends EgovAbstractServiceIm
// 저장 실패해도 비교 로직은 계속 진행
}
}
}

@ -88,4 +88,5 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im
// 저장 실패해도 비교 로직은 계속 진행
}
}
}

@ -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<String, Integer> firstLine;
/**
*
*/
private Map<String, Integer> 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;
}
}

@ -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<CarFfnlgTrgtIncmpVO> 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<String, Object> result = service.uploadAndParsePrnFile(convertedFile, rgtr);
int successCount = (int) result.get("successCount");
int failCount = (int) result.get("failCount");
@SuppressWarnings("unchecked")
List<String> errorMessages = (List<String>) 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<Map<String, String>> targetList) {
log.info("API 호출 및 비교 요청 (미필) - 선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);
try {
Map<String, Object> 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<CarFfnlgTrgtIncmpVO> allData = service.selectList(searchParams);
// 목록을 Map 형태로 변환
List<Map<String, String>> targetList = allData.stream()
.map(vo -> {
Map<String, String> 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<String, Object> 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<String> 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<String, Object> 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<CarFfnlgTrgtIncmpExcelVO> 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);
}
}
}

@ -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<CarFfnlgTrgtIncmpVO> 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<String, Object> result = service.uploadAndParsePrnFile(convertedFile, rgtr);
int successCount = (int) result.get("successCount");
int failCount = (int) result.get("failCount");
@SuppressWarnings("unchecked")
List<String> errorMessages = (List<String>) 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<Map<String, String>> targetList) {
log.info("API 호출 및 비교 요청 (미필) - 선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);
try {
Map<String, Object> 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<CarFfnlgTrgtIncmpVO> allData = service.selectList(searchParams);
// 목록을 Map 형태로 변환
List<Map<String, String>> targetList = allData.stream()
.map(vo -> {
Map<String, String> 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<String, Object> 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<String> 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<String, Object> 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<CarFfnlgTrgtIncmpExcelVO> 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);
}
}
}

@ -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<CarFfnlgTrgtIncmpVO> 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<CarFfnlgTrgtIncmpExcelVO> selectListForExcel(CarFfnlgTrgtIncmpVO vo);
/**
* (OM_DAY_CD D)
* @return (: 145)
*/
String selectOmDayPlusDay();
}

@ -0,0 +1,100 @@
package go.kr.project.carInspectionPenalty.registrationOm.model;
import egovframework.util.excel.ExcelColumn;
import egovframework.util.excel.ExcelSheet;
import lombok.*;
/**
* VO
*
* <p> VO @ExcelColumn </p>
* <p> , </p>
*/
@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;
}

@ -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<CarFfnlgTrgtIncmpVO> createdRows;
private List<CarFfnlgTrgtIncmpVO> updatedRows;
private List<CarFfnlgTrgtIncmpVO> deletedRows;
}

@ -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<String> schTaskPrcsSttsCd; // 검색 업무 처리 상태 코드 (다중 선택 가능)
private String schPrcsYmdStart; // 검색 시작 처리 일자
private String schPrcsYmdEnd; // 검색 종료 처리 일자
// 부과일자 계산용 필드 (검사유효기간 종료일 + 145일)
private String levyCrtrYmd; // 부과기준일자 (API 호출 시 사용)
}

@ -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<CarFfnlgTrgtIncmpVO> 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<String, Object> 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<String, Object> compareWithApi(List<Map<String, String>> targetList);
/**
*
* , , .
*
* @param modifyData // VO
* @return
*/
int saveCarFfnlgTrgtIncmps(CarFfnlgTrgtIncmpModifiedDataVO modifyData);
/**
*
* @param vo
* @return
*/
List<CarFfnlgTrgtIncmpExcelVO> selectListForExcel(CarFfnlgTrgtIncmpVO vo);
}

@ -0,0 +1,30 @@
package go.kr.project.carInspectionPenalty.registrationOm.service;
import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO;
/**
*
*
* <p> API .</p>
* <p> = + 145</p>
*/
public interface ComparisonOmService {
/**
* .
*
* <p> :</p>
* <ol>
* <li> </li>
* <li> </li>
* <li> ...</li>
* </ol>
*
* <p> .</p>
* <p> levyCrtrYmd() .</p>
*
* @param existingData (levyCrtrYmd )
* @return (02=, 03=, null=)
*/
String executeComparison(CarFfnlgTrgtIncmpVO existingData);
}

@ -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<CarFfnlgTrgtIncmpVO> 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<String, Object> uploadAndParsePrnFile(MultipartFile file, String rgtr) {
Map<String, Object> result = new HashMap<>();
List<String> 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<String> 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<String> 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<CarFfnlgTrgtIncmpVO> 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<String, Object> compareWithApi(List<Map<String, String>> targetList) {
log.info("========== 미필 API 호출 및 비교 시작 ==========");
log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);
if (targetList == null || targetList.isEmpty()) {
throw new IllegalArgumentException("선택된 데이터가 없습니다.");
}
List<Map<String, Object>> 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<String, String> target : targetList) {
String carFfnlgTrgtIncmpId = target.get("carFfnlgTrgtIncmpId");
String vhclno = target.get("vhclno");
String inspVldPrd = target.get("inspVldPrd");
log.info("처리 중 - 차량번호: {}, 검사유효기간: {}", vhclno, inspVldPrd);
Map<String, Object> 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<String, Object> 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<CarFfnlgTrgtIncmpVO> 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<CarFfnlgTrgtIncmpVO> 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<CarFfnlgTrgtIncmpVO> 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<CarFfnlgTrgtIncmpExcelVO> 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<String> 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<String> 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<String> validateParsedData(int dataLineNumber, CarFfnlgTrgtIncmpVO vo) {
List<String> 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);
}
}
}

@ -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)
*
* <p> .</p>
* <p> = + 145</p>
*/
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.)
* 222283
* -
*
*
*
*
* @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.)
* 222283
* -
*
*
*
* @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();
}
}

@ -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;
/**
*
*
* <p> .</p>
* <p> = + 145</p>
*/
@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;
/**
*
*
* <p> , .</p>
* <p> (levyCrtrYmd) existingData .</p>
*/
@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;
}
}

@ -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;
/**
*
*
* <p> .</p>
* <p> = + 145</p>
*/
@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;
}
}

@ -0,0 +1,20 @@
package go.kr.project.carInspectionPenalty.registrationOm.service.impl.om_checker;
import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO;
/**
*
*
* <p> .</p>
* <p> (levyCrtrYmd) existingData .</p>
*/
public interface ComparisonOmChecker {
/**
*
*
* @param existingData (levyCrtrYmd )
* @return null ()
*/
String check(CarFfnlgTrgtIncmpVO existingData);
}

@ -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 ) ()
*
* <p> , </p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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 ) ()
*
* <p> , , 31 </p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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 ()
*
* <p> , 31 </p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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 ()
*
* <p> , 31 </p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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. - ()
*
* <p>api-1..contains("상품용")</p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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. ()
*
* <p>api-1..contains("상품용")</p>
* <p> = + 145</p>
*/
@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<NewLedgerResponse.Record> 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);
}
}
}

@ -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. ()
*
* <p>DAYCNT </p>
* <p> = + 145</p>
*/
@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);
}
}
}

@ -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 # 사용본거지주소 (나머지 전체)

@ -149,7 +149,7 @@
WHERE CAR_BASS_MATTER_INQIRE_ID = #{carBassMatterInqireId}
</select>
<!-- txId로 CAR_FFNLG_TRGT_ID 업데이트 -->
<!-- txId로 CAR_FFNLG_TRGT_ID 업데이트 (지연/미필 공통) -->
<update id="updateCarFfnlgTrgtIdByTxId">
UPDATE tb_car_bass_matter_inqire
SET CAR_FFNLG_TRGT_ID = #{param2}

@ -184,7 +184,7 @@
WHERE CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId}
</select>
<!-- txId로 CAR_FFNLG_TRGT_ID 업데이트 -->
<!-- txId로 CAR_FFNLG_TRGT_ID 업데이트 (지연/미필 공통) -->
<update id="updateCarFfnlgTrgtIdByTxId">
UPDATE tb_car_ledger_frmbk
SET CAR_FFNLG_TRGT_ID = #{param2}

@ -0,0 +1,309 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper">
<!-- 공통 검색 조건 -->
<sql id="searchCondition">
<if test='schRcptYmdStart != null and schRcptYmdStart != ""'>
AND t.RCPT_YMD &gt;= #{schRcptYmdStart}
</if>
<if test='schRcptYmdEnd != null and schRcptYmdEnd != ""'>
AND t.RCPT_YMD &lt;= #{schRcptYmdEnd}
</if>
<if test='schVhclno != null and schVhclno != ""'>
AND t.VHCLNO LIKE CONCAT('%', #{schVhclno}, '%')
</if>
<if test='schOwnrNm != null and schOwnrNm != ""'>
AND t.OWNR_NM LIKE CONCAT('%', #{schOwnrNm}, '%')
</if>
<if test='schTaskPrcsSttsCd != null and schTaskPrcsSttsCd.size() > 0'>
AND t.TASK_PRCS_STTS_CD IN
<foreach collection="schTaskPrcsSttsCd" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test='schPrcsYmdStart != null and schPrcsYmdStart != ""'>
AND t.PRCS_YMD &gt;= #{schPrcsYmdStart}
</if>
<if test='schPrcsYmdEnd != null and schPrcsYmdEnd != ""'>
AND t.PRCS_YMD &lt;= #{schPrcsYmdEnd}
</if>
</sql>
<!-- 과태료 대상 미필 목록 총 개수 조회 -->
<select id="selectListTotalCount" parameterType="CarFfnlgTrgtIncmpVO" resultType="int">
SELECT COUNT(*)
FROM tb_car_ffnlg_trgt_incmp t
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
LEFT JOIN (
SELECT CD_ID, CD_NM
FROM tb_cd_detail
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
AND USE_YN = 'Y'
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
WHERE t.DEL_DT IS NULL
<include refid="searchCondition"/>
</select>
<!-- 과태료 대상 미필 목록 조회 -->
<select id="selectList" parameterType="CarFfnlgTrgtIncmpVO" resultType="CarFfnlgTrgtIncmpVO">
SELECT
t.CAR_FFNLG_TRGT_INCMP_ID AS carFfnlgTrgtIncmpId,
t.RCPT_YMD AS rcptYmd,
t.PRGRM_ID AS prgrmId,
t.PRCS_YMD AS prcsYmd,
t.OTPT_DT AS otptDt,
t.NO AS no,
t.VHCLNO AS vhclno,
t.OWNR_NM AS ownrNm,
ECL_DECRYPT(t.RRNO) AS rrno,
t.CAR_NM AS carNm,
t.USE_STRHLD_ADDR AS useStrhldAddr,
t.INSP_VLD_PRD AS inspVldPrd,
t.TASK_PRCS_STTS_CD AS taskPrcsSttsCd,
t.TASK_PRCS_YMD AS taskPrcsYmd,
t.RMRK AS rmrk,
t.CAR_BASS_MATTER_INQIRE_ID AS carBassMatterInqireId,
t.CAR_LEDGER_FRMBK_ID AS carLedgerFrmbkId,
t.CAR_BSC_MTTR_INQ_FLNM AS carBscMttrInqFlnm,
t.CAR_BSC_MTTR_INQ_SGG_CD AS carBscMttrInqSggCd,
t.CAR_BSC_MTTR_INQ_SGG_NM AS carBscMttrInqSggNm,
t.CAR_REG_FRMBK_CHG_TASK_SE_CD AS carRegFrmbkChgTaskSeCd,
t.CAR_REG_FRMBK_CHG_TASK_SE_NM AS carRegFrmbkChgTaskSeNm,
t.CAR_REG_FRMBK_CHG_YMD AS carRegFrmbkChgYmd,
t.CAR_REG_FRMBK_DTL AS carRegFrmbkDtl,
t.REG_DT AS regDt,
t.RGTR AS rgtr,
t.DEL_YN AS delYn,
t.DEL_DT AS delDt,
t.DLTR AS dltr,
cd.CD_NM AS taskPrcsSttsCdNm,
u.USER_NM AS rgtrNm
FROM tb_car_ffnlg_trgt_incmp t
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
LEFT JOIN (
SELECT CD_ID, CD_NM
FROM tb_cd_detail
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
AND USE_YN = 'Y'
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
WHERE t.DEL_DT IS NULL
<include refid="searchCondition"/>
ORDER BY t.CAR_FFNLG_TRGT_INCMP_ID ASC
<if test='pagingYn == "Y"'>
limit #{startIndex}, #{perPage} /* 서버사이드 페이징 처리 */
</if>
</select>
<!-- 과태료 대상 미필 상세 조회 -->
<select id="selectOne" parameterType="CarFfnlgTrgtIncmpVO" resultType="CarFfnlgTrgtIncmpVO">
SELECT
t.CAR_FFNLG_TRGT_INCMP_ID AS carFfnlgTrgtIncmpId,
t.RCPT_YMD AS rcptYmd,
t.PRGRM_ID AS prgrmId,
t.PRCS_YMD AS prcsYmd,
t.OTPT_DT AS otptDt,
t.NO AS no,
t.VHCLNO AS vhclno,
t.OWNR_NM AS ownrNm,
ECL_DECRYPT(t.RRNO) AS rrno,
t.CAR_NM AS carNm,
t.USE_STRHLD_ADDR AS useStrhldAddr,
t.INSP_VLD_PRD AS inspVldPrd,
t.TASK_PRCS_STTS_CD AS taskPrcsSttsCd,
t.TASK_PRCS_YMD AS taskPrcsYmd,
t.RMRK AS rmrk,
t.CAR_BASS_MATTER_INQIRE_ID AS carBassMatterInqireId,
t.CAR_LEDGER_FRMBK_ID AS carLedgerFrmbkId,
t.CAR_BSC_MTTR_INQ_FLNM AS carBscMttrInqFlnm,
t.CAR_BSC_MTTR_INQ_SGG_CD AS carBscMttrInqSggCd,
t.CAR_BSC_MTTR_INQ_SGG_NM AS carBscMttrInqSggNm,
t.CAR_REG_FRMBK_CHG_TASK_SE_CD AS carRegFrmbkChgTaskSeCd,
t.CAR_REG_FRMBK_CHG_TASK_SE_NM AS carRegFrmbkChgTaskSeNm,
t.CAR_REG_FRMBK_CHG_YMD AS carRegFrmbkChgYmd,
t.CAR_REG_FRMBK_DTL AS carRegFrmbkDtl,
t.REG_DT AS regDt,
t.RGTR AS rgtr,
t.DEL_YN AS delYn,
t.DEL_DT AS delDt,
t.DLTR AS dltr,
cd.CD_NM AS taskPrcsSttsCdNm,
u.USER_NM AS rgtrNm
FROM tb_car_ffnlg_trgt_incmp t
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
LEFT JOIN (
SELECT CD_ID, CD_NM
FROM tb_cd_detail
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
AND USE_YN = 'Y'
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
WHERE t.CAR_FFNLG_TRGT_INCMP_ID = #{carFfnlgTrgtIncmpId}
AND t.DEL_DT IS NULL
</select>
<!-- 과태료 대상 미필 목록 엑셀 다운로드용 조회 -->
<select id="selectListForExcel" parameterType="CarFfnlgTrgtIncmpVO" resultType="CarFfnlgTrgtIncmpExcelVO">
SELECT
DATE_FORMAT(STR_TO_DATE(t.RCPT_YMD, '%Y%m%d'), '%Y-%m-%d') AS rcptYmd,
t.PRGRM_ID AS prgrmId,
t.PRCS_YMD AS prcsYmd,
t.NO AS no,
t.VHCLNO AS vhclno,
t.OWNR_NM AS ownrNm,
ECL_DECRYPT(t.RRNO) AS rrno,
t.CAR_NM AS carNm,
t.USE_STRHLD_ADDR AS useStrhldAddr,
t.INSP_VLD_PRD AS inspVldPrd,
cd.CD_NM AS taskPrcsSttsCdNm,
DATE_FORMAT(STR_TO_DATE(t.TASK_PRCS_YMD, '%Y%m%d'), '%Y-%m-%d') AS taskPrcsYmd,
t.RMRK AS rmrk,
t.CAR_BSC_MTTR_INQ_FLNM AS carBscMttrInqFlnm,
t.CAR_BSC_MTTR_INQ_SGG_NM AS carBscMttrInqSggNm,
t.CAR_REG_FRMBK_CHG_TASK_SE_NM AS carRegFrmbkChgTaskSeNm,
DATE_FORMAT(STR_TO_DATE(t.CAR_REG_FRMBK_CHG_YMD, '%Y%m%d'), '%Y-%m-%d') AS carRegFrmbkChgYmd,
t.CAR_REG_FRMBK_DTL AS carRegFrmbkDtl,
DATE_FORMAT(t.REG_DT, '%Y-%m-%d %H:%i:%s') AS regDt,
u.USER_NM AS rgtrNm
FROM tb_car_ffnlg_trgt_incmp t
LEFT JOIN tb_user u ON t.RGTR = u.USER_ID
LEFT JOIN (
SELECT CD_ID, CD_NM
FROM tb_cd_detail
WHERE CD_GROUP_ID = 'TASK_PRCS_STTS_CD'
AND USE_YN = 'Y'
) cd ON t.TASK_PRCS_STTS_CD = cd.CD_ID
WHERE t.DEL_DT IS NULL
<include refid="searchCondition"/>
ORDER BY t.CAR_FFNLG_TRGT_INCMP_ID ASC
</select>
<!-- 과태료 대상 미필 등록 -->
<insert id="insert" parameterType="CarFfnlgTrgtIncmpVO">
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'
)
</insert>
<!-- 과태료 대상 미필 수정 -->
<update id="update" parameterType="CarFfnlgTrgtIncmpVO">
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>
<!-- 과태료 대상 미필의 처리상태와 비고만 수정 -->
<update id="updateTaskPrcsSttsCdAndRmrk" parameterType="CarFfnlgTrgtIncmpVO">
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>
<!-- 과태료 대상 미필 삭제 (논리삭제) -->
<update id="delete" parameterType="CarFfnlgTrgtIncmpVO">
UPDATE tb_car_ffnlg_trgt_incmp
SET DEL_DT = NOW(),
DLTR = #{dltr}
WHERE CAR_FFNLG_TRGT_INCMP_ID = #{carFfnlgTrgtIncmpId}
AND DEL_DT IS NULL
</update>
<!-- 차량번호와 검사유효기간 중복 체크 -->
<select id="checkDuplicateVhclno" parameterType="CarFfnlgTrgtIncmpVO" resultType="int">
SELECT COUNT(*)
FROM tb_car_ffnlg_trgt_incmp
WHERE VHCLNO = #{vhclno}
AND INSP_VLD_PRD = #{inspVldPrd}
AND DEL_DT IS NULL
</select>
<!-- 시군구 코드로 시군구명 조회 -->
<select id="selectSggNmBySggCd" parameterType="String" resultType="String">
SELECT SGG_NM
FROM tb_sgg_cd
WHERE SGG_CD = #{sggCd}
AND DEL_YN = 'N'
LIMIT 1
</select>
<!-- 미필 부과일자 가산일 조회 (OM_DAY_CD 코드의 D값) -->
<select id="selectOmDayPlusDay" resultType="String">
SELECT CD_NM AS plusDay
FROM tb_cd_detail
WHERE CD_GROUP_ID = 'OM_DAY_CD'
AND CD_ID = 'D'
AND USE_YN = 'Y'
LIMIT 1
</select>
</mapper>

@ -9,7 +9,7 @@
<section id="section8" class="main_bars">
<div class="bgs-main">
<section id="section5">
<div class="sub_title"> &gt; 지연 과태료 대상 목록</div>
<div class="sub_title"></div>
<button type="button" id="registerBtn" class="newbtn bg1">PRN 등록</button>
<button type="button" id="downloadBtn" class="newbtn bg3 iconz">
<span class="mdi mdi-file-document"></span>PRN 다운로드

@ -0,0 +1,901 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dateUtil" uri="http://egovframework.go.kr/functions/date-util" %>
<!-- Main body -->
<div class="main_body">
<section id="section8" class="main_bars">
<div class="bgs-main">
<section id="section5">
<div class="sub_title"></div>
<button type="button" id="registerBtn" class="newbtn bg1">PRN 등록</button>
<button type="button" id="downloadBtn" class="newbtn bg3 iconz">
<span class="mdi mdi-file-document"></span>PRN 다운로드
</button>
<button type="button" id="excelDownloadBtn" class="newbtn bg3 iconz">
<span class="mdi mdi-microsoft-excel"></span>엑셀 다운로드
</button>
</section>
</div>
</section>
<div class="contants_body">
<div class="gs_b_top">
<ul class="lef">
<li class="th">접수일자</li>
<li>
<input type="text" id="schRcptYmdStart" name="schRcptYmdStart" class="input calender datepicker" style="width: 120px;" autocomplete="off" value="${dateUtil:getCurrentDateAddDays('yyyy-MM-dd', -15)}"/> ~
<input type="text" id="schRcptYmdEnd" name="schRcptYmdEnd" class="input calender datepicker" style="width: 120px;" autocomplete="off" value="${dateUtil:getCurrentDateTime('yyyy-MM-dd')}"/>
</li>
<li class="th">처리일자</li>
<li>
<input type="text" id="schPrcsYmdStart" name="schPrcsYmdStart" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/> ~
<input type="text" id="schPrcsYmdEnd" name="schPrcsYmdEnd" class="input calender datepicker" style="width: 120px;" autocomplete="off" value=""/>
</li>
<li class="th">차량번호</li>
<li>
<input type="text" id="schVhclno" name="schVhclno" class="input" style="width: 150px;" maxlength="30" autocomplete="off" placeholder="예: 12가3456"/>
</li>
<li class="th">소유자명</li>
<li>
<input type="text" id="schOwnrNm" name="schOwnrNm" class="input" style="width: 150px;" maxlength="75" autocomplete="off"/>
</li>
<li class="th">처리상태</li>
<li>
<c:forEach var="code" items="${taskPrcsSttsCdList}">
<label style="margin-right: 10px; cursor: pointer;">
<input type="checkbox" name="schTaskPrcsSttsCd" value="${code.cdId}" class="schTaskPrcsSttsCdCheckbox"/>
${code.cdNm}
</label>
</c:forEach>
</li>
</ul>
<ul class="rig2">
<li><button type="button" id="search_btn" class="newbtnss bg1">검색</button></li>
<li><button type="button" id="reset_btn" class="newbtnss bg5" style="margin-left: 5px;">초기화</button></li>
</ul>
</div>
<div class="gs_booking">
<div class="row">
<div class="col-sm-12">
<div class="box_column">
<ul class="box_title" style="display: flex; justify-content: space-between; align-items: center;">
<li class="tit">미필 과태료 대상 목록</li>
<li class="rig">
<button type="button" id="callApiAllBtn" class="newbtn bg2-1">검색조건 전체 API 호출</button>
<button type="button" id="callApiBtn" class="newbtn bg2-1">선택 API 호출</button>
<button type="button" id="deleteBtn" class="newbtn bg6">선택 삭제</button>
<button type="button" id="btn_cancel" class="newbtn bg6">수정 취소</button>
<button type="button" id="btn_save" class="newbtn bg4">저장</button>
<span id="totalCount" class="total-count" style="padding-left: 25px;padding-right: 25px;">총 0건</span>
<select id="perPageSelect" class="input" style="width: 112px; ">
<option value="15">페이지당 15</option>
<option value="50">페이지당 50</option>
<option value="100">페이지당 100</option>
</select>
<span class="page_number"><span id="currentPage"></span><span class="bar">/</span><span id="totalPages"></span> Pages</span>
</li>
</ul>
<div class="containers">
<div id="grid"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /Main body -->
<!-- 비고 레이어 팝업 -->
<div id="rmrkLayerPopup" style="display: none; position: fixed; z-index: 10000; background: white; border: 2px solid #2196F3; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 400px; max-width: 600px;">
<div id="rmrkPopupHeader" style="background: #2196F3; color: white; padding: 12px 15px; cursor: move; border-radius: 6px 6px 0 0; display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold;">비고 상세</span>
<span id="rmrkPopupClose" onclick="closeRmrkPopup()" style="cursor: pointer; font-size: 20px; line-height: 1;">&times;</span>
</div>
<div style="padding: 20px; max-height: 400px; overflow-y: auto;">
<pre id="rmrkPopupContent" style="white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: inherit; font-size: 14px; line-height: 1.6;"></pre>
</div>
</div>
<script type="text/javascript">
/**
* 미필 과태료 대상 목록 관리 모듈
*/
(function(window, $) {
'use strict';
var SEARCH_COND = {};
var LAST_GRID_SEARCH_COND = {};
var GRID_PAGINATION_INFO = {
totalCount: 0,
page: 0,
perPage: 0
};
// 검색정보 설정
var setSearchCond = function() {
var schRcptYmdStart = $.trim(nvl($("#schRcptYmdStart").val(), ""));
var schRcptYmdEnd = $.trim(nvl($("#schRcptYmdEnd").val(), ""));
var schPrcsYmdStart = $.trim(nvl($("#schPrcsYmdStart").val(), ""));
var schPrcsYmdEnd = $.trim(nvl($("#schPrcsYmdEnd").val(), ""));
var schVhclno = $.trim(nvl($("#schVhclno").val(), ""));
var schOwnrNm = $.trim(nvl($("#schOwnrNm").val(), ""));
var schTaskPrcsSttsCd = [];
$("input[name='schTaskPrcsSttsCd']:checked").each(function() {
schTaskPrcsSttsCd.push($(this).val());
});
SEARCH_COND.schRcptYmdStart = schRcptYmdStart.replace(/-/g, '');
SEARCH_COND.schRcptYmdEnd = schRcptYmdEnd.replace(/-/g, '');
SEARCH_COND.schPrcsYmdStart = schPrcsYmdStart.replace(/-/g, '');
SEARCH_COND.schPrcsYmdEnd = schPrcsYmdEnd.replace(/-/g, '');
SEARCH_COND.schVhclno = schVhclno;
SEARCH_COND.schOwnrNm = schOwnrNm;
SEARCH_COND.schTaskPrcsSttsCd = schTaskPrcsSttsCd;
};
// 다운로드 URL 생성
var buildDownloadUrl = function() {
setSearchCond();
var baseUrl = '<c:url value="/carInspectionPenalty/registration-om/download.do"/>';
var params = [];
if (SEARCH_COND.schRcptYmdStart) params.push('schRcptYmdStart=' + encodeURIComponent(SEARCH_COND.schRcptYmdStart));
if (SEARCH_COND.schRcptYmdEnd) params.push('schRcptYmdEnd=' + encodeURIComponent(SEARCH_COND.schRcptYmdEnd));
if (SEARCH_COND.schPrcsYmdStart) params.push('schPrcsYmdStart=' + encodeURIComponent(SEARCH_COND.schPrcsYmdStart));
if (SEARCH_COND.schPrcsYmdEnd) params.push('schPrcsYmdEnd=' + encodeURIComponent(SEARCH_COND.schPrcsYmdEnd));
if (SEARCH_COND.schVhclno) params.push('schVhclno=' + encodeURIComponent(SEARCH_COND.schVhclno));
if (SEARCH_COND.schOwnrNm) params.push('schOwnrNm=' + encodeURIComponent(SEARCH_COND.schOwnrNm));
if (SEARCH_COND.schTaskPrcsSttsCd && SEARCH_COND.schTaskPrcsSttsCd.length > 0) {
SEARCH_COND.schTaskPrcsSttsCd.forEach(function(val) {
params.push('schTaskPrcsSttsCd=' + encodeURIComponent(val));
});
}
return baseUrl + (params.length ? ('?' + params.join('&')) : '');
};
/**
* 미필 과태료 대상 목록 관리 네임스페이스
*/
var CarFfnlgTrgtIncmpList = {
selectedRow: null,
grid: {
instance: null,
initConfig: function() {
var dataSource = this.createDataSource();
var perPage = parseInt($('#perPageSelect').val() || 15, 10);
var gridConfig = new XitTuiGridConfig();
gridConfig.setOptDataSource(dataSource);
gridConfig.setOptGridId('grid');
gridConfig.setOptGridHeight(470);
gridConfig.setOptRowHeight(30);
gridConfig.setOptRowHeaderType('checkbox');
gridConfig.setOptUseClientSort(false);
gridConfig.setOptPageOptions({
useClient: false,
perPage: perPage
});
gridConfig.setOptColumnOptions({
frozenCount: 5,
frozenBorderWidth: 2,
resizable: true
});
gridConfig.setOptColumns(this.getGridColumns());
return gridConfig;
},
getGridColumns: function() {
return [
{
header: '번호',
name: '_rowNum',
align: 'center',
width: 60,
sortable: false,
formatter: function(e) {
var totalCount = GRID_PAGINATION_INFO.totalCount;
var page = GRID_PAGINATION_INFO.page;
var perPage = GRID_PAGINATION_INFO.perPage;
var rowIndex = e.row.rowKey;
return totalCount - (page - 1) * perPage - rowIndex;
}
},
{ header: '접수일자', name: 'rcptYmd', align: 'center', width: 100,
formatter: function(e) {
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
}
},
{ header: '프로그램ID', name: 'prgrmId', align: 'center', width: 100 },
{ header: '처리일자', name: 'prcsYmd', align: 'center', width: 200 },
{ header: '차량번호', name: 'vhclno', align: 'center', width: 100 },
{ header: '소유자명', name: 'ownrNm', align: 'center', width: 100 },
{ header: '주민등록번호', name: 'rrno', align: 'center', width: 130 },
{ header: '자동차명', name: 'carNm', align: 'left', width: 150 },
{ header: '사용본거지주소', name: 'useStrhldAddr', align: 'left', width: 250 },
{ header: '검사유효기간', name: 'inspVldPrd', align: 'center', width: 200 },
{ header: '업무처리일자', name: 'taskPrcsYmd', align: 'center', width: 100,
formatter: function(e) {
return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : '';
}
},
{
header: '처리상태',
name: 'taskPrcsSttsCd',
align: 'center',
width: 100,
editor: {
type: 'select',
options: {
listItems: [
<c:forEach var="code" items="${taskPrcsSttsCdList}" varStatus="status">
{text: '${code.cdNm}', value: '${code.cdId}'}<c:if test="${!status.last}">,</c:if>
</c:forEach>
]
}
},
formatter: function(props) {
var value = props.value;
var codeList = [
<c:forEach var="code" items="${taskPrcsSttsCdList}" varStatus="status">
{id: '${code.cdId}', nm: '${code.cdNm}'}<c:if test="${!status.last}">,</c:if>
</c:forEach>
];
var code = codeList.find(function(c) { return c.id === value; });
return code ? code.nm : value;
}
},
{
header: '비고',
name: 'rmrk',
align: 'left',
width: 200,
editor: 'text',
formatter: function(e) {
var rmrk = e.value || '';
var displayText = rmrk.length > 30 ? rmrk.substring(0, 30) + '...' : rmrk;
if (rmrk && rmrk.trim()) {
var escapedRmrk = rmrk.replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '');
return '<span class="mdi mdi-note-text" style="color: #2196F3; cursor: pointer; margin-right: 5px;" onclick="showRmrkPopup(\'' + escapedRmrk + '\')"></span>' +
'<span style="cursor: pointer;" onclick="showRmrkPopup(\'' + escapedRmrk + '\')">' + displayText + '</span>';
}
return displayText;
}
},
{ header: '기본사항조회성명', name: 'carBscMttrInqFlnm', align: 'center', width: 100 },
{ header: '기본사항조회시군구명', name: 'carBscMttrInqSggNm', align: 'center', width: 120 },
{ header: '등록원부변경업무명', name: 'carRegFrmbkChgTaskSeNm', align: 'center', width: 120 },
{ header: '등록원부변경일자', name: 'carRegFrmbkChgYmd', align: 'center', width: 120 },
{ header: '등록원부상세', name: 'carRegFrmbkDtl', align: 'left', width: 250 },
{ header: '미필과태료대상ID', name: 'carFfnlgTrgtIncmpId', align: 'center', width: 180 },
{ header: '등록일시', name: 'regDt', align: 'center', width: 150 },
{ header: '등록자', name: 'rgtrNm', align: 'center', width: 100 }
];
},
createDataSource: function() {
return {
api: {
readData: {
url: '<c:url value="/carInspectionPenalty/registration-om/list.ajax"/>',
method: 'POST',
contentType: 'application/x-www-form-urlencoded',
processData: true
}
},
initialRequest: false,
serializer: function(params) {
setSearchCond();
SEARCH_COND.perPage = params.perPage;
SEARCH_COND.page = params.page;
return $.param(SEARCH_COND);
}
};
},
create: function() {
var gridConfig = this.initConfig();
var Grid = tui.Grid;
this.instance = gridConfig.instance(Grid);
Grid.applyTheme('striped');
this.gridBindEvents();
},
gridBindEvents: function() {
var self = this;
this.instance.on('successResponse', function(ev) {
var responseObj = JSON.parse(ev.xhr.response);
if(responseObj){
$("#currentPage").text(responseObj.data.pagination.page);
$("#totalPages").text(responseObj.data.pagination.totalPages);
var totalCount = responseObj.data.pagination.totalCount;
$("#totalCount").text('총 ' + totalCount.toLocaleString() + '건');
GRID_PAGINATION_INFO.totalCount = responseObj.data.pagination.totalCount;
GRID_PAGINATION_INFO.page = responseObj.data.pagination.page;
GRID_PAGINATION_INFO.perPage = responseObj.data.pagination.perPage;
}
CarFfnlgTrgtIncmpList.selectedRow = null;
});
this.instance.on('check', function(ev) {
var rowKey = ev.rowKey;
CarFfnlgTrgtIncmpList.selectedRow = self.instance.getRow(rowKey);
});
this.instance.on('uncheck', function(ev) {
CarFfnlgTrgtIncmpList.selectedRow = null;
});
},
reload: function() {
setSearchCond();
LAST_GRID_SEARCH_COND = Object.assign({}, SEARCH_COND);
this.instance.readData(1);
},
cancelChanges: function() {
var modifiedRows = this.instance.getModifiedRows();
if (modifiedRows.createdRows.length === 0 &&
modifiedRows.updatedRows.length === 0 &&
modifiedRows.deletedRows.length === 0) {
alert('취소할 변경사항이 없습니다.');
return;
}
if (confirm('모든 변경사항을 취소하시겠습니까?')) {
var currentPage = this.instance.getPagination().getCurrentPage();
this.instance.readData(currentPage);
}
},
saveData: function() {
var self = this;
var modifiedRows = this.instance.getModifiedRows();
if (modifiedRows.createdRows.length === 0 &&
modifiedRows.updatedRows.length === 0 &&
modifiedRows.deletedRows.length === 0) {
alert('저장할 데이터가 없습니다.');
return;
}
if (confirm("변경된 내용을 저장하시겠습니까?")) {
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration-om/saveAll.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(modifiedRows),
success: function(response) {
if (response.result) {
alert(response.message);
self.instance.readData(1);
} else {
alert(response.message);
}
},
error: function(xhr, status, error) {
console.error("저장 실패:", error);
alert("저장 중 오류가 발생했습니다.");
}
});
}
}
},
init: function() {
this.grid.create();
this.bindEvents();
this.grid.reload();
},
bindEvents: function() {
var self = this;
$("#search_btn").on('click', function() {
self.grid.reload();
});
$("#reset_btn").on('click', function() {
$("#schRcptYmdStart").val("${dateUtil:getCurrentDateAddDays('yyyy-MM-dd', -15)}");
$("#schRcptYmdEnd").val("${dateUtil:getCurrentDateTime('yyyy-MM-dd')}");
$("#schPrcsYmdStart").val("");
$("#schPrcsYmdEnd").val("");
$("#schVhclno").val("");
$("#schOwnrNm").val("");
$("input[name='schTaskPrcsSttsCd']").prop('checked', false);
self.grid.reload();
});
$("#registerBtn").on('click', function() {
self.openUploadPopup();
});
$("#deleteBtn").on('click', function() {
self.deleteData();
});
$("#callApiBtn").on('click', function() {
self.callApiAndCompare();
});
$("#callApiAllBtn").on('click', function() {
self.callApiAndCompareAll();
});
$("#downloadBtn").on('click', function() {
var url = buildDownloadUrl();
window.location.href = url;
});
$("#excelDownloadBtn").on('click', function() {
var $form = $('<form>', {
method: 'POST',
action: '<c:url value="/carInspectionPenalty/registration-om/excel.do"/>'
});
setSearchCond();
$.each(SEARCH_COND, function(key, value) {
if (key === 'schTaskPrcsSttsCd') {
if (value && value.length > 0) {
value.forEach(function(val) {
$form.append($('<input>', { type: 'hidden', name: key, value: val }));
});
}
} else if (value) {
$form.append($('<input>', { type: 'hidden', name: key, value: value }));
}
});
$form.appendTo('body').submit().remove();
});
$("#perPageSelect").on('change', function() {
var perPage = parseInt($(this).val(), 10);
self.grid.instance.setPerPage(perPage);
self.grid.reload();
});
$(".gs_b_top input").on('keypress', function(e) {
if (e.which === 13) {
self.grid.reload();
}
});
$('#btn_cancel').on('click', function() {
self.grid.cancelChanges();
});
$('#btn_save').on('click', function() {
self.grid.saveData();
});
},
openUploadPopup: function() {
var popupUrl = '<c:url value="/carInspectionPenalty/registration-om/uploadPopup.do"/>';
var popup = openPopup(popupUrl, 800, 450, 'uploadPopup');
var checkPopupClosed = setInterval(function() {
if (popup && popup.closed) {
clearInterval(checkPopupClosed);
CarFfnlgTrgtIncmpList.grid.reload();
}
}, 500);
},
deleteData: function() {
var checkedRows = this.grid.instance.getCheckedRows();
if (checkedRows.length === 0) {
alert("삭제할 데이터를 선택해주세요.");
return;
}
if (!confirm(checkedRows.length + "건의 데이터를 삭제하시겠습니까?")) {
return;
}
var deleteIds = checkedRows.map(function(row) {
return row.carFfnlgTrgtIncmpId;
});
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration-om/deleteBatch.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(deleteIds),
success: function(response) {
if (response.success) {
alert("삭제가 완료되었습니다.");
CarFfnlgTrgtIncmpList.grid.reload();
} else {
alert("삭제 중 오류가 발생했습니다: " + response.message);
}
},
error: function(xhr, status, error) {
console.error("삭제 실패:", error);
}
});
},
callApiAndCompare: function() {
var checkedRows = this.grid.instance.getCheckedRows();
if (checkedRows.length === 0) {
alert("API 호출할 데이터를 선택해주세요.");
return;
}
var nonRcptRows = checkedRows.filter(function(row) {
return row.taskPrcsSttsCd !== '01';
});
if (nonRcptRows.length > 0) {
alert("접수 상태(01)인 데이터만 API 호출이 가능합니다.\n접수 상태가 아닌 데이터가 " + nonRcptRows.length + "건 포함되어 있습니다.");
return;
}
if (!confirm(checkedRows.length + "건의 데이터에 대해 API를 호출하고 비교하시겠습니까?\n(미필: 부과일자 = 검사유효기간 종료일 + 145일)")) {
return;
}
var targetList = checkedRows.map(function(row) {
return {
carFfnlgTrgtIncmpId: row.carFfnlgTrgtIncmpId,
vhclno: row.vhclno,
inspVldPrd: row.inspVldPrd,
ownrNm: row.ownrNm,
carNm: row.carNm
};
});
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration-om/compareWithApi.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(targetList),
success: function(response) {
if (response.success) {
alert("API 호출 및 비교가 완료되었습니다.\n\n" + response.message);
CarFfnlgTrgtIncmpList.grid.reload();
} else {
alert("오류: " + response.message);
}
},
error: function(xhr, status, error) {
console.error("API 호출 실패:", error);
}
});
},
callApiAndCompareAll: function() {
if (!confirm("현재 검색조건의 전체 데이터에 대해 API를 호출하고 비교하시겠습니까?\n(미필: 부과일자 = 검사유효기간 종료일 + 145일)")) {
return;
}
setSearchCond();
var params = Object.assign({}, SEARCH_COND, { pagingYn: 'N' });
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration-om/compareWithApiAll.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(params),
success: function(response) {
if (response.success) {
alert("API 호출 및 비교가 완료되었습니다.\n\n" + response.message);
CarFfnlgTrgtIncmpList.grid.reload();
} else {
alert("오류: " + response.message);
}
},
error: function(xhr, status, error) {
console.error("API 호출 실패:", error);
}
});
}
};
$(document).ready(function() {
CarFfnlgTrgtIncmpList.init();
});
// 전역에 노출
window.CarFfnlgTrgtIncmpList = CarFfnlgTrgtIncmpList;
})(window, jQuery);
/**
* 비고 레이어 팝업 열기 (전역 함수)
*/
function showRmrkPopup(rmrkText) {
var $popup = $('#rmrkLayerPopup');
var $content = $('#rmrkPopupContent');
rmrkText = rmrkText.replace(/\\n/g, '\n').replace(/\\'/g, "'");
$content.text(rmrkText);
$popup.css({ display: 'block', transform: 'none' });
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var popupWidth = $popup.outerWidth();
var popupHeight = $popup.outerHeight();
$popup.css({
left: (windowWidth - popupWidth) / 2 + 'px',
top: (windowHeight - popupHeight) / 2 + 'px'
});
}
function closeRmrkPopup() {
$('#rmrkLayerPopup').hide();
}
$(document).on('click', function(e) {
var $popup = $('#rmrkLayerPopup');
if ($popup.is(':visible') && !$(e.target).closest('#rmrkLayerPopup, .mdi-note-text').length) {
closeRmrkPopup();
}
});
// 엑셀 다운로드 버튼
$('#btnExcel').click(function() {
downloadExcel();
});
// 선택 API 비교 버튼
$('#btnApiCompare').click(function() {
compareWithApi();
});
// 전체 API 비교 버튼
$('#btnApiCompareAll').click(function() {
compareWithApiAll();
});
// 저장 버튼
$('#btnSave').click(function() {
saveAll();
});
// 선택 삭제 버튼
$('#btnDelete').click(function() {
deleteSelected();
});
}
function searchList() {
var params = $('#searchForm').serializeArray();
var paramObj = {};
$.each(params, function(i, field) {
if (field.name === 'schTaskPrcsSttsCd') {
if (!paramObj[field.name]) {
paramObj[field.name] = [];
}
paramObj[field.name].push(field.value);
} else {
paramObj[field.name] = field.value;
}
});
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/list.ajax',
type: 'POST',
data: paramObj,
success: function(response) {
if (response.success) {
grid.resetData(response.data);
grid.refreshLayout();
} else {
alert('조회 실패: ' + response.message);
}
},
error: function(xhr, status, error) {
alert('조회 중 오류가 발생했습니다.');
}
});
}
function openUploadPopup() {
var popup = window.open(
contextPath + '/carInspectionPenalty/registration-om/uploadPopup.do',
'uploadPopup',
'width=500,height=300,resizable=yes,scrollbars=yes'
);
// 팝업 닫힘 감지 (업로드 완료 후 목록 새로고침)
var pollTimer = setInterval(function() {
if (popup.closed) {
clearInterval(pollTimer);
searchList();
}
}, 500);
}
function downloadPrn() {
var params = $('#searchForm').serialize();
window.location.href = contextPath + '/carInspectionPenalty/registration-om/download.do?' + params;
}
function downloadExcel() {
var params = $('#searchForm').serialize();
// form submit 방식으로 변경
var form = $('<form>', {
action: contextPath + '/carInspectionPenalty/registration-om/excel.do',
method: 'POST'
});
var searchParams = $('#searchForm').serializeArray();
$.each(searchParams, function(i, field) {
form.append($('<input>', {
type: 'hidden',
name: field.name,
value: field.value
}));
});
form.appendTo('body').submit().remove();
}
function compareWithApi() {
var checkedRows = grid.getCheckedRows();
if (checkedRows.length === 0) {
alert('API 비교할 데이터를 선택해주세요.');
return;
}
// 접수(01) 상태인 것만 필터링
var targetList = checkedRows.filter(function(row) {
return row.taskPrcsSttsCd === '01';
}).map(function(row) {
return {
carFfnlgTrgtIncmpId: row.carFfnlgTrgtIncmpId,
vhclno: row.vhclno,
inspVldPrd: row.inspVldPrd,
ownrNm: row.ownrNm,
carNm: row.carNm
};
});
if (targetList.length === 0) {
alert('접수 상태(01)인 데이터만 API 비교가 가능합니다.');
return;
}
if (!confirm(targetList.length + '건의 데이터에 대해 API 비교를 수행하시겠습니까?\n(미필: 부과일자 = 검사유효기간 종료일 + 145일)')) {
return;
}
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/compareWithApi.ajax',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(targetList),
success: function(response) {
if (response.success) {
alert(response.message);
searchList();
} else {
alert('API 비교 실패: ' + response.message);
}
},
error: function(xhr, status, error) {
alert('API 비교 중 오류가 발생했습니다.');
}
});
}
function compareWithApiAll() {
if (!confirm('검색 조건에 해당하는 전체 데이터에 대해 API 비교를 수행하시겠습니까?\n(미필: 부과일자 = 검사유효기간 종료일 + 145일)')) {
return;
}
var params = {};
var searchParams = $('#searchForm').serializeArray();
$.each(searchParams, function(i, field) {
if (field.name === 'schTaskPrcsSttsCd') {
if (!params[field.name]) {
params[field.name] = [];
}
params[field.name].push(field.value);
} else {
params[field.name] = field.value;
}
});
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/compareWithApiAll.ajax',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(params),
success: function(response) {
if (response.success) {
alert(response.message);
searchList();
} else {
alert('전체 API 비교 실패: ' + response.message);
}
},
error: function(xhr, status, error) {
alert('전체 API 비교 중 오류가 발생했습니다.');
}
});
}
function saveAll() {
var modifiedData = grid.getModifiedRows();
if (modifiedData.createdRows.length === 0 &&
modifiedData.updatedRows.length === 0 &&
modifiedData.deletedRows.length === 0) {
alert('저장할 데이터가 없습니다.');
return;
}
if (!confirm('수정된 데이터를 저장하시겠습니까?')) {
return;
}
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/saveAll.ajax',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(modifiedData),
success: function(response) {
if (response.success) {
alert(response.message);
searchList();
} else {
alert('저장 실패: ' + response.message);
}
},
error: function(xhr, status, error) {
alert('저장 중 오류가 발생했습니다.');
}
});
}
function deleteSelected() {
var checkedRows = grid.getCheckedRows();
if (checkedRows.length === 0) {
alert('삭제할 데이터를 선택해주세요.');
return;
}
if (!confirm(checkedRows.length + '건의 데이터를 삭제하시겠습니까?')) {
return;
}
var deleteIds = checkedRows.map(function(row) {
return row.carFfnlgTrgtIncmpId;
});
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/deleteBatch.ajax',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(deleteIds),
success: function(response) {
if (response.success) {
alert(response.message);
searchList();
} else {
alert('삭제 실패: ' + response.message);
}
},
error: function(xhr, status, error) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
// 날짜 포맷 함수
function formatDate(value) {
if (!value) return '';
var str = String(value).replace(/[^0-9]/g, '');
if (str.length === 8) {
return str.substring(0, 4) + '-' + str.substring(4, 6) + '-' + str.substring(6, 8);
}
return value;
}
// 주민등록번호 마스킹 함수
function maskRrno(value) {
if (!value) return '';
var str = String(value).replace(/[^0-9]/g, '');
if (str.length >= 6) {
return str.substring(0, 6) + '-*******';
}
return value;
}
</script>

@ -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" %>
<style>
.upload-area {
padding: 20px;
}
.upload-form {
margin-bottom: 20px;
}
.file-input-wrapper {
margin-bottom: 15px;
}
.file-info {
margin-top: 10px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.btn-area {
text-align: center;
margin-top: 20px;
}
.upload-result {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
}
.upload-result.success {
background: #dff0d8;
color: #3c763d;
}
.upload-result.error {
background: #f2dede;
color: #a94442;
}
</style>
<div class="upload-area">
<h4><i class="fa fa-upload"></i> 미필 과태료 대상 PRN 파일 업로드</h4>
<hr>
<form id="uploadForm" enctype="multipart/form-data">
<div class="file-input-wrapper">
<label for="file">PRN 파일 선택</label>
<input type="file" class="form-control" id="file" name="file" accept=".prn,.txt">
<p class="help-block">* PRN 또는 TXT 파일만 업로드 가능합니다.</p>
<p class="help-block">* 업로드 시 UTF-8 → EUC-KR 변환됩니다.</p>
</div>
<div id="fileInfo" class="file-info" style="display: none;">
<strong>선택된 파일:</strong> <span id="fileName"></span><br>
<strong>파일 크기:</strong> <span id="fileSize"></span>
</div>
</form>
<div id="uploadResult" class="upload-result" style="display: none;"></div>
<div class="btn-area">
<button type="button" class="btn btn-primary" id="btnUpload" disabled>
<i class="fa fa-upload"></i> 업로드
</button>
<button type="button" class="btn btn-default" id="btnClose">
<i class="fa fa-times"></i> 닫기
</button>
</div>
</div>
<script>
var contextPath = '${pageContext.request.contextPath}';
$(document).ready(function() {
// 파일 선택 이벤트
$('#file').change(function() {
var file = this.files[0];
if (file) {
$('#fileName').text(file.name);
$('#fileSize').text(formatFileSize(file.size));
$('#fileInfo').show();
$('#btnUpload').prop('disabled', false);
$('#uploadResult').hide();
} else {
$('#fileInfo').hide();
$('#btnUpload').prop('disabled', true);
}
});
// 업로드 버튼 클릭
$('#btnUpload').click(function() {
uploadFile();
});
// 닫기 버튼 클릭
$('#btnClose').click(function() {
window.close();
});
});
function uploadFile() {
var file = $('#file')[0].files[0];
if (!file) {
alert('파일을 선택해주세요.');
return;
}
// 파일 확장자 검사
var ext = file.name.split('.').pop().toLowerCase();
if (ext !== 'prn' && ext !== 'txt') {
alert('PRN 또는 TXT 파일만 업로드 가능합니다.');
return;
}
var formData = new FormData();
formData.append('file', file);
$('#btnUpload').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 업로드 중...');
$.ajax({
url: contextPath + '/carInspectionPenalty/registration-om/upload.ajax',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$('#uploadResult')
.removeClass('error')
.addClass('success')
.html('<i class="fa fa-check"></i> ' + response.message.replace(/\n/g, '<br>'))
.show();
// 성공 시 파일 입력 초기화
$('#file').val('');
$('#fileInfo').hide();
} else {
$('#uploadResult')
.removeClass('success')
.addClass('error')
.html('<i class="fa fa-times"></i> ' + response.message.replace(/\n/g, '<br>'))
.show();
}
},
error: function(xhr, status, error) {
$('#uploadResult')
.removeClass('success')
.addClass('error')
.html('<i class="fa fa-times"></i> 업로드 중 오류가 발생했습니다.')
.show();
},
complete: function() {
$('#btnUpload').prop('disabled', false).html('<i class="fa fa-upload"></i> 업로드');
}
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
var k = 1024;
var sizes = ['Bytes', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
Loading…
Cancel
Save