TASK_PRCS_STTS_CD 01~04 까지 src/main/java/egovframework/constant 아래에 class 추가하여 공통관리

현재 검색조건 list 전체를 paging 처리 없이 전체 api 호출하는 로직 추가

apiResponse.isSuccess() == true 일 경우, [tb_car_ffnlg_trgt.CAR_BASS_MATTER_INQIRE_ID, tb_car_ffnlg_trgt.CAR_LEDGER_FRMBK_ID] 값 update
internalApi
박성영 4 weeks ago
parent 8eeee86a46
commit b012e51aaf

@ -1,113 +0,0 @@
package egovframework.constant;
/**
* packageName : go.kr.project.common.constant
* fileName : BatchConstants
* author :
* date : 2025-06-10
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-06-10
*/
public class BatchConstants {
/**
* (TB_BATCH_JOB_INFO.STATUS)
*/
public static final String JOB_INFO_STATUS_ACTIVE = "ACTIVE";
public static final String JOB_INFO_STATUS_PAUSED = "PAUSED";
public static final String JOB_INFO_STATUS_DELETED = "DELETED";
/**
* (TB_BATCH_JOB_EXECUTION.STATUS)
* - ,,,
*/
public static final String JOB_EXECUTION_STATUS_STARTED = "STARTED";
public static final String JOB_EXECUTION_STATUS_COMPLETED = "COMPLETED";
public static final String JOB_EXECUTION_STATUS_FAILED = "FAILED";
public static final String JOB_EXECUTION_STATUS_VETOED = "VETOED";
/**
* (TB_BATCH_JOB_EXECUTION.EXIT_CODE)
*/
public static final String JOB_EXECUTION_EXIT_COMPLETED = "COMPLETED";
public static final String JOB_EXECUTION_EXIT_FAILED = "FAILED";
public static final String JOB_EXECUTION_EXIT_UNKNOWN = "UNKNOWN";
public static final String JOB_EXECUTION_EXIT_PARTIALLY_COMPLETED = "PARTIALLY_COMPLETED";
/**
*
*/
public static final String LOG_LEVEL_INFO = "INFO";
public static final String LOG_LEVEL_WARN = "WARN";
public static final String LOG_LEVEL_ERROR = "ERROR";
/**
*
*/
public static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss";
/**
* ()
*/
public static final long TIME_ITEM_PROCESS_DELAY = 100; // 0.1초
public static final long TIME_JOB_DELAY_TEST = 3000; // 3초 (테스트용)
public static final long TIME_JOB_DELAY_1_MIN = 60000; // 1분
public static final long TIME_JOB_DELAY_3_MIN = 180000; // 3분
public static final long TIME_JOB_DELAY_5_MIN = 300000; // 5분
/**
* JobDataMap
*/
public static final String JOB_DATA_EXECUTION_ID = "executionId";
/**
*
*/
public static final String JOB_NAME_SAMPLE = "sampleJob";
public static final String JOB_GROUP_SAMPLE = "sampleGroup";
public static final String JOB_TRIGGER_SUFFIX = "Trigger";
/**
* Cron
*/
public static final String CRON_EVERY_MINUTE = "0 * * * * ?";
public static final String CRON_EVERY_ONE_SECOND = "1 0 * * * ?";
/**
*
*/
public static final String JOB_DESC_SAMPLE = "샘플 배치 작업 (매 분마다 실행)";
/**
* 릿
*/
// 기본 배치 작업 로그 메시지
public static final String LOG_MSG_BATCH_JOB_START = "===== 배치 작업 시작: %s.%s - %s =====";
public static final String LOG_MSG_BATCH_JOB_COMPLETE = "===== 배치 작업 완료: %s.%s - %s =====";
public static final String LOG_MSG_BATCH_JOB_PARTIAL_COMPLETE = "===== 배치 작업 부분 완료: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_BATCH_JOB_FAILED = "===== 배치 작업 실패: %s.%s - 총 %d개 파일 중 처리: %d개, 성공: %d개, 실패: %d개 (%s) =====";
public static final String LOG_MSG_BATCH_JOB_ERROR = "===== 배치 작업 실행 중 오류 발생: %s.%s - %s =====";
/**
*
*/
public static final String LOG_MSG_BATCH_INIT_SERVER_BOOT_DETECTED = "서버 부팅 완료 감지 - 배치 작업 초기화를 5초 후에 시작합니다.";
public static final String LOG_MSG_BATCH_INIT_START = "배치 작업 초기화 시작";
public static final String LOG_MSG_BATCH_INIT_COMPLETE = "배치 작업 초기화 완료";
public static final String LOG_MSG_BATCH_INIT_INTERRUPT_ERROR = "배치 작업 초기화 대기 중 인터럽트 발생: {}";
public static final String LOG_MSG_BATCH_INIT_ERROR = "배치 작업 초기화 중 오류 발생: %s";
public static final String LOG_MSG_BATCH_INIT_NO_JOBS = "등록된 배치 작업이 없습니다.";
public static final String LOG_MSG_BATCH_INIT_JOBS_COUNT = "총 {}개의 배치 작업 정보를 조회했습니다.";
public static final String LOG_MSG_BATCH_INIT_SKIP_DELETED = "삭제된 작업은 스케줄러에 등록하지 않습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_SUCCESS = "배치 작업을 스케줄러에 등록했습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_FAILED = "배치 작업 스케줄링에 실패했습니다: {}.{}";
public static final String LOG_MSG_BATCH_INIT_CLASS_NOT_FOUND = "배치 작업 클래스를 찾을 수 없습니다: {}";
public static final String LOG_MSG_BATCH_INIT_SCHEDULE_ERROR = "배치 작업 스케줄링 중 오류 발생: {}.{} - {}";
public static final String LOG_MSG_BATCH_INIT_DB_ERROR = "배치 작업 초기화 중 오류 발생: {}";
public static final String LOG_MSG_BATCH_INIT_REGISTER_COMPLETE = "배치 작업 정보 등록 완료: %s";
public static final String LOG_MSG_BATCH_INIT_REGISTER_ERROR = "샘플 배치 작업 등록 중 오류 발생: %s";
}

@ -1,59 +0,0 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : CrdnPrcsSttsConstants
* author :
* date : 2025-08-26
* description :
* : ,
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-26
*/
public class CrdnPrcsSttsConstants {
/**
* - 10:
*
*/
public static final String CRDN_PRCS_STTS_CD_10_CRDN = "10";
/**
* - 20:
*
*/
public static final String CRDN_PRCS_STTS_CD_20_DSPS_BFHD = "20";
/**
* - 30:
*
*/
public static final String CRDN_PRCS_STTS_CD_30_CRC_CMD = "30";
/**
* - 40:
*
*/
public static final String CRDN_PRCS_STTS_CD_40_CRC_URG = "40";
/**
* - 50:
*
*/
public static final String CRDN_PRCS_STTS_CD_50_LEVY_PRVNTC = "50";
/**
* - 60:
*
*/
public static final String CRDN_PRCS_STTS_CD_60_LEVY = "60";
/**
* - 70:
*
*/
public static final String CRDN_PRCS_STTS_CD_70_PAY_URG = "70";
}

@ -1,106 +0,0 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : ImpltTaskSeConstants
* author :
* date : 2025-09-08
* description :
* : ,
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-09-08
*/
public class ImpltTaskSeConstants {
/**
* - 1:
*
*/
public static final String IMPLT_TASK_SE_CD_1_DSPS_BFHD = "1";
/**
* - 2:
*
*/
public static final String IMPLT_TASK_SE_CD_2_CRC_CMD = "2";
/**
* - 3:
*
*/
public static final String IMPLT_TASK_SE_CD_3_CRC_URG = "3";
/**
* - 4:
*
*/
public static final String IMPLT_TASK_SE_CD_4_LEVY_PRVNTC = "4";
/**
* - 5:
*
*/
public static final String IMPLT_TASK_SE_CD_5_LEVY = "5";
/**
* - 6:
*
*/
public static final String IMPLT_TASK_SE_CD_6_PAY_URG = "6";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_DSPS_BFHD = "처분사전";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_CRC_CMD = "시정명령";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_CRC_URG = "시정촉구";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_LEVY_PRVNTC = "부과예고";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_LEVY = "부과";
/**
* -
*/
public static final String IMPLT_TASK_SE_CD_NM_PAY_URG = "납부촉구";
/**
*
*/
public static final String IMPLT_TASK_SE_CD_NM_UNKNOWN = "알수없음";
/**
* .
*
* @param impltTaskSeCd
* @return
*/
public static String getImpltTaskSeCdNm(String impltTaskSeCd) {
switch (impltTaskSeCd) {
case IMPLT_TASK_SE_CD_1_DSPS_BFHD: return IMPLT_TASK_SE_CD_NM_DSPS_BFHD;
case IMPLT_TASK_SE_CD_2_CRC_CMD: return IMPLT_TASK_SE_CD_NM_CRC_CMD;
case IMPLT_TASK_SE_CD_3_CRC_URG: return IMPLT_TASK_SE_CD_NM_CRC_URG;
case IMPLT_TASK_SE_CD_4_LEVY_PRVNTC: return IMPLT_TASK_SE_CD_NM_LEVY_PRVNTC;
case IMPLT_TASK_SE_CD_5_LEVY: return IMPLT_TASK_SE_CD_NM_LEVY;
case IMPLT_TASK_SE_CD_6_PAY_URG: return IMPLT_TASK_SE_CD_NM_PAY_URG;
default: return IMPLT_TASK_SE_CD_NM_UNKNOWN;
}
}
}

@ -1,18 +0,0 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : SEQConstants
* author :
* date : 25. 8. 26.
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 25. 8. 26.
*/
public class SEQConstants {
public static final String SEQ_CRDN = "seq_crdn_no_";
}

@ -0,0 +1,41 @@
package egovframework.constant;
/**
* packageName : egovframework.constant
* fileName : TaskPrcsSttsConstants
* author :
* date : 2025-11-17
* description :
* :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-11-17
*/
public class TaskPrcsSttsConstants {
/**
* - 01:
*
*/
public static final String TASK_PRCS_STTS_CD_01_RCPT = "01";
/**
* - 02:
* API "상품용"
*/
public static final String TASK_PRCS_STTS_CD_02_PRODUCT_USE = "02";
/**
* - 03:
* API
*/
public static final String TASK_PRCS_STTS_CD_03_TRANSFER = "03";
/**
* - 04:
* API
*/
public static final String TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED = "04";
}

@ -56,6 +56,7 @@ public class InternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl impl
private final VmisCarBassMatterInqireService carBassMatterInqireService;
private final VmisCarLedgerFrmbkService carLedgerFrmbkService;
private final go.kr.project.carInspectionPenalty.history.service.VehicleApiHistoryService vehicleApiHistoryService;
@Override
@ -97,6 +98,19 @@ public class InternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl impl
response.setSuccess(true);
response.setMessage("조회 성공");
log.info("[Internal Mode] 차량번호 {} 조회 성공", vehicleNumber);
// 4. API 호출 성공 시 히스토리 ID 조회 및 설정
try {
String carBassMatterInqireId = vehicleApiHistoryService.selectLatestCarBassMatterInqireIdByVhclno(vehicleNumber);
String carLedgerFrmbkId = vehicleApiHistoryService.selectLatestCarLedgerFrmbkIdByVhclno(vehicleNumber);
response.setCarBassMatterInqireId(carBassMatterInqireId);
response.setCarLedgerFrmbkId(carLedgerFrmbkId);
log.debug("[Internal Mode] 히스토리 ID 설정 완료 - 차량번호: {}, 기본정보ID: {}, 원부ID: {}",
vehicleNumber, carBassMatterInqireId, carLedgerFrmbkId);
} catch (Exception e) {
log.warn("[Internal Mode] 히스토리 ID 조회 실패 - 차량번호: {}", vehicleNumber, e);
// ID 조회 실패는 치명적이지 않으므로 계속 진행
}
} else {
response.setSuccess(false);
response.setMessage(basicInfo != null ? basicInfo.getCntcResultDtls() : "조회 실패");

@ -24,6 +24,9 @@ public class VehicleApiResponseVO {
private BasicResponse basicInfo; // 차량 기본정보
private LedgerResponse ledgerInfo; // 등록원부 정보
private String carBassMatterInqireId; // 자동차 기본 사항 조회 ID
private String carLedgerFrmbkId; // 자동차 등록 원부 갑 ID
public VehicleApiResponseVO() {
this.success = true;
}

@ -91,4 +91,20 @@ public interface VehicleApiHistoryMapper {
* @return ()
*/
CarLedgerFrmbkDtlVO selectCarLedgerFrmbkDtlOne(String carLedgerFrmbkDtlId);
/**
* ID .
*
* @param vhclno
* @return ID
*/
String selectLatestCarBassMatterInqireIdByVhclno(String vhclno);
/**
* () ID .
*
* @param vhclno
* @return ID
*/
String selectLatestCarLedgerFrmbkIdByVhclno(String vhclno);
}

@ -89,4 +89,20 @@ public interface VehicleApiHistoryService {
* @return ()
*/
CarLedgerFrmbkDtlVO selectCarLedgerFrmbkDtlOne(String carLedgerFrmbkDtlId);
/**
* ID .
*
* @param vhclno
* @return ID
*/
String selectLatestCarBassMatterInqireIdByVhclno(String vhclno);
/**
* () ID .
*
* @param vhclno
* @return ID
*/
String selectLatestCarLedgerFrmbkIdByVhclno(String vhclno);
}

@ -84,4 +84,16 @@ public class VehicleApiHistoryServiceImpl extends EgovAbstractServiceImpl implem
log.debug("자동차 등록원부(갑) 상세 조회 이력 상세 조회 - ID: {}", carLedgerFrmbkDtlId);
return mapper.selectCarLedgerFrmbkDtlOne(carLedgerFrmbkDtlId);
}
@Override
public String selectLatestCarBassMatterInqireIdByVhclno(String vhclno) {
log.debug("차량번호로 최신 자동차 기본정보 조회 이력 ID 조회 - 차량번호: {}", vhclno);
return mapper.selectLatestCarBassMatterInqireIdByVhclno(vhclno);
}
@Override
public String selectLatestCarLedgerFrmbkIdByVhclno(String vhclno) {
log.debug("차량번호로 최신 자동차 등록원부(갑) 조회 이력 ID 조회 - 차량번호: {}", vhclno);
return mapper.selectLatestCarLedgerFrmbkIdByVhclno(vhclno);
}
}

@ -22,6 +22,7 @@ import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -293,4 +294,101 @@ public class CarFfnlgTrgtController {
return ApiResponseUtil.error("처리 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* API / ( )
* @param searchParams
* @return
*/
@PostMapping("/compareWithApiAll.ajax")
@ResponseBody
@Operation(summary = "검색조건 전체 API 호출 및 데이터 비교", description = "검색조건에 해당하는 전체 목록에 대해 차량 API를 호출하고 비교합니다.")
public ResponseEntity<?> compareWithApiAll(@RequestBody CarFfnlgTrgtVO searchParams) {
log.info("전체 API 호출 및 비교 요청 - 검색 조건: {}", searchParams);
try {
// 페이징 비활성화
searchParams.setPagingYn("N");
// 전체 목록 조회
List<CarFfnlgTrgtVO> allData = service.selectList(searchParams);
// 목록을 Map 형태로 변환
List<Map<String, String>> targetList = allData.stream()
.map(vo -> {
Map<String, String> map = new HashMap<>();
map.put("carFfnlgTrgtId", vo.getCarFfnlgTrgtId());
map.put("vhclno", vo.getVhclno());
map.put("inspYmd", vo.getInspYmd());
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 {
CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
vo.setCarFfnlgTrgtId(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());
}
}
}

@ -1,5 +1,6 @@
package go.kr.project.carInspectionPenalty.registration.service.impl;
import egovframework.constant.TaskPrcsSttsConstants;
import egovframework.exception.MessageException;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
@ -191,7 +192,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
}
// 업무 처리 상태 및 등록자 설정
vo.setTaskPrcsSttsCd("01"); // 01=접수
vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT); // 01=접수
vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
vo.setRcptYmd(LocalDate.now().format(DATE_FORMATTER)); // 접수일자는 현재 날짜
vo.setRgtr(rgtr);
@ -961,7 +962,13 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
continue;
}
// 2.5. API 호출 성공 시 히스토리 ID 업데이트
if (apiResponse.getCarBassMatterInqireId() != null || apiResponse.getCarLedgerFrmbkId() != null) {
existingData.setCarBassMatterInqireId(apiResponse.getCarBassMatterInqireId());
existingData.setCarLedgerFrmbkId(apiResponse.getCarLedgerFrmbkId());
log.info("API 히스토리 ID 업데이트 - 차량번호: {}, 기본정보ID: {}, 원부ID: {}",
vhclno, apiResponse.getCarBassMatterInqireId(), apiResponse.getCarLedgerFrmbkId());
}
// 3. 비교 로직 실행
String statusCode = comparisonService.executeComparison(existingData, apiResponse, rgtr);
@ -969,15 +976,15 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if ("02".equals(statusCode)) {
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
productUseCount++;
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if ("03".equals(statusCode)) {
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
transferCount++;
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if ("04".equals(statusCode)) {
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
normalCount++;
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
@ -999,9 +1006,12 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
} catch (Exception e) {
log.error("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno, e);
throw new MessageException(String.format("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno), e);
/*
compareResult.put("success", false);
compareResult.put("message", "비교 중 오류: " + e.getMessage());
failCount++;
*/
}
compareResults.add(compareResult);

@ -1,5 +1,6 @@
package go.kr.project.carInspectionPenalty.registration.service.impl;
import egovframework.constant.TaskPrcsSttsConstants;
import egovframework.exception.MessageException;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.response.BasicResponse;
@ -94,7 +95,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
log.info("[상품용] 조건 충족! 차량번호: {}, 소유자명: {}", vhclno, mberNm);
// DB 업데이트
existingData.setTaskPrcsSttsCd("02"); // 상품용
existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE); // 상품용
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(mberNm); // 소유자명 저장
existingData.setCarBscMttrInqSggCd(null);
@ -106,7 +107,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
}
log.info("[상품용] 처리 완료! 차량번호: {}", vhclno);
return "02";
return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE;
}
/**
@ -192,7 +193,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
}
// DB 업데이트
existingData.setTaskPrcsSttsCd("03"); // 이첩
existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER); // 이첩
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(null);
existingData.setCarBscMttrInqSggCd(sggCd); // 시군구 코드
@ -204,7 +205,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
}
log.info("[이첩] 처리 완료! 차량번호: {}, 시군구: {}({}), 사유: {}", vhclno, sggNm, sggCd, reason);
return "03";
return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER;
}
/**
@ -229,7 +230,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
log.info("[내사종결] 조건 충족! 차량번호: {}, 말소구분: {}", vhclno, ersrRegistSeCode);
// DB 업데이트
existingData.setTaskPrcsSttsCd("04"); // 내사종결
existingData.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED); // 내사종결
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
// 필요한 추가 필드 설정
@ -239,7 +240,7 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co
}
log.info("[내사종결] 처리 완료! 차량번호: {}", vhclno);
return "04";
return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED;
}
}

@ -1,788 +0,0 @@
# 과태료 대상 비교 로직 구현 가이드
## 📌 목차
1. [개요](#개요)
2. [두 가지 구현 방법 비교](#두-가지-구현-방법-비교)
3. [방법1: Service/Impl 패턴 (추천)](#방법1-serviceimpl-패턴-추천)
4. [방법2: Chain of Responsibility 패턴](#방법2-chain-of-responsibility-패턴)
5. [어떤 방법을 선택해야 할까?](#어떤-방법을-선택해야-할까)
6. [참고사항](#참고사항)
---
## 개요
이 모듈은 과태료 대상 차량을 API 응답 데이터와 비교하여 자동으로 분류하는 기능을 제공합니다.
현재 프로젝트는 **두 가지 비교 로직 구현 방법**을 제공하며, 프로젝트 상황에 맞게 선택하여 사용할 수 있습니다.
### 비교 로직 처리 흐름
```
API 응답 데이터 수신
비교 로직 실행
조건1 (상품용) 체크 → 충족 → TASK_PRCS_STTS_CD = 02 → 종료
↓ 미충족
조건2 (이첩) 체크 → 충족 → TASK_PRCS_STTS_CD = 03 → 종료
↓ 미충족
조건3 (내사종결) 체크 → 충족 → TASK_PRCS_STTS_CD = 04 → 종료
↓ 미충족
모든 조건 미충족 → 정상 처리 (null 반환)
```
---
## 두 가지 구현 방법 비교
### 비교표
| 항목 | 방법1 (Service/Impl) ⭐ | 방법2 (Chain of Responsibility) |
|------|---------------------|--------------------------------|
| **구현 난이도** | ⭐ 쉬움 | ⭐⭐⭐ 어려움 |
| **학습 시간** | 10분 | 1시간+ |
| **조건 추가 시간** | 5분 | 15분 |
| **코드 가독성** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| **확장성** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **테스트 용이성** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **파일 개수** | 2개 | N+4개 |
| **디버깅** | ⭐⭐⭐⭐⭐ 쉬움 | ⭐⭐⭐ 보통 |
| **추천 조건 개수** | ~10개 | 10개 이상 |
---
## 방법1: Service/Impl 패턴 (추천)
### 🌟 특징
- ✅ **간단하고 직관적** - 학습 곡선 낮음
- ✅ **명확한 코드 흐름** - 순차적 실행으로 이해하기 쉬움
- ✅ **빠른 개발** - 메서드 하나만 추가하면 됨
- ✅ **디버깅 쉬움** - 코드 추적이 직관적
- ✅ **한 파일에서 관리** - 모든 비교 로직을 한눈에 확인
### 📁 디렉토리 구조
```
service/
├── ComparisonService.java # 인터페이스
└── impl/
└── ComparisonServiceImpl.java # 구현체 (모든 비교 로직 포함)
```
### 🚀 새로운 비교 조건 추가 방법
#### 예제 1: 일반 비교 조건 추가
**1단계: ComparisonServiceImpl에 private 메서드 추가**
```java
/**
* 4. 장기미검차량 체크
*
* <p>조건: 검사 유효기간 종료일로부터 1년 이상 경과</p>
* <p>처리: TASK_PRCS_STTS_CD = 05</p>
*
* @return 05 (적용됨) 또는 null (미적용)
*/
private String checkLongTermUninspected(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo) {
String vhclno = existingData.getVhclno();
String insptValidPdEndde = basicInfo.getInsptValidPdEndde(); // 검사유효기간종료일자
// 조건 체크 로직
if (insptValidPdEndde == null || insptValidPdEndde.isEmpty()) {
log.debug("[장기미검] 조건 미충족. 차량번호: {}", vhclno);
return null;
}
// 1년 경과 여부 확인 로직
LocalDate endDate = LocalDate.parse(insptValidPdEndde, DATE_FORMATTER);
LocalDate oneYearAgo = LocalDate.now().minusYears(1);
if (endDate.isAfter(oneYearAgo)) {
log.debug("[장기미검] 조건 미충족. 차량번호: {}, 종료일: {}", vhclno, insptValidPdEndde);
return null;
}
log.info("[장기미검] 조건 충족! 차량번호: {}, 종료일: {}", vhclno, insptValidPdEndde);
// DB 업데이트
existingData.setTaskPrcsSttsCd("05"); // 장기미검
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
int updateCount = carFfnlgTrgtMapper.update(existingData);
if (updateCount == 0) {
throw new RuntimeException(String.format("[장기미검] 업데이트 실패: %s", vhclno));
}
log.info("[장기미검] 처리 완료! 차량번호: {}", vhclno);
return "05";
}
```
**2단계: executeComparison() 메서드에 호출 추가**
```java
@Override
public String executeComparison(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId) {
String vhclno = existingData.getVhclno();
log.info("========== 비교 로직 시작: {} ==========", vhclno);
// API 응답 데이터 유효성 검사
if (!isValidApiResponse(apiResponse)) {
log.warn("API 응답 데이터가 유효하지 않습니다. 차량번호: {}", vhclno);
return null;
}
BasicResponse.Record basicInfo = apiResponse.getBasicInfo().getRecord().get(0);
// ========== 1. 상품용 체크 ==========
String productUseResult = checkProductUse(existingData, basicInfo);
if (productUseResult != null) {
log.info("========== 비교 로직 종료 (상품용): {} ==========", vhclno);
return productUseResult;
}
// ========== 2. 이첩 체크 ==========
String transferResult = checkTransfer(existingData, basicInfo, userId);
if (transferResult != null) {
log.info("========== 비교 로직 종료 (이첩): {} ==========", vhclno);
return transferResult;
}
// ========== 3. 장기미검 체크 (새로 추가!) ==========
String longTermResult = checkLongTermUninspected(existingData, basicInfo);
if (longTermResult != null) {
log.info("========== 비교 로직 종료 (장기미검): {} ==========", vhclno);
return longTermResult;
}
// 모든 비교 로직에 해당하지 않음
log.info("========== 비교 로직 종료 (정상): {} ==========", vhclno);
return null;
}
```
**끝! 매우 간단합니다.**
---
#### 예제 2: 이첩 조건 추가 (OR 구조)
이첩 조건은 **OR 로직**으로 구성되어 있습니다. **여러 조건 중 하나라도 만족하면** 이첩으로 처리됩니다.
**현재 이첩 구조:**
```java
private String checkTransfer(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo, String userId) {
String vhclno = existingData.getVhclno();
// ========== 이첩 조건들 (OR 로직: 하나라도 만족하면 이첩) ==========
// 조건1: 법정동코드 불일치
if (checkTransferCondition1_LegalDongMismatch(basicInfo, userId, vhclno)) {
return processTransfer(existingData, basicInfo, vhclno, "법정동코드 불일치");
}
// 조건2: 향후 추가될 이첩 조건들...
// if (checkTransferCondition2_XXX(basicInfo, userId, vhclno)) {
// return processTransfer(existingData, basicInfo, vhclno, "XXX");
// }
// 모든 이첩 조건에 해당하지 않음
log.debug("[이첩] 모든 조건 미충족. 차량번호: {}", vhclno);
return null;
}
```
**새로운 이첩 조건 추가:**
**1단계: 조건 체크 메서드 추가**
```java
/**
* 이첩 조건2: 차량 소유자 주소 불일치 (예시)
* 조건: 차량 등록지와 소유자 실거주지가 다른 경우
*/
private boolean checkTransferCondition2_AddressMismatch(BasicResponse.Record basicInfo, String userId, String vhclno) {
String useStrnghldAdresNm = basicInfo.getUseStrnghldAdresNm(); // 사용본거지주소명
String ownerAdresNm = basicInfo.getOwnerAdresNm(); // 소유자주소명
// 주소 유효성 검사
if (useStrnghldAdresNm == null || ownerAdresNm == null) {
log.debug("[이첩][조건2] 주소 정보 없음. 차량번호: {}", vhclno);
return false;
}
// 주소 일치 여부 확인 (시/도 단위 비교 등)
if (useStrnghldAdresNm.equals(ownerAdresNm)) {
log.debug("[이첩][조건2] 주소 일치. 차량번호: {}", vhclno);
return false;
}
log.info("[이첩][조건2] 주소 불일치! 차량번호: {}, 사용본거지: {}, 소유자주소: {}",
vhclno, useStrnghldAdresNm, ownerAdresNm);
return true;
}
```
**2단계: checkTransfer() 메서드에 조건 추가**
```java
private String checkTransfer(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo, String userId) {
String vhclno = existingData.getVhclno();
// ========== 이첩 조건들 (OR 로직: 하나라도 만족하면 이첩) ==========
// 조건1: 법정동코드 불일치
if (checkTransferCondition1_LegalDongMismatch(basicInfo, userId, vhclno)) {
return processTransfer(existingData, basicInfo, vhclno, "법정동코드 불일치");
}
// 조건2: 주소 불일치 (새로 추가!)
if (checkTransferCondition2_AddressMismatch(basicInfo, userId, vhclno)) {
return processTransfer(existingData, basicInfo, vhclno, "주소 불일치");
}
// 조건3: 향후 추가될 조건들...
// if (checkTransferCondition3_XXX(basicInfo, userId, vhclno)) {
// return processTransfer(existingData, basicInfo, vhclno, "XXX");
// }
return null;
}
```
**끝! 매우 간단합니다.**
---
### 📊 이첩 조건 OR 로직 동작 방식
```java
// 이첩 체크 시작
checkTransfer(...)
조건1 체크 → TRUE → 즉시 이첩 처리 후 return "03"
↓ FALSE
조건2 체크 → TRUE → 즉시 이첩 처리 후 return "03"
↓ FALSE
조건3 체크 → TRUE → 즉시 이첩 처리 후 return "03"
↓ FALSE
모든 조건 FALSE → return null (이첩 아님)
```
### 🔍 로그 예시
#### 조건1에 걸린 경우
```
[이첩][조건1] 법정동코드 불일치! 차량번호: 12가3456, 법정동: 1100, 조직: 4100
[이첩] 조건 충족! 차량번호: 12가3456, 사유: 법정동코드 불일치
[이첩] 처리 완료! 차량번호: 12가3456, 시군구: 서울특별시(11000), 사유: 법정동코드 불일치
```
#### 조건2에 걸린 경우
```
[이첩][조건1] 법정동코드 일치. 차량번호: 12가3456, 법정동: 1100, 조직: 1100
[이첩][조건2] 주소 불일치! 차량번호: 12가3456
[이첩] 조건 충족! 차량번호: 12가3456, 사유: 주소 불일치
[이첩] 처리 완료! 차량번호: 12가3456, 시군구: 서울특별시(11000), 사유: 주소 불일치
```
#### 모든 조건에 안 걸린 경우
```
[이첩][조건1] 법정동코드 일치. 차량번호: 12가3456
[이첩][조건2] 주소 일치. 차량번호: 12가3456
[이첩] 모든 조건 미충족. 차량번호: 12가3456
```
---
### 🚀 빠른 템플릿
#### 일반 비교 조건 템플릿
```java
/**
* N. 비교 조건명
*
* <p>조건: 조건 설명</p>
* <p>처리: TASK_PRCS_STTS_CD = XX</p>
*
* @return XX (적용됨) 또는 null (미적용)
*/
private String checkXXX(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo) {
String vhclno = existingData.getVhclno();
// 1. 데이터 추출
String data1 = basicInfo.getXXX();
String data2 = basicInfo.getYYY();
// 2. 유효성 검사
if (data1 == null || data2 == null) {
log.debug("[비교조건명] 조건 미충족. 차량번호: {}", vhclno);
return null;
}
// 3. 조건 체크
if (조건_만족) {
log.info("[비교조건명] 조건 충족! 차량번호: {}, 상세정보...", vhclno);
// 4. DB 업데이트
existingData.setTaskPrcsSttsCd("XX");
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
// 필요한 필드 추가 설정
int updateCount = carFfnlgTrgtMapper.update(existingData);
if (updateCount == 0) {
throw new RuntimeException(String.format("[비교조건명] 업데이트 실패: %s", vhclno));
}
log.info("[비교조건명] 처리 완료! 차량번호: {}", vhclno);
return "XX";
}
log.debug("[비교조건명] 조건 미충족. 차량번호: {}", vhclno);
return null;
}
```
#### 이첩 조건 템플릿
```java
/**
* 이첩 조건N: XXX
* 조건 설명
*/
private boolean checkTransferConditionN_XXX(BasicResponse.Record basicInfo, String userId, String vhclno) {
// 1. 데이터 추출
String data1 = basicInfo.getXXX();
String data2 = basicInfo.getYYY();
// 2. 유효성 검사
if (data1 == null || data2 == null) {
log.debug("[이첩][조건N] 데이터 없음. 차량번호: {}", vhclno);
return false;
}
// 3. 조건 체크
if (조건_만족) {
log.info("[이첩][조건N] 조건 충족! 차량번호: {}, 상세정보...", vhclno);
return true; // 이첩!
}
log.debug("[이첩][조건N] 조건 미충족. 차량번호: {}", vhclno);
return false;
}
// checkTransfer()에 추가
if (checkTransferConditionN_XXX(basicInfo, userId, vhclno)) {
return processTransfer(existingData, basicInfo, vhclno, "XXX");
}
```
---
### ⚠️ 주의사항
1. **조건 순서**: 위에서부터 순차적으로 체크됩니다
- 자주 걸리는 조건을 위쪽에 배치하면 성능 향상
2. **조건 메서드 네이밍**:
- 일반: `checkXXX`
- 이첩: `checkTransferConditionN_XXX` 형식 권장
3. **return 값**:
- 일반 조건: `"상태코드"` (적용됨) 또는 `null` (미적용)
- 이첩 조건: `true` (해당함) 또는 `false` (해당 안함)
4. **processTransfer() 공통 사용**:
- DB 업데이트는 `processTransfer()` 메서드에서 공통으로 처리
- 각 조건에서는 `true/false`만 반환
---
### 📝 체크리스트
새로운 비교 조건 추가 시 확인사항:
- [ ] 조건 메서드 작성 완료
- [ ] executeComparison()에 조건 추가 완료 (일반 조건)
- [ ] checkTransfer()에 조건 추가 완료 (이첩 조건)
- [ ] 로그 메시지에 조건 정보 포함
- [ ] null 체크 처리 완료
- [ ] 컴파일 테스트 완료
- [ ] 실제 데이터로 테스트 완료
---
## 방법2: Chain of Responsibility 패턴
### 🌟 특징
- ✅ **확장성 우수** - 규칙 추가/삭제가 독립적
- ✅ **테스트 용이** - 각 규칙을 독립적으로 테스트 가능
- ✅ **순서 제어** - getOrder()로 실행 순서 명확히 제어
- ✅ **관심사 분리** - 각 규칙이 독립된 파일로 관리
- ❌ **복잡한 구조** - 학습 곡선 높음
- ❌ **파일 분산** - 여러 파일을 확인해야 함
### 📁 디렉토리 구조
```
comparison/
├── ComparisonRule.java # 비교 규칙 인터페이스
├── ComparisonContext.java # 비교에 필요한 데이터 컨테이너
├── ComparisonResult.java # 비교 결과 객체
├── ComparisonRuleProcessor.java # 규칙 실행 체인 관리자
└── rules/ # 개별 규칙 구현체
├── ProductUseComparisonRule.java # 상품용 규칙
└── TransferComparisonRule.java # 이첩 규칙
```
### 🔄 동작 방식
1. **ComparisonRuleProcessor**가 모든 `@Component` 규칙을 자동으로 찾아서 등록
2. 규칙들을 `getOrder()` 순서대로 정렬 (낮을수록 먼저 실행)
3. 각 규칙을 순차적으로 실행
4. 규칙이 `applied=true`를 반환하면 즉시 중단
5. 모든 규칙이 `applied=false`를 반환하면 "정상" 처리
```
ComparisonRuleProcessor
규칙 자동 스캔 (@Component)
순서대로 정렬 (getOrder())
ProductUseComparisonRule (order=10) → 적용됨? → 종료
↓ 미적용
TransferComparisonRule (order=20) → 적용됨? → 종료
↓ 미적용
YourNewRule (order=30) → 적용됨? → 종료
↓ 미적용
정상 처리
```
---
### 🚀 새로운 비교 규칙 추가 방법
#### 1단계: rules/ 폴더에 새 클래스 생성
```java
package go.kr.project.carInspectionPenalty.registration.comparison.rules;
import go.kr.project.carInspectionPenalty.registration.comparison.*;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 내사종결 비교 규칙
*
* 적용 조건:
* - 예시: 차량이 말소된 경우
*
* 처리 내용:
* - TASK_PRCS_STTS_CD = 04 (내사종결)
* - TASK_PRCS_YMD = 현재 날짜
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class InvestigationClosedComparisonRule implements ComparisonRule {
private static final String STATUS_CODE = "04"; // 내사종결
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final CarFfnlgTrgtMapper mapper;
@Override
public ComparisonResult execute(ComparisonContext context) {
String vhclno = context.getVhclno();
// 1. API 응답 데이터 유효성 검사
if (context.getApiResponse().getBasicInfo() == null ||
context.getApiResponse().getBasicInfo().getRecord() == null ||
context.getApiResponse().getBasicInfo().getRecord().isEmpty()) {
log.debug("[{}] API 응답 데이터가 없어 규칙을 적용할 수 없습니다. 차량번호: {}",
getRuleName(), vhclno);
return ComparisonResult.notApplied();
}
// 2. 필요한 데이터 추출
go.kr.project.api.model.response.BasicResponse.Record basicInfo =
context.getApiResponse().getBasicInfo().getRecord().get(0);
String ersrRegistSeCode = basicInfo.getErsrRegistSeCode(); // 말소등록구분코드
// 3. 비교 로직 (말소된 차량인지 확인)
if (ersrRegistSeCode == null || ersrRegistSeCode.isEmpty()) {
log.debug("[{}] 말소되지 않은 차량입니다. 차량번호: {}", getRuleName(), vhclno);
return ComparisonResult.notApplied();
}
log.info("[{}] 내사종결 감지! 차량번호: {}, 말소구분: {}",
getRuleName(), vhclno, ersrRegistSeCode);
// 4. DB 업데이트
CarFfnlgTrgtVO updateData = context.getExistingData();
updateData.setTaskPrcsSttsCd(STATUS_CODE);
updateData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
// 필요시 추가 필드 설정
int updateResult = mapper.update(updateData);
// 5. 결과 반환
if (updateResult > 0) {
log.info("[{}] 처리 완료! 차량번호: {}", getRuleName(), vhclno);
return ComparisonResult.applied(STATUS_CODE, "내사종결로 처리되었습니다.");
} else {
log.error("[{}] 업데이트 실패! 차량번호: {}", getRuleName(), vhclno);
throw new RuntimeException(String.format("내사종결 업데이트 실패: %s", vhclno));
}
}
@Override
public String getRuleName() {
return "내사종결";
}
@Override
public int getOrder() {
return 30; // 상품용(10), 이첩(20) 다음으로 실행
}
}
```
#### 2단계: 자동 등록
Spring이 자동으로 `@Component` 어노테이션이 붙은 클래스를 찾아서 등록합니다.
**별도의 설정 파일 수정이 필요 없습니다!**
#### 끝! 자동으로 등록됩니다.
---
### 📊 실행 순서 제어
`getOrder()` 메서드로 실행 순서를 제어할 수 있습니다.
```java
@Override
public int getOrder() {
return 30; // 숫자가 낮을수록 먼저 실행
}
```
**현재 순서:**
- 상품용: 10
- 이첩: 20
- (향후 추가): 30, 40, 50...
---
### 📦 API 응답 데이터 활용
`ComparisonContext`를 통해 다음 데이터에 접근할 수 있습니다:
```java
// 기존 과태료 대상 데이터
CarFfnlgTrgtVO existingData = context.getExistingData();
// API 응답 - 기본 정보
var basicInfo = context.getApiResponse().getBasicInfo().getRecord().get(0);
String mberNm = basicInfo.getMberNm(); // 대표소유자성명
String vhrno = basicInfo.getVhrno(); // 차량번호
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode(); // 사용본거지법정동코드
String ersrRegistSeCode = basicInfo.getErsrRegistSeCode(); // 말소등록구분코드
// ... 기타 필드들
// API 응답 - 등록원부
var ledgerInfo = context.getApiResponse().getLedgerInfo();
// 사용자 ID
String userId = context.getUserId();
```
---
### 💉 의존성 주입
규칙 클래스에서 필요한 빈을 자유롭게 주입받을 수 있습니다.
```java
@Component
@RequiredArgsConstructor
public class MyComparisonRule implements ComparisonRule {
private final CarFfnlgTrgtMapper mapper; // Mapper
private final UserMapper userMapper; // User 정보 필요 시
private final SomeOtherService someService; // 다른 서비스
// ...
}
```
---
### 📝 로깅
규칙 실행 중 상세한 로그가 자동으로 출력됩니다.
```
[상품용] 규칙 실행 중... 차량번호: 12가3456
[상품용] 상품용 감지! 차량번호: 12가3456, 소유자명: 상품용차량
[상품용] 처리 완료! 차량번호: 12가3456
```
---
### 🧪 테스트 방법
규칙을 독립적으로 테스트할 수 있습니다.
```java
@SpringBootTest
class InvestigationClosedComparisonRuleTest {
@Autowired
private InvestigationClosedComparisonRule rule;
@Test
void 말소된_차량은_내사종결로_처리된다() {
// Given
ComparisonContext context = ComparisonContext.builder()
.existingData(existingData)
.apiResponse(apiResponse)
.userId("USER001")
.build();
// When
ComparisonResult result = rule.execute(context);
// Then
assertTrue(result.isApplied());
assertEquals("04", result.getStatusCode());
}
}
```
---
### 📚 기존 규칙 예제
#### 상품용 규칙 (ProductUseComparisonRule)
```java
// 대표소유자성명에 "상품용" 문자열이 포함되어 있는지 확인
String mberNm = basicInfo.getMberNm();
if (mberNm != null && mberNm.contains("상품용")) {
// 상품용으로 처리
return ComparisonResult.applied("02", "상품용으로 처리되었습니다.");
}
```
#### 이첩 규칙 (TransferComparisonRule)
```java
// 법정동코드 앞 4자리와 사용자 조직코드 앞 4자리 비교
String legalDong4 = useStrnghldLegaldongCode.substring(0, 4);
String userOrg4 = userOrgCd.substring(0, 4);
if (!legalDong4.equals(userOrg4)) {
// 이첩으로 처리
return ComparisonResult.applied("03", "이첩으로 처리되었습니다.");
}
```
---
## 어떤 방법을 선택해야 할까?
### 방법1 선택 (Service/Impl) - 다음과 같은 경우 추천 ⭐
- ✅ **간단한 프로젝트** - 비교 조건이 10개 이하
- ✅ **빠른 개발 필요** - 당장 구현해야 할 때
- ✅ **팀원 경험 부족** - 디자인 패턴에 익숙하지 않은 경우
- ✅ **유지보수 단순** - 한 파일에서 모든 로직 확인 가능
- ✅ **작은 팀** - 1~3명 정도의 소규모 팀
### 방법2 선택 (Chain of Responsibility) - 다음과 같은 경우 추천
- ✅ **복잡한 프로젝트** - 비교 조건이 10개 이상
- ✅ **장기 유지보수** - 규칙이 자주 추가/변경될 것으로 예상
- ✅ **팀 규모 큽** - 여러 개발자가 동시에 작업
- ✅ **테스트 중요** - 각 규칙을 독립적으로 테스트해야 함
- ✅ **확장성 중시** - 규칙을 동적으로 추가/제거 필요
---
## 참고사항
### 💡 현재 설정 변경 방법
`CarFfnlgTrgtServiceImpl.java``executeComparisonLogic()` 메서드에서 주석을 변경하면 됩니다.
```java
private String executeComparisonLogic(...) {
// 방법1 사용하려면: (현재 설정)
return executeWithServicePattern(existingData, apiResponse, userId);
// 방법2 사용하려면:
// return executeWithChainPattern(existingData, apiResponse, userId);
}
```
---
### ⚠️ 주의사항
1. **두 방법을 동시에 사용하지 마세요** - 하나만 선택
2. **트랜잭션 관리** - 각 비교 메서드는 DB 업데이트를 직접 수행
3. **예외 처리** - 업데이트 실패 시 RuntimeException 발생 → 전체 롤백
4. **로깅** - 각 조건의 충족/미충족 여부를 명확히 로깅
5. **순서 관리** (방법2) - `getOrder()` 값이 중복되지 않도록 주의
6. **null 체크** - API 응답 데이터는 항상 null 체크 필수
---
### 🚀 빠른 시작
#### 방법1로 시작하기 (추천)
1. `ComparisonServiceImpl.java` 열기
2. 기존 메서드 참고하여 새 메서드 추가
3. `executeComparison()`에서 호출
4. 끝!
#### 방법2로 시작하기
1. `comparison/rules/` 폴더에 새 규칙 클래스 생성
2. `@Component` 붙이기
3. `ComparisonRule` 인터페이스 구현
4. 끝!
---
### 📞 문의
추가 질문이나 제안사항은 개발팀에 문의하세요.
---
### 📖 관련 파일
- **방법1 파일**:
- `service/ComparisonService.java`
- `service/impl/ComparisonServiceImpl.java`
- **방법2 파일**:
- `comparison/` 패키지 전체
- **통합 지점**:
- `CarFfnlgTrgtServiceImpl.java` - `executeComparisonLogic()` 메서드

@ -184,4 +184,24 @@
WHERE CAR_LEDGER_FRMBK_DTL_ID = #{carLedgerFrmbkDtlId}
</select>
<!-- ==================== 최신 이력 ID 조회 ==================== -->
<!-- 차량번호로 최신 자동차 기본정보 조회 이력 ID 조회 -->
<select id="selectLatestCarBassMatterInqireIdByVhclno" parameterType="String" resultType="String">
SELECT CAR_BASS_MATTER_INQIRE_ID
FROM tb_car_bass_matter_inqire
WHERE DMND_VHRNO = #{vhclno} OR VHRNO = #{vhclno}
ORDER BY REG_DT DESC
LIMIT 1
</select>
<!-- 차량번호로 최신 자동차 등록원부(갑) 조회 이력 ID 조회 -->
<select id="selectLatestCarLedgerFrmbkIdByVhclno" parameterType="String" resultType="String">
SELECT CAR_LEDGER_FRMBK_ID
FROM tb_car_ledger_frmbk
WHERE DMND_VHRNO = #{vhclno} OR VHRNO = #{vhclno}
ORDER BY REG_DT DESC
LIMIT 1
</select>
</mapper>

@ -58,6 +58,7 @@
<li class="tit">과태료 대상 목록</li>
<li class="rig">
<button type="button" id="callApiBtn" class="newbtn bg2-1">그리드 선택 API 호출</button>
<button type="button" id="callApiAllBtn" class="newbtn bg2-1">검색조건 전체 API 호출</button>
<span id="totalCount" class="total-count" style="padding-left: 25px;padding-right: 25px;">총 0건</span>
<select id="perPageSelect" class="input" style="width: 112px; ">
@ -388,11 +389,16 @@
self.deleteData();
});
// API 호출 버튼 클릭
// API 호출 버튼 클릭 (그리드 선택)
$("#callApiBtn").on('click', function() {
self.callApiAndCompare();
});
// API 호출 버튼 클릭 (검색조건 전체)
$("#callApiAllBtn").on('click', function() {
self.callApiAndCompareAll();
});
// 목록 다운로드 버튼 클릭
$("#downloadBtn").on('click', function() {
var url = buildDownloadUrl();
@ -446,28 +452,33 @@
return;
}
var deletePromises = [];
checkedRows.forEach(function(row) {
var promise = $.ajax({
url: '<c:url value="/carInspectionPenalty/registration/delete.ajax"/>',
type: 'POST',
data: {
carFfnlgTrgtId: row.carFfnlgTrgtId
}
});
deletePromises.push(promise);
// 삭제할 ID 목록 준비
var deleteIds = checkedRows.map(function(row) {
return row.carFfnlgTrgtId;
});
$.when.apply($, deletePromises).done(function() {
// AJAX 호출
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration/deleteBatch.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(deleteIds),
success: function(response) {
if (response.success) {
alert("삭제가 완료되었습니다.");
CarFfnlgTrgtList.grid.reload();
}).fail(function() {
} else {
alert("삭제 중 오류가 발생했습니다: " + response.message);
}
},
error: function(xhr, status, error) {
console.error("삭제 실패:", error);
}
});
},
/**
* API 호출 및 데이터 비교
* API 호출 및 데이터 비교 (그리드 선택)
*/
callApiAndCompare: function() {
var checkedRows = this.grid.instance.getCheckedRows();
@ -521,6 +532,53 @@
console.error("API 호출 실패:", error);
}
});
},
/**
* API 호출 및 데이터 비교 (검색조건 전체, 페이징 없이)
*/
callApiAndCompareAll: function() {
if (!confirm("현재 검색조건의 전체 데이터에 대해 API를 호출하고 비교하시겠습니까?")) {
return;
}
// 검색 조건 설정
setSearchCond();
// 페이징 없이 전체 조회를 위한 파라미터
var params = Object.assign({}, SEARCH_COND, {
pagingYn: 'N' // 페이징 비활성화
});
// 로딩 메시지 표시
console.log("전체 데이터 API 호출 및 비교 중입니다...\n잠시만 기다려주세요.");
// AJAX 호출
$.ajax({
url: '<c:url value="/carInspectionPenalty/registration/compareWithApiAll.ajax"/>',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(params),
success: function(response) {
if (response.success) {
alert("API 호출 및 비교가 완료되었습니다.\n\n" + response.message);
// 비교 결과 상세 보기 (콘솔 출력)
if (response.data && response.data.compareResults) {
console.log("비교 결과 상세:", response.data.compareResults);
}
// 그리드 새로고침
CarFfnlgTrgtList.grid.reload();
} else {
alert("오류: " + response.message);
}
},
error: function(xhr, status, error) {
console.error("API 호출 실패:", error);
alert("API 호출 중 오류가 발생했습니다.");
}
});
}
};

Loading…
Cancel
Save