상품용, 이첩 sample 진행, 모듈화 2가지 템플릿 소스 적용

추후 테스트 후 선택
internalApi
박성영 1 month ago
parent 94cbef2857
commit 1f442a9472

@ -0,0 +1,36 @@
package go.kr.project.carInspectionPenalty.registration.comparison;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import lombok.Builder;
import lombok.Getter;
/**
*
*/
@Getter
@Builder
public class ComparisonContext {
/**
*
*/
private final CarFfnlgTrgtVO existingData;
/**
* API
*/
private final VehicleApiResponseVO apiResponse;
/**
* ( ID)
*/
private final String userId;
/**
* ( )
*/
public String getVhclno() {
return existingData != null ? existingData.getVhclno() : null;
}
}

@ -0,0 +1,50 @@
package go.kr.project.carInspectionPenalty.registration.comparison;
import lombok.Builder;
import lombok.Getter;
/**
*
*/
@Getter
@Builder
public class ComparisonResult {
/**
*
* true: ( )
* false: ( )
*/
private final boolean applied;
/**
*
* 02=, 03=, 04=
*/
private final String statusCode;
/**
*
*/
private final String message;
/**
*
*/
public static ComparisonResult notApplied() {
return ComparisonResult.builder()
.applied(false)
.build();
}
/**
*
*/
public static ComparisonResult applied(String statusCode, String message) {
return ComparisonResult.builder()
.applied(true)
.statusCode(statusCode)
.message(message)
.build();
}
}

@ -0,0 +1,42 @@
package go.kr.project.carInspectionPenalty.registration.comparison;
/**
*
*
* <p>Chain of Responsibility .</p>
*
* <p> :</p>
* <ol>
* <li> </li>
* <li> DB ComparisonResult.applied() </li>
* <li> ComparisonResult.notApplied() </li>
* </ol>
*
* <p> applied=true , false .</p>
*/
public interface ComparisonRule {
/**
* .
*
* @param context
* @return (applied=true , false )
*/
ComparisonResult execute(ComparisonContext context);
/**
* . ( )
*
* @return
*/
String getRuleName();
/**
* . ( )
*
* @return (: 100)
*/
default int getOrder() {
return 100;
}
}

@ -0,0 +1,90 @@
package go.kr.project.carInspectionPenalty.registration.comparison;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
*
*
* <p> .</p>
*
* <p> :</p>
* <ol>
* <li> getOrder() </li>
* <li> </li>
* <li> applied=true </li>
* <li> applied=false null </li>
* </ol>
*/
@Slf4j
@Component
public class ComparisonRuleProcessor {
private final List<ComparisonRule> rules;
/**
* ComparisonRule .
* Spring ComparisonRule .
*/
public ComparisonRuleProcessor(List<ComparisonRule> rules) {
// getOrder() 순서대로 정렬
this.rules = rules.stream()
.sorted(Comparator.comparingInt(ComparisonRule::getOrder))
.collect(Collectors.toList());
log.info("========== 비교 규칙 프로세서 초기화 ==========");
log.info("등록된 규칙 개수: {}", this.rules.size());
this.rules.forEach(rule ->
log.info(" - [순서: {}] {}", rule.getOrder(), rule.getRuleName())
);
log.info("============================================");
}
/**
* .
*
* @param context
* @return ( null)
*/
public ComparisonResult process(ComparisonContext context) {
String vhclno = context.getVhclno();
log.debug("========== 비교 규칙 체인 시작: {} ==========", vhclno);
for (ComparisonRule rule : rules) {
log.debug("[{}] 규칙 실행 중... 차량번호: {}", rule.getRuleName(), vhclno);
try {
ComparisonResult result = rule.execute(context);
if (result.isApplied()) {
log.info("[{}] 규칙 적용됨! 차량번호: {}, 상태코드: {}, 메시지: {}",
rule.getRuleName(), vhclno, result.getStatusCode(), result.getMessage());
log.debug("========== 비교 규칙 체인 종료 (규칙 적용): {} ==========", vhclno);
return result;
}
log.debug("[{}] 규칙 적용되지 않음. 다음 규칙으로 이동...", rule.getRuleName());
} catch (Exception e) {
log.error("[{}] 규칙 실행 중 오류 발생! 차량번호: {}", rule.getRuleName(), vhclno, e);
throw new RuntimeException(
String.format("[%s] 규칙 실행 중 오류 발생: %s - %s",
rule.getRuleName(), vhclno, e.getMessage()), e);
}
}
log.debug("========== 비교 규칙 체인 종료 (적용된 규칙 없음): {} ==========", vhclno);
return null; // 적용된 규칙이 없음
}
/**
* . ()
*/
public List<ComparisonRule> getRules() {
return rules;
}
}

@ -0,0 +1,261 @@
# 과태료 대상 비교 규칙 모듈
## 개요
이 모듈은 과태료 대상 차량을 API 응답 데이터와 비교하여 자동으로 분류하는 기능을 제공합니다.
**Chain of Responsibility 패턴**을 사용하여 각 비교 규칙을 독립적으로 관리하고 확장할 수 있습니다.
## 아키텍처
```
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`를 반환하면 "정상" 처리
## 새로운 비교 규칙 추가 방법
### 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. 필요한 데이터 추출
var 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` 어노테이션이 붙은 클래스를 찾아서 등록합니다.
**별도의 설정 파일 수정이 필요 없습니다!**
### 3. 실행 순서 제어
`getOrder()` 메서드로 실행 순서를 제어할 수 있습니다.
```java
@Override
public int getOrder() {
return 30; // 숫자가 낮을수록 먼저 실행
}
```
**현재 순서:**
- 상품용: 10
- 이첩: 20
- (향후 추가): 30, 40, 50...
## 기존 규칙 예제
### 상품용 규칙 (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", "이첩으로 처리되었습니다.");
}
```
## 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());
}
}
```
## 주의사항
1. **순서 관리**: `getOrder()` 값이 중복되지 않도록 주의
2. **트랜잭션**: 각 규칙 내에서 DB 업데이트 수행 (전체는 Service에서 @Transactional)
3. **예외 처리**: 업데이트 실패 시 RuntimeException 발생 → 전체 롤백
4. **null 체크**: API 응답 데이터는 항상 null 체크 필수
5. **로깅**: DEBUG 레벨로 상세한 로그 남기기
## 문의
추가 비교 규칙이 필요하거나 질문이 있으면 개발팀에 문의하세요.

@ -0,0 +1,97 @@
package go.kr.project.carInspectionPenalty.registration.comparison.rules;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonContext;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonResult;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonRule;
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;
/**
*
*
* <p>(MBER_NM) "상품용" .</p>
*
* <p> :</p>
* <ul>
* <li> "상품용" </li>
* </ul>
*
* <p> :</p>
* <ul>
* <li>TASK_PRCS_STTS_CD = 02 ()</li>
* <li>TASK_PRCS_YMD = </li>
* <li>CAR_BSC_MTTR_INQ_FLNM = </li>
* </ul>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductUseComparisonRule implements ComparisonRule {
private static final String STATUS_CODE = "02"; // 상품용
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final CarFfnlgTrgtMapper mapper;
@Override
public ComparisonResult execute(ComparisonContext context) {
String vhclno = context.getVhclno();
// API 응답 데이터 유효성 검사
if (context.getApiResponse().getBasicInfo() == null ||
context.getApiResponse().getBasicInfo().getRecord() == null ||
context.getApiResponse().getBasicInfo().getRecord().isEmpty()) {
log.debug("[{}] API 응답 데이터가 없어 규칙을 적용할 수 없습니다. 차량번호: {}", getRuleName(), vhclno);
return ComparisonResult.notApplied();
}
// 대표소유자성명 추출
String mberNm = context.getApiResponse()
.getBasicInfo()
.getRecord()
.get(0)
.getMberNm();
// 상품용 체크
if (mberNm == null || !mberNm.contains("상품용")) {
log.debug("[{}] 상품용에 해당하지 않습니다. 차량번호: {}, 소유자명: {}", getRuleName(), vhclno, mberNm);
return ComparisonResult.notApplied();
}
log.info("[{}] 상품용 감지! 차량번호: {}, 소유자명: {}", getRuleName(), vhclno, mberNm);
// 업무 처리 상태 업데이트
CarFfnlgTrgtVO updateData = context.getExistingData();
updateData.setTaskPrcsSttsCd(STATUS_CODE);
updateData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
updateData.setCarBscMttrInqFlnm(mberNm); // 소유자명 저장
updateData.setCarBscMttrInqSggCd(null); // 이첩 필드는 null
updateData.setCarBscMttrInqSggNm(null);
int updateResult = mapper.update(updateData);
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 10; // 상품용은 가장 먼저 체크 (낮은 순서)
}
}

@ -0,0 +1,169 @@
package go.kr.project.carInspectionPenalty.registration.comparison.rules;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonContext;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonResult;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonRule;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.system.user.mapper.UserMapper;
import go.kr.project.system.user.model.SystemUserVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
*
*
* <p> 4 4 .</p>
*
* <p> :</p>
* <ul>
* <li> (USE_STRNGHLD_LEGALDONG_CODE) 4</li>
* <li>(tb_user) ORG_CD 4</li>
* <li> </li>
* </ul>
*
* <p> :</p>
* <ul>
* <li>TASK_PRCS_STTS_CD = 03 ()</li>
* <li>TASK_PRCS_YMD = </li>
* <li>CAR_BSC_MTTR_INQ_SGG_CD = 5</li>
* <li>CAR_BSC_MTTR_INQ_SGG_NM = tb_sgg_cd </li>
* </ul>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class TransferComparisonRule implements ComparisonRule {
private static final String STATUS_CODE = "03"; // 이첩
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final CarFfnlgTrgtMapper mapper;
private final UserMapper userMapper;
@Override
public ComparisonResult execute(ComparisonContext context) {
String vhclno = context.getVhclno();
// API 응답 데이터 유효성 검사
if (context.getApiResponse().getBasicInfo() == null ||
context.getApiResponse().getBasicInfo().getRecord() == null ||
context.getApiResponse().getBasicInfo().getRecord().isEmpty()) {
log.debug("[{}] API 응답 데이터가 없어 규칙을 적용할 수 없습니다. 차량번호: {}", getRuleName(), vhclno);
return ComparisonResult.notApplied();
}
go.kr.project.api.model.response.BasicResponse.Record basicInfo = context.getApiResponse().getBasicInfo().getRecord().get(0);
// ========== 이첩 조건들 (OR 로직: 하나라도 만족하면 이첩) ==========
// 조건1: 법정동코드 불일치
if (checkLegalDongCodeMismatch(context, basicInfo, vhclno)) {
return processTransfer(context, basicInfo, vhclno, "법정동코드 불일치");
}
// 조건2: 향후 추가될 이첩 조건들 (예시)
// if (checkOtherTransferCondition(context, basicInfo, vhclno)) {
// return processTransfer(context, basicInfo, vhclno, "다른 조건");
// }
// 모든 이첩 조건에 해당하지 않음
log.debug("[{}] 모든 이첩 조건에 해당하지 않습니다. 차량번호: {}", getRuleName(), vhclno);
return ComparisonResult.notApplied();
}
/**
* 1:
* 4 != 4
*/
private boolean checkLegalDongCodeMismatch(ComparisonContext context,
go.kr.project.api.model.response.BasicResponse.Record basicInfo,
String vhclno) {
// 사용본거지법정동코드 추출
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode();
if (useStrnghldLegaldongCode == null || useStrnghldLegaldongCode.length() < 4) {
log.debug("[{}][조건1] 법정동코드가 없거나 길이 부족. 차량번호: {}", getRuleName(), vhclno);
return false;
}
// 사용자 정보 조회
SystemUserVO userInfo = userMapper.selectUser(context.getUserId());
if (userInfo == null || userInfo.getOrgCd() == null) {
log.debug("[{}][조건1] 사용자 정보 없음. 차량번호: {}", getRuleName(), vhclno);
return false;
}
// 법정동코드 앞 4자리와 사용자 조직코드 앞 4자리 비교
String legalDong4 = useStrnghldLegaldongCode.substring(0, 4);
String userOrgCd = userInfo.getOrgCd();
String userOrg4 = userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : userOrgCd;
if (legalDong4.equals(userOrg4)) {
log.debug("[{}][조건1] 법정동코드 일치. 차량번호: {}, 법정동: {}, 조직: {}",
getRuleName(), vhclno, legalDong4, userOrg4);
return false;
}
log.info("[{}][조건1] 법정동코드 불일치! 차량번호: {}, 법정동: {}, 조직: {}",
getRuleName(), vhclno, legalDong4, userOrg4);
return true;
}
/**
* ( )
*/
private ComparisonResult processTransfer(ComparisonContext context,
go.kr.project.api.model.response.BasicResponse.Record basicInfo,
String vhclno,
String reason) {
log.info("[{}] 이첩 감지! 차량번호: {}, 사유: {}", getRuleName(), vhclno, reason);
// 사용본거지법정동코드 추출
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode();
// 시군구 코드 (법정동코드 앞 5자리)
String sggCd = (useStrnghldLegaldongCode != null && useStrnghldLegaldongCode.length() >= 5)
? useStrnghldLegaldongCode.substring(0, 5)
: (useStrnghldLegaldongCode != null ? useStrnghldLegaldongCode : "");
// 시군구명 조회
String sggNm = mapper.selectSggNmBySggCd(sggCd);
if (sggNm == null || sggNm.isEmpty()) {
log.warn("[{}] 시군구명 조회 실패. 시군구코드: {} (빈 문자열로 처리)", getRuleName(), sggCd);
sggNm = ""; // 시군구명이 없어도 이첩 처리는 진행
}
// 업무 처리 상태 업데이트
CarFfnlgTrgtVO updateData = context.getExistingData();
updateData.setTaskPrcsSttsCd(STATUS_CODE);
updateData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
updateData.setCarBscMttrInqFlnm(null); // 상품용 필드는 null
updateData.setCarBscMttrInqSggCd(sggCd); // 시군구 코드 저장
updateData.setCarBscMttrInqSggNm(sggNm); // 시군구명 저장
int updateResult = mapper.update(updateData);
if (updateResult > 0) {
log.info("[{}] 처리 완료! 차량번호: {}, 시군구: {}({})", getRuleName(), vhclno, sggNm, sggCd);
return ComparisonResult.applied(STATUS_CODE, String.format("이첩으로 처리되었습니다. (시군구: %s)", sggNm));
} else {
log.error("[{}] 업데이트 실패! 차량번호: {}", getRuleName(), vhclno);
throw new RuntimeException(String.format("이첩 업데이트 실패: %s", vhclno));
}
}
@Override
public String getRuleName() {
return "이첩";
}
@Override
public int getOrder() {
return 20; // 상품용 다음으로 체크
}
}

@ -0,0 +1,234 @@
# 비교 로직 구현 방법 가이드
## 📌 두 가지 구현 방법 제공
현재 프로젝트는 두 가지 비교 로직 구현 방법을 제공합니다.
프로젝트 상황에 맞게 선택하여 사용하세요.
---
## 방법1: 일반 Service/Impl 패턴 ⭐ **추천**
### 특징
- ✅ **간단하고 직관적** - 학습 곡선 낮음
- ✅ **명확한 코드 흐름** - 순차적 실행으로 이해하기 쉬움
- ✅ **빠른 개발** - 메서드 하나만 추가하면 됨
- ✅ **디버깅 쉬움** - 코드 추적이 직관적
### 구조
```
service/
├── ComparisonService.java # 인터페이스
└── impl/
└── ComparisonServiceImpl.java # 구현체
```
### 새로운 비교 조건 추가 방법
#### 1단계: ComparisonServiceImpl에 private 메서드 추가
```java
/**
* 4. 새로운 비교 조건 (예: 장기미검차량)
*
* <p>조건: 검사 유효기간 종료일로부터 1년 이상 경과</p>
* <p>처리: TASK_PRCS_STTS_CD = 05</p>
*/
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) {
// ... 기존 코드 ...
// ========== 3. 향후 추가될 비교 로직들 ==========
String longTermResult = checkLongTermUninspected(existingData, basicInfo);
if (longTermResult != null) {
log.info("========== 비교 로직 종료 (장기미검): {} ==========", vhclno);
return longTermResult;
}
// ... 나머지 코드 ...
}
```
#### 끝! 매우 간단합니다.
### 파일 위치
- `service/ComparisonService.java`
- `service/impl/ComparisonServiceImpl.java`
### 실행 순서
1. 상품용 체크 → 2. 이첩 체크 → 3. 새로운 조건들...
- **순서대로 실행**되며, 하나라도 조건이 맞으면 즉시 종료
---
## 방법2: Chain of Responsibility 패턴
### 특징
- ✅ **확장성 우수** - 규칙 추가/삭제가 독립적
- ✅ **테스트 용이** - 각 규칙을 독립적으로 테스트 가능
- ✅ **순서 제어** - getOrder()로 실행 순서 명확히 제어
- ❌ **복잡한 구조** - 학습 곡선 높음
- ❌ **파일 분산** - 여러 파일을 확인해야 함
### 구조
```
comparison/
├── ComparisonRule.java # 규칙 인터페이스
├── ComparisonContext.java # 데이터 컨테이너
├── ComparisonResult.java # 결과 객체
├── ComparisonRuleProcessor.java # 체인 관리자
└── rules/ # 규칙 구현체들
├── ProductUseComparisonRule.java
├── TransferComparisonRule.java
└── ... (새 규칙 파일들)
```
### 새로운 비교 조건 추가 방법
#### 1단계: rules/ 폴더에 새 클래스 생성
```java
@Component
@RequiredArgsConstructor
public class LongTermUninspectedComparisonRule implements ComparisonRule {
@Override
public ComparisonResult execute(ComparisonContext context) {
// 비교 로직 작성
if (/* 조건 충족 */) {
// DB 업데이트
return ComparisonResult.applied("05", "장기미검으로 처리");
}
return ComparisonResult.notApplied();
}
@Override
public String getRuleName() { return "장기미검"; }
@Override
public int getOrder() { return 40; } // 실행 순서
}
```
#### 끝! 자동으로 등록됩니다.
### 파일 위치
- `comparison/` 패키지 전체
---
## 🎯 어떤 방법을 선택해야 할까?
### 방법1 선택 (Service/Impl) - 다음과 같은 경우 추천
- ✅ **간단한 프로젝트** - 비교 조건이 10개 이하
- ✅ **빠른 개발 필요** - 당장 구현해야 할 때
- ✅ **팀원 경험 부족** - 디자인 패턴에 익숙하지 않은 경우
- ✅ **유지보수 단순** - 한 파일에서 모든 로직 확인 가능
### 방법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 (Service/Impl) | 방법2 (Chain of Responsibility) |
|------|---------------------|--------------------------------|
| **구현 난이도** | ⭐ 쉬움 | ⭐⭐⭐ 어려움 |
| **학습 시간** | 10분 | 1시간+ |
| **추가 시간** | 5분 | 15분 |
| **코드 가독성** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| **확장성** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **테스트 용이성** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **파일 개수** | 2개 | N+4개 |
| **디버깅** | ⭐⭐⭐⭐⭐ 쉬움 | ⭐⭐⭐ 보통 |
---
## 🚀 빠른 시작
### 방법1로 시작하기 (추천)
1. `ComparisonServiceImpl.java` 열기
2. 기존 메서드 참고하여 새 메서드 추가
3. `executeComparison()`에서 호출
4. 끝!
### 방법2로 시작하기
1. `comparison/README.md` 읽기
2. `rules/` 폴더에 새 규칙 클래스 생성
3. `@Component` 붙이기
4. 끝!
---
## ⚠️ 주의사항
1. **두 방법을 동시에 사용하지 마세요** - 하나만 선택
2. **트랜잭션 관리** - 각 비교 메서드는 DB 업데이트를 직접 수행
3. **예외 처리** - 업데이트 실패 시 RuntimeException 발생 → 전체 롤백
4. **로깅** - 각 조건의 충족/미충족 여부를 명확히 로깅
---
## 📞 문의
추가 질문이나 제안사항은 개발팀에 문의하세요.

@ -0,0 +1,32 @@
package go.kr.project.carInspectionPenalty.registration.service;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
/**
*
*
* <p> API .</p>
*/
public interface ComparisonService {
/**
* .
*
* <p> :</p>
* <ol>
* <li> </li>
* <li> </li>
* <li> ( )</li>
* <li> ...</li>
* </ol>
*
* <p> .</p>
*
* @param existingData
* @param apiResponse API
* @param userId ID
* @return (02=, 03=, 04=, null=)
*/
String executeComparison(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId);
}

@ -4,12 +4,14 @@ import egovframework.exception.MessageException;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.service.VehicleInfoService;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonContext;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonResult;
import go.kr.project.carInspectionPenalty.registration.comparison.ComparisonRuleProcessor;
import go.kr.project.carInspectionPenalty.registration.config.CarFfnlgTxtParseConfig;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
import go.kr.project.system.user.mapper.UserMapper;
import go.kr.project.system.user.model.SystemUserVO;
import go.kr.project.carInspectionPenalty.registration.service.ComparisonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -39,7 +41,13 @@ public class CarFfnlgTrgtServiceImpl implements CarFfnlgTrgtService {
private final CarFfnlgTrgtMapper mapper;
private final CarFfnlgTxtParseConfig parseConfig;
private final VehicleInfoService vehicleInfoService;
private final UserMapper userMapper;
// ========== 비교 로직 선택 (둘 중 하나 선택) ==========
// 방법1: Chain of Responsibility 패턴 (복잡하지만 확장성 좋음)
private final ComparisonRuleProcessor comparisonRuleProcessor;
// 방법2: 일반 Service/Impl 패턴 (간단하고 명확함)
private final ComparisonService comparisonService;
// 날짜 형식 (YYYYMMDD)
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
@ -963,22 +971,30 @@ public class CarFfnlgTrgtServiceImpl implements CarFfnlgTrgtService {
continue;
}
// 3. 비교 로직 실행 (상품용 → 이첩 순서로 체크)
String processedStatus = processComparison(existingData, apiResponse, rgtr);
// 3. 비교 로직 실행
String statusCode = executeComparisonLogic(existingData, apiResponse, rgtr);
if ("02".equals(processedStatus)) {
// 상품용으로 처리됨
productUseCount++;
compareResult.put("success", true);
compareResult.put("message", "상품용으로 처리되었습니다.");
compareResult.put("processStatus", "상품용");
successCount++;
} else if ("03".equals(processedStatus)) {
// 이첩으로 처리됨
transferCount++;
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if ("02".equals(statusCode)) {
productUseCount++;
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if ("03".equals(statusCode)) {
transferCount++;
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if ("04".equals(statusCode)) {
normalCount++;
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
} else {
normalCount++;
compareResult.put("processStatus", "기타");
compareResult.put("message", "기타 상태로 처리되었습니다.");
}
compareResult.put("success", true);
compareResult.put("message", "이첩으로 처리되었습니다.");
compareResult.put("processStatus", "이첩");
successCount++;
} else {
// 정상 처리 (비교 로직에 해당 안됨)
@ -1016,168 +1032,43 @@ public class CarFfnlgTrgtServiceImpl implements CarFfnlgTrgtService {
}
/**
*
* ( )
*
* :
* 1. ( "상품용" )
* 2. ( 4 != ORG_CD 4)
* 3. 1
*
* @param existingData
* @param apiResponse API
* @param rgtr ( ID)
* @return (02=, 03=, null= )
* @param existingData
* @param apiResponse API
* @param userId ID
* @return
*/
private String processComparison(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String rgtr) {
// API 응답 데이터 유효성 검사
if (apiResponse.getBasicInfo() == null ||
apiResponse.getBasicInfo().getRecord() == null ||
apiResponse.getBasicInfo().getRecord().isEmpty()) {
log.warn("API 응답 데이터가 없습니다. 차량번호: {}", existingData.getVhclno());
return null;
}
go.kr.project.api.model.response.BasicResponse.Record basicInfo = apiResponse.getBasicInfo().getRecord().get(0);
String vhclno = existingData.getVhclno();
// ========== 1. 상품용 체크 ==========
String mberNm = basicInfo.getMberNm(); // 대표소유자성명
if (mberNm != null && mberNm.contains("상품용")) {
log.info("[상품용 감지] 차량번호: {}, 소유자명: {}", vhclno, mberNm);
// 업무 처리 상태 업데이트
existingData.setTaskPrcsSttsCd("02"); // 02=상품용
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(mberNm); // 자동차 기본 사항 조회 성명 저장
existingData.setCarBscMttrInqSggCd(null); // 이첩 필드는 null
existingData.setCarBscMttrInqSggNm(null);
private String executeComparisonLogic(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId) {
// ========== 방법 선택 (둘 중 하나만 사용) ==========
int updateResult = mapper.update(existingData);
if (updateResult > 0) {
log.info("[상품용 처리 완료] 차량번호: {}", vhclno);
return "02";
} else {
log.error("[상품용 업데이트 실패] 차량번호: {}", vhclno);
throw new RuntimeException("상품용 업데이트 실패: " + vhclno);
}
}
// ========== 2. 이첩 체크 ==========
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode(); // 사용본거지법정동코드
if (useStrnghldLegaldongCode != null && useStrnghldLegaldongCode.length() >= 4) {
// 사용자 정보 조회
SystemUserVO userInfo = userMapper.selectUser(rgtr);
if (userInfo == null || userInfo.getOrgCd() == null) {
log.warn("[이첩 체크 스킵] 사용자 정보 또는 조직코드가 없습니다. 사용자ID: {}", rgtr);
} else {
String userOrgCd = userInfo.getOrgCd();
// 법정동코드 앞 4자리와 사용자 조직코드 앞 4자리 비교
String legalDong4 = useStrnghldLegaldongCode.substring(0, 4);
String userOrg4 = userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : userOrgCd;
// 방법1: Chain of Responsibility 패턴 사용 (복잡하지만 확장성 좋음)
// return executeWithChainPattern(existingData, apiResponse, userId);
if (!legalDong4.equals(userOrg4)) {
log.info("[이첩 감지] 차량번호: {}, 법정동코드: {}, 사용자조직코드: {}",
vhclno, legalDong4, userOrg4);
// 시군구 코드 (법정동코드 앞 5자리)
String sggCd = useStrnghldLegaldongCode.length() >= 5
? useStrnghldLegaldongCode.substring(0, 5)
: useStrnghldLegaldongCode;
// 시군구명 조회
String sggNm = mapper.selectSggNmBySggCd(sggCd);
if (sggNm == null || sggNm.isEmpty()) {
log.warn("[이첩 경고] 시군구명 조회 실패. 시군구코드: {}", sggCd);
sggNm = ""; // 시군구명이 없어도 이첩 처리는 진행
}
// 업무 처리 상태 업데이트
existingData.setTaskPrcsSttsCd("03"); // 03=이첩
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(null); // 상품용 필드는 null
existingData.setCarBscMttrInqSggCd(sggCd); // 시군구 코드 저장
existingData.setCarBscMttrInqSggNm(sggNm); // 시군구명 저장
int updateResult = mapper.update(existingData);
if (updateResult > 0) {
log.info("[이첩 처리 완료] 차량번호: {}, 시군구: {}({})", vhclno, sggNm, sggCd);
return "03";
} else {
log.error("[이첩 업데이트 실패] 차량번호: {}", vhclno);
throw new RuntimeException("이첩 업데이트 실패: " + vhclno);
}
}
}
}
// 비교 로직에 해당되지 않음
log.info("[정상 처리] 차량번호: {} - 상품용, 이첩에 해당하지 않음", vhclno);
return null;
// 방법2: 일반 Service/Impl 패턴 사용 (간단하고 명확함) ⭐ 현재 사용 중
return executeWithServicePattern(existingData, apiResponse, userId);
}
/**
* API
* @deprecated processComparison
*
* @param existingData
* @param apiResponse API
* @return
* 1: Chain of Responsibility
*/
@Deprecated
private Map<String, Object> compareData(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse) {
Map<String, Object> comparison = new HashMap<>();
// 기본정보 비교
if (apiResponse.getBasicInfo() != null && apiResponse.getBasicInfo().getRecord() != null
&& !apiResponse.getBasicInfo().getRecord().isEmpty()) {
Map<String, String> basicComparison = new HashMap<>();
// 예시: 차량번호 비교
String existingVhclno = existingData.getVhclno();
String apiVhclno = apiResponse.getBasicInfo().getRecord().get(0).getVhrno();
basicComparison.put("차량번호_일치여부", Objects.equals(existingVhclno, apiVhclno) ? "일치" : "불일치");
basicComparison.put("기존_차량번호", existingVhclno);
basicComparison.put("API_차량번호", apiVhclno);
// 예시: 소유자명 비교
String existingOwnrNm = existingData.getOwnrNm();
String apiOwnrNm = apiResponse.getBasicInfo().getRecord().get(0).getMberNm();
basicComparison.put("소유자명_일치여부", Objects.equals(existingOwnrNm, apiOwnrNm) ? "일치" : "불일치");
basicComparison.put("기존_소유자명", existingOwnrNm);
basicComparison.put("API_소유자명", apiOwnrNm);
comparison.put("기본정보_비교", basicComparison);
}
// 등록원부 비교
if (apiResponse.getLedgerInfo() != null) {
Map<String, String> ledgerComparison = new HashMap<>();
@SuppressWarnings("unused")
private String executeWithChainPattern(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId) {
ComparisonContext context = ComparisonContext.builder()
.existingData(existingData)
.apiResponse(apiResponse)
.userId(userId)
.build();
// 예시: 차량번호 비교 (등록원부)
String existingVhclno = existingData.getVhclno();
String apiVhclno = apiResponse.getLedgerInfo().getVhrno();
ledgerComparison.put("차량번호_일치여부", Objects.equals(existingVhclno, apiVhclno) ? "일치" : "불일치");
comparison.put("등록원부_비교", ledgerComparison);
}
// 전체 일치 여부 판단
boolean allMatched = true;
if (apiResponse.getBasicInfo() != null && apiResponse.getBasicInfo().getRecord() != null
&& !apiResponse.getBasicInfo().getRecord().isEmpty()) {
String existingVhclno = existingData.getVhclno();
String apiVhclno = apiResponse.getBasicInfo().getRecord().get(0).getVhrno();
String existingOwnrNm = existingData.getOwnrNm();
String apiOwnrNm = apiResponse.getBasicInfo().getRecord().get(0).getMberNm();
if (!Objects.equals(existingVhclno, apiVhclno) || !Objects.equals(existingOwnrNm, apiOwnrNm)) {
allMatched = false;
}
}
comparison.put("전체_일치여부", allMatched ? "일치" : "불일치");
ComparisonResult result = comparisonRuleProcessor.process(context);
return result != null && result.isApplied() ? result.getStatusCode() : null;
}
return comparison;
/**
* 2: Service/Impl
*/
private String executeWithServicePattern(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId) {
return comparisonService.executeComparison(existingData, apiResponse, userId);
}
}

@ -0,0 +1,262 @@
package go.kr.project.carInspectionPenalty.registration.service.impl;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.carInspectionPenalty.registration.service.ComparisonService;
import go.kr.project.system.user.mapper.UserMapper;
import go.kr.project.system.user.model.SystemUserVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
*
*
* <p> .</p>
* <p> : private executeComparison() </p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ComparisonServiceImpl implements ComparisonService {
private final CarFfnlgTrgtMapper carFfnlgTrgtMapper;
private final UserMapper userMapper;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
/**
*
*
* <p> , .</p>
*/
@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 investigationClosedResult = checkInvestigationClosed(existingData, basicInfo);
// if (investigationClosedResult != null) {
// return investigationClosedResult;
// }
// 모든 비교 로직에 해당하지 않음
log.info("========== 비교 로직 종료 (정상): {} ==========", vhclno);
return null;
}
// ========================================
// 비교 로직 메서드들
// ========================================
/**
* 1.
*
* <p>: (MBER_NM) "상품용" </p>
* <p>: TASK_PRCS_STTS_CD = 02, CAR_BSC_MTTR_INQ_FLNM = </p>
*
* @return 02 () null ()
*/
private String checkProductUse(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo) {
String vhclno = existingData.getVhclno();
String mberNm = basicInfo.getMberNm(); // 대표소유자성명
// 조건 체크
if (mberNm == null || !mberNm.contains("상품용")) {
log.debug("[상품용] 조건 미충족. 차량번호: {}, 소유자명: {}", vhclno, mberNm);
return null;
}
log.info("[상품용] 조건 충족! 차량번호: {}, 소유자명: {}", vhclno, mberNm);
// DB 업데이트
existingData.setTaskPrcsSttsCd("02"); // 상품용
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(mberNm); // 소유자명 저장
existingData.setCarBscMttrInqSggCd(null);
existingData.setCarBscMttrInqSggNm(null);
int updateCount = carFfnlgTrgtMapper.update(existingData);
if (updateCount == 0) {
throw new RuntimeException(String.format("[상품용] 업데이트 실패: %s", vhclno));
}
log.info("[상품용] 처리 완료! 차량번호: {}", vhclno);
return "02";
}
/**
* 2. (OR : )
*
* <p>: TASK_PRCS_STTS_CD = 03, CAR_BSC_MTTR_INQ_SGG_CD = , CAR_BSC_MTTR_INQ_SGG_NM = </p>
*
* @return 03 () null ()
*/
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_OtherReason(basicInfo, userId, vhclno)) {
// return processTransfer(existingData, basicInfo, vhclno, "다른 사유");
// }
// 모든 이첩 조건에 해당하지 않음
log.debug("[이첩] 모든 조건 미충족. 차량번호: {}", vhclno);
return null;
}
/**
* 1:
* 4 != 4
*/
private boolean checkTransferCondition1_LegalDongMismatch(BasicResponse.Record basicInfo, String userId, String vhclno) {
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode();
// 법정동코드 유효성 검사
if (useStrnghldLegaldongCode == null || useStrnghldLegaldongCode.length() < 4) {
log.debug("[이첩][조건1] 법정동코드 없음. 차량번호: {}", vhclno);
return false;
}
// 사용자 정보 조회
SystemUserVO userInfo = userMapper.selectUser(userId);
if (userInfo == null || userInfo.getOrgCd() == null) {
log.debug("[이첩][조건1] 사용자 정보 없음. 사용자ID: {}", userId);
return false;
}
// 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 비교
String legalDong4 = useStrnghldLegaldongCode.substring(0, 4);
String userOrgCd = userInfo.getOrgCd();
String userOrg4 = userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : userOrgCd;
if (legalDong4.equals(userOrg4)) {
log.debug("[이첩][조건1] 법정동코드 일치. 차량번호: {}, 법정동: {}, 조직: {}",
vhclno, legalDong4, userOrg4);
return false;
}
log.info("[이첩][조건1] 법정동코드 불일치! 차량번호: {}, 법정동: {}, 조직: {}",
vhclno, legalDong4, userOrg4);
return true;
}
/**
* ( )
*/
private String processTransfer(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo,
String vhclno, String reason) {
log.info("[이첩] 조건 충족! 차량번호: {}, 사유: {}", vhclno, reason);
// 시군구 코드 및 시군구명 조회
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode();
String sggCd = (useStrnghldLegaldongCode != null && useStrnghldLegaldongCode.length() >= 5)
? useStrnghldLegaldongCode.substring(0, 5)
: (useStrnghldLegaldongCode != null ? useStrnghldLegaldongCode : "");
String sggNm = carFfnlgTrgtMapper.selectSggNmBySggCd(sggCd);
if (sggNm == null || sggNm.isEmpty()) {
log.warn("[이첩] 시군구명 조회 실패. 시군구코드: {}", sggCd);
sggNm = "";
}
// DB 업데이트
existingData.setTaskPrcsSttsCd("03"); // 이첩
existingData.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER));
existingData.setCarBscMttrInqFlnm(null);
existingData.setCarBscMttrInqSggCd(sggCd); // 시군구 코드
existingData.setCarBscMttrInqSggNm(sggNm); // 시군구명
int updateCount = carFfnlgTrgtMapper.update(existingData);
if (updateCount == 0) {
throw new RuntimeException(String.format("[이첩] 업데이트 실패: %s", vhclno));
}
log.info("[이첩] 처리 완료! 차량번호: {}, 시군구: {}({}), 사유: {}", vhclno, sggNm, sggCd, reason);
return "03";
}
/**
* 3. ( - )
*
* <p>: </p>
* <p>: TASK_PRCS_STTS_CD = 04</p>
*
* @return 04 () null ()
*/
@SuppressWarnings("unused")
private String checkInvestigationClosed(CarFfnlgTrgtVO existingData, BasicResponse.Record basicInfo) {
String vhclno = existingData.getVhclno();
String ersrRegistSeCode = basicInfo.getErsrRegistSeCode(); // 말소등록구분코드
// 조건 체크: 말소된 차량인지
if (ersrRegistSeCode == null || ersrRegistSeCode.isEmpty()) {
log.debug("[내사종결] 조건 미충족. 차량번호: {}", vhclno);
return null;
}
log.info("[내사종결] 조건 충족! 차량번호: {}, 말소구분: {}", vhclno, ersrRegistSeCode);
// DB 업데이트
existingData.setTaskPrcsSttsCd("04"); // 내사종결
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 "04";
}
// ========================================
// 유틸리티 메서드
// ========================================
/**
* API
*/
private boolean isValidApiResponse(VehicleApiResponseVO apiResponse) {
return apiResponse != null
&& apiResponse.getBasicInfo() != null
&& apiResponse.getBasicInfo().getRecord() != null
&& !apiResponse.getBasicInfo().getRecord().isEmpty();
}
}

@ -0,0 +1,263 @@
# 이첩 조건 추가 방법 (OR 구조)
## 📌 현재 구조
이첩 비교 로직은 **OR 구조**로 되어 있습니다.
- **여러 조건 중 하나라도 만족하면** 이첩으로 처리
- 조건을 순차적으로 체크하다가 **하나라도 true가 나오면 즉시 이첩 처리**
---
## 🎯 새로운 이첩 조건 추가하기
### 방법1: Service/Impl 패턴 (간단) ⭐ 추천
`ComparisonServiceImpl.java` 파일 수정
#### 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;
}
```
#### 끝! 매우 간단합니다.
---
### 방법2: Chain of Responsibility 패턴
`TransferComparisonRule.java` 파일 수정
#### 1단계: 조건 체크 메서드 추가
```java
/**
* 조건2: 주소 불일치 체크
*/
private boolean checkAddressMismatch(ComparisonContext context,
go.kr.project.api.model.response.BasicResponse.Record basicInfo,
String vhclno) {
String useStrnghldAdresNm = basicInfo.getUseStrnghldAdresNm();
String ownerAdresNm = basicInfo.getOwnerAdresNm();
if (useStrnghldAdresNm == null || ownerAdresNm == null) {
log.debug("[{}][조건2] 주소 정보 없음. 차량번호: {}", getRuleName(), vhclno);
return false;
}
if (useStrnghldAdresNm.equals(ownerAdresNm)) {
log.debug("[{}][조건2] 주소 일치. 차량번호: {}", getRuleName(), vhclno);
return false;
}
log.info("[{}][조건2] 주소 불일치! 차량번호: {}", getRuleName(), vhclno);
return true;
}
```
#### 2단계: execute() 메서드에 조건 추가
```java
@Override
public ComparisonResult execute(ComparisonContext context) {
String vhclno = context.getVhclno();
// ... API 유효성 검사 ...
var basicInfo = context.getApiResponse().getBasicInfo().getRecord().get(0);
// ========== 이첩 조건들 (OR 로직) ==========
// 조건1: 법정동코드 불일치
if (checkLegalDongCodeMismatch(context, basicInfo, vhclno)) {
return processTransfer(context, basicInfo, vhclno, "법정동코드 불일치");
}
// 조건2: 주소 불일치 (새로 추가!)
if (checkAddressMismatch(context, basicInfo, vhclno)) {
return processTransfer(context, basicInfo, vhclno, "주소 불일치");
}
return ComparisonResult.notApplied();
}
```
---
## 📊 현재 이첩 조건 목록
### 조건1: 법정동코드 불일치 ✅ 구현됨
- **체크 항목**: 사용본거지법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리
- **이첩 기준**: 두 값이 다르면 이첩
### 조건2: (예시) 주소 불일치
- **체크 항목**: 사용본거지주소 vs 소유자주소
- **이첩 기준**: 주소가 다르면 이첩
### 조건3: (예시) 기타 조건들...
- 필요에 따라 추가
---
## 💡 동작 방식
```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
```
---
## ⚠️ 주의사항
1. **조건 순서**: 위에서부터 순차적으로 체크됩니다
- 자주 걸리는 조건을 위쪽에 배치하면 성능 향상
2. **조건 메서드 네이밍**: `checkTransferConditionN_XXX` 형식 권장
- 예: `checkTransferCondition1_LegalDongMismatch`
- 예: `checkTransferCondition2_AddressMismatch`
3. **return 값**:
- `true`: 이 조건에 **해당함** → 이첩 처리
- `false`: 이 조건에 **해당 안함** → 다음 조건 체크
4. **processTransfer() 공통 사용**:
- DB 업데이트는 `processTransfer()` 메서드에서 공통으로 처리
- 각 조건에서는 `true/false`만 반환
---
## 📝 체크리스트
새로운 이첩 조건 추가 시 확인사항:
- [ ] 조건 메서드 작성 완료
- [ ] checkTransfer()에 조건 추가 완료
- [ ] 로그 메시지에 조건 번호 포함 (`[조건N]`)
- [ ] 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");
}
```
---
끝!
Loading…
Cancel
Save