# 자동차 과태료 비교 로직 명세서 ## 개요 자동차 과태료 부과 대상을 검증하기 위한 비교 로직 정의서입니다. ### 구현 위치 - **서비스**: `ComparisonServiceImpl.java` - **경로**: `src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/ComparisonServiceImpl.java` ### 기본 설정 - 비교로직에 사용되는 API는 `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출 - 날짜 유틸리티: `DateUtil.parseDate()`, `DateUtil.formatDateString()` 사용 - 일수 계산 기준: `DAYS_THRESHOLD = 31일` ### 문서 이력 | 일자 | 변경 내용 | 비고 | |------|----------|------| | 2025-12-03 | 명의이전 케이스 추가 (내사종결, 날짜 수정 후 부과) | 상품용과 순수 명의이전 분리 | | 2025-12-02 | 내사종결 로직 추가 및 상품용 조건 변경 | 명의이전 31일 이내 조건 추가 | | 2025-01-XX | 상품용 로직 완전 변경 | API 호출 4단계, 소유자명 일치 확인 추가 | ### 처리 규칙 > **중요**: 순서가 중요함! > 1. 상품용 → 2. 내사종결(상품용) → 3. 내사종결(명의이전) → 4. 날짜수정(상품용) → 5. 날짜수정(명의이전) → 6. 이첩 > - 조건에 걸리는 순간 다음 차량번호 비교로 진행 > - 각 비교 로직별로 개별 API 호출 수행 --- ## 비교 로직 상세 ### 1. 상품용 검증 - 검사일 소유자가 상품용 **처리상태코드**: `02` (상품용) **메서드**: `checkProductUse()` #### API 호출 순서 | 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 | |------|-----|--------------|-------------|------| | 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 | | 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` | 현재 소유자 정보 | | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=조건에맞는CHG_YMD` | `소유자명` | 명의이전 시점 소유자 확인 | #### 비교 조건 ```java // 조건 1: 소유자명에 '상품용' 포함 여부 if (!step1Record.소유자명.contains("상품용")) { return null; // 미충족 } // 조건 2: 갑부 상세에서 조건에 맞는 명의이전 레코드 찾기 // - CHG_TASK_SE_CD == "11" (명의이전) // - CHG_YMD <= 검사종료일자 // - 가장 마지막 일자 // 조건 3: 명의이전 시점 소유자명 == 검사일 기준 소유자명 if (!step4Record.소유자명.equals(step1Record.소유자명)) { return null; // 소유자명 불일치 } return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE; // 상품용 조건 충족 ``` #### 결과 처리 - **업무 처리 상태 코드**: `02` (상품용) - **비고 컬럼 형식**: ``` 상품용 - 상품용검사 ``` --- ### 2. 내사종결 검증 - 명의이전 전 소유자 상품용 (31일 이내) **처리상태코드**: `04` (내사종결) **메서드**: `checkInvestigationClosedByProductUseWithin31Days()` #### API 호출 순서 | 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 | |------|-----|--------------|-------------|------| | 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 | | 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` | 현재 소유자 정보 | | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 | #### 비교 조건 ```java // 조건 1: 검사일 소유자명에 '상품용' 미포함 (상품용은 1번에서 처리됨) if (step1Record.소유자명 == null || step1Record.소유자명.contains("상품용")) { return null; } // 조건 2: 갑부 상세에서 검사일 이전 가장 마지막 명의이전(11) 레코드 찾기 // - CHG_TASK_SE_CD == "11" (명의이전) // - CHG_YMD <= 검사일자 // - 가장 마지막 일자 // 조건 3: 명의이전일자가 검사일의 31일 이내 long daysBetween = ChronoUnit.DAYS.between(latestChgDate, 검사일); if (daysBetween < 0 || daysBetween > 31) { return null; // 31일 초과 } // 조건 4: 명의이전 직전 시점(CHG_YMD-1일) 소유자명에 '상품용' 포함 if (step4Record.소유자명 == null || !step4Record.소유자명.contains("상품용")) { return null; // 명의이전 직전 소유자가 상품용이 아님 } return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; ``` #### 로직 설명 **검사일 당시에는 상품용이 아니었지만, 명의이전 이전(31일 이내)에는 상품용이었던 경우** 1. 검사일 소유자가 상품용 아님 2. 검사일 이전 31일 이내에 명의이전(코드 11) 발생 3. 명의이전 직전 소유자가 상품용 #### 결과 처리 - **업무 처리 상태 코드**: `04` (내사종결) - **비고 컬럼 형식**: ``` 명의이전(25.9.3.) 이전소유자 상품용 22루2283 - 검사기간: 2024-08-01 - 2024-08-31 - 검사일: 2024-08-15 - 명의이전: 2024-09-03 - 상품용: 2024-09-03 일수차이: 19일 ``` --- ### 3. 내사종결 검증 - 순수 명의이전 (31일 이내) **처리상태코드**: `04` (내사종결) **메서드**: `checkInvestigationClosedByOwnerChangeWithin31Days()` #### API 호출 순서 | 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 | |------|-----|--------------|-------------|------| | 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 | | 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` | 현재 소유자 정보 | | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 | #### 비교 조건 ```java // 조건 1: 검사일 소유자명에 '상품용' 미포함 if (step1Record.소유자명 != null && step1Record.소유자명.contains("상품용")) { return null; // 상품용이면 1번에서 처리됨 } // 조건 2: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(11) 레코드 찾기 // - CHG_TASK_SE_CD == "11" (명의이전) // - 유효기간만료일 <= CHG_YMD <= 검사종료일자 // 조건 3: 명의이전일자가 검사일의 31일 이내 long daysBetween = ChronoUnit.DAYS.between(chgDate, 검사일); if (daysBetween < 0 || daysBetween > 31) { return null; } // 조건 4: 명의이전 직전 소유자가 상품용 아님 (상품용이면 2번에서 처리됨) if (step4Record.소유자명 == null || step4Record.소유자명.contains("상품용")) { return null; // 상품용이면 이미 2번에서 처리됨 } return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; ``` #### 로직 설명 **순수 명의이전 케이스 (상품용과 무관)** 1. 검사일 소유자가 상품용 아님 2. 검사기간 내 명의이전(코드 11) 발생 3. 명의이전일자가 검사일의 31일 이내 4. 명의이전 직전 소유자도 상품용 아님 (상품용이면 2번 로직에서 처리됨) #### 결과 처리 - **업무 처리 상태 코드**: `04` (내사종결) - **비고 컬럼 형식**: ``` 명의이전(25.9.3.) 22루2283 - 검사기간: 2024-08-01 - 2024-08-31 - 검사일: 2024-08-15 - 명의이전: 2024-09-03 - 상품용: 2024-09-03 일수차이: 19일 ``` --- ### 4. 날짜 수정 후 부과 검증 - 명의이전 전 소유자 상품용 (31일 초과) **처리상태코드**: `05` (날짜 수정 후 부과) **메서드**: `checkDateModifiedLevyByProductUseOver31Days()` #### API 호출 순서 | 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 | |------|-----|--------------|-------------|------| | 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 | | 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` | 현재 소유자 정보 | | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 | #### 비교 조건 ```java // 조건 1: 검사일 소유자명에 '상품용' 미포함 if (step1Record.소유자명 == null || step1Record.소유자명.contains("상품용")) { return null; } // 조건 2: 갑부 상세에서 검사일 이전 가장 마지막 명의이전(11) 레코드 찾기 // - CHG_TASK_SE_CD == "11" (명의이전) // - CHG_YMD <= 검사일자 // - 가장 마지막 일자 // 조건 3: 명의이전일자가 검사일의 31일 초과 long daysBetween = ChronoUnit.DAYS.between(latestChgDate, 검사일); if (daysBetween <= 31) { return null; // 31일 이내면 2번에서 처리됨 } // 조건 4: 명의이전 직전 소유자가 상품용 if (step4Record.소유자명 == null || !step4Record.소유자명.contains("상품용")) { return null; } return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY; ``` #### 로직 설명 **명의이전 전 소유자가 상품용이지만 31일을 초과한 경우** 1. 검사일 소유자가 상품용 아님 2. 검사일 이전에 명의이전(코드 11) 발생 3. 명의이전일자가 검사일의 31일 초과 4. 명의이전 직전 소유자가 상품용 #### 결과 처리 - **업무 처리 상태 코드**: `05` (날짜 수정 후 부과) - **비고 컬럼 형식**: 2번과 동일 --- ### 5. 날짜 수정 후 부과 검증 - 순수 명의이전 (31일 초과) **처리상태코드**: `05` (날짜 수정 후 부과) **메서드**: `checkDateModifiedLevyByOwnerChangeOver31Days()` #### API 호출 순서 | 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 | |------|-----|--------------|-------------|------| | 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 | | 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` | 현재 소유자 정보 | | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 | #### 비교 조건 ```java // 조건 1: 검사일 소유자명에 '상품용' 미포함 if (step1Record.소유자명 != null && step1Record.소유자명.contains("상품용")) { return null; } // 조건 2: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(11) 레코드 찾기 // - CHG_TASK_SE_CD == "11" (명의이전) // - 유효기간만료일 <= CHG_YMD <= 검사종료일자 // 조건 3: 명의이전일자가 검사일의 31일 초과 long daysBetween = ChronoUnit.DAYS.between(chgDate, 검사일); if (daysBetween <= 31) { return null; // 31일 이내면 3번에서 처리됨 } // 조건 4: 명의이전 직전 소유자가 상품용 아님 if (step4Record.소유자명 == null || step4Record.소유자명.contains("상품용")) { return null; // 상품용이면 4번에서 처리됨 } return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY; ``` #### 로직 설명 **순수 명의이전 케이스 중 31일을 초과한 경우** 1. 검사일 소유자가 상품용 아님 2. 검사기간 내 명의이전(코드 11) 발생 3. 명의이전일자가 검사일의 31일 초과 4. 명의이전 직전 소유자도 상품용 아님 (상품용이면 4번 로직에서 처리됨) #### 결과 처리 - **업무 처리 상태 코드**: `05` (날짜 수정 후 부과) - **비고 컬럼 형식**: 3번과 동일 --- ### 6. 이첩 검증 (이첩-1, 이첩-2) **처리상태코드**: `03` (이첩) **메서드**: `checkTransferCase115Day()` #### 부과기준일 결정 ```java int dayCnt = TB_CAR_FFNLG_TRGT.DAYCNT; if (dayCnt > 115) { // 이첩-2 부과기준일 = TB_CAR_FFNLG_TRGT.검사종료일자.plusDays(115); } else { // 이첩-1 부과기준일 = TB_CAR_FFNLG_TRGT.검사일자; } ``` #### 법정동코드 비교 ```java // 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 String legalDong4 = useStrnghldLegaldongCode.substring(0, 4); String userOrg4 = userInfo.getOrgCd().substring(0, 4); if (!legalDong4.equals(userOrg4)) { return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER; // 이첩 } ``` #### 결과 처리 | 구분 | 조건 | 비고 컬럼 형식 | |------|------|---------------| | 이첩-1 | `DAYCNT <= 115` | `"서울시 용산구/ 이경호, 검사일사용본거지, [검사대상, 사용자 조직코드 앞 4자리 및 법정동명]"` | | 이첩-2 | `DAYCNT > 115` | `"전라남도 순천시 / 김정대, 115일 도래지, [2개의 api 법정동코드 및 법정동명]"` | --- ## 처리 흐름도 ``` 시작 │ ▼ [차량번호 조회] │ ▼ ┌─────────────────────────────────────┐ │ 1. 상품용 (검사일 소유자가 상품용) │ │ → 조건 충족: 상품용(02) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ ┌─────────────────────────────────────┐ │ 2. 내사종결 - 명의이전 전 상품용 │ │ (31일 이내) │ │ → 조건 충족: 내사종결(04) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ ┌─────────────────────────────────────┐ │ 3. 내사종결 - 순수 명의이전 │ │ (31일 이내, 상품용 아님) │ │ → 조건 충족: 내사종결(04) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ ┌─────────────────────────────────────┐ │ 4. 날짜수정 - 명의이전 전 상품용 │ │ (31일 초과) │ │ → 조건 충족: 날짜수정후부과(05) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ ┌─────────────────────────────────────┐ │ 5. 날짜수정 - 순수 명의이전 │ │ (31일 초과, 상품용 아님) │ │ → 조건 충족: 날짜수정후부과(05) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ ┌─────────────────────────────────────┐ │ 6. 이첩 (법정동코드 불일치) │ │ → 조건 충족: 이첩(03) │ └─────────────────────────────────────┘ │ (조건 미충족) ▼ [다음 차량] ``` --- ## 요약 정리 ### 비교 로직 실행 순서 ```java public String executeComparison(CarFfnlgTrgtVO existingData) { // 1. 상품용 String result1 = checkProductUse(existingData); if (result1 != null) return result1; // "02" // 2. 내사종결 - 명의이전 전 소유자 상품용 (31일 이내) String result2 = checkInvestigationClosedByProductUseWithin31Days(existingData); if (result2 != null) return result2; // "04" // 3. 내사종결 - 순수 명의이전 (31일 이내) String result3 = checkInvestigationClosedByOwnerChangeWithin31Days(existingData); if (result3 != null) return result3; // "04" // 4. 날짜 수정 후 부과 - 명의이전 전 소유자 상품용 (31일 초과) String result4 = checkDateModifiedLevyByProductUseOver31Days(existingData); if (result4 != null) return result4; // "05" // 5. 날짜 수정 후 부과 - 순수 명의이전 (31일 초과) String result5 = checkDateModifiedLevyByOwnerChangeOver31Days(existingData); if (result5 != null) return result5; // "05" // 6. 이첩 String result6 = checkTransferCase115Day(existingData); if (result6 != null) return result6; // "03" // 모든 조건 미충족 return null; } ``` ### 처리상태코드 매핑 | 코드 | 상태명 | 처리 로직 | 메서드 | |------|--------|----------|--------| | 02 | 상품용 | 검사일 소유자가 상품용 | `checkProductUse()` | | 04 | 내사종결 | 명의이전 전 상품용 (31일 이내) | `checkInvestigationClosedByProductUseWithin31Days()` | | 04 | 내사종결 | 순수 명의이전 (31일 이내) | `checkInvestigationClosedByOwnerChangeWithin31Days()` | | 05 | 날짜 수정 후 부과 | 명의이전 전 상품용 (31일 초과) | `checkDateModifiedLevyByProductUseOver31Days()` | | 05 | 날짜 수정 후 부과 | 순수 명의이전 (31일 초과) | `checkDateModifiedLevyByOwnerChangeOver31Days()` | | 03 | 이첩 | 법정동코드 불일치 | `checkTransferCase115Day()` | ### 상품용 vs 순수 명의이전 비교 | 구분 | 상품용 케이스 (2, 4번) | 순수 명의이전 케이스 (3, 5번) | |------|----------------------|----------------------------| | 검사일 소유자 | 상품용 아님 | 상품용 아님 | | 명의이전 직전 소유자 | 상품용 **포함** | 상품용 **아님** | | Step 4 확인사항 | 상품용 포함 여부 | 상품용 미포함 여부 (상품용이면 이미 2, 4번에서 처리) | | 일수 기준 | 31일 이내/초과 | 31일 이내/초과 | | 처리상태코드 | 31일 이내: 04, 초과: 05 | 31일 이내: 04, 초과: 05 | --- ## 구현 완료 상태 ### ✅ 1. 상품용 로직 - **메서드**: `checkProductUse()` - **처리상태코드**: 02 ### ✅ 2. 내사종결 - 명의이전 전 소유자 상품용 (31일 이내) - **메서드**: `checkInvestigationClosedByProductUseWithin31Days()` - **처리상태코드**: 04 ### ✅ 3. 내사종결 - 순수 명의이전 (31일 이내) - **메서드**: `checkInvestigationClosedByOwnerChangeWithin31Days()` - **처리상태코드**: 04 - **특징**: 명의이전 직전 소유자도 상품용 아님 ### ✅ 4. 날짜 수정 후 부과 - 명의이전 전 소유자 상품용 (31일 초과) - **메서드**: `checkDateModifiedLevyByProductUseOver31Days()` - **처리상태코드**: 05 ### ✅ 5. 날짜 수정 후 부과 - 순수 명의이전 (31일 초과) - **메서드**: `checkDateModifiedLevyByOwnerChangeOver31Days()` - **처리상태코드**: 05 - **특징**: 명의이전 직전 소유자도 상품용 아님 ### ✅ 6. 이첩 로직 - **메서드**: `checkTransferCase115Day()` - **처리상태코드**: 03 --- ## 구현 시 주의사항 ### 공통 주의사항 1. **API 호출 순서 준수**: 각 검증 단계별로 필요한 API만 호출 2. **조건 우선순위**: 1 → 2 → 3 → 4 → 5 → 6 순서로 검증 3. **조기 종료**: 조건 충족 시 즉시 다음 차량으로 이동 4. **비고 컬럼**: 각 조건별 정해진 형식으로 기록 5. **날짜 형식 지원**: yyyyMMdd, yyyy-MM-dd 두 형식 모두 자동 처리 6. **일수 계산 기준**: DAYS_THRESHOLD = 31일 (상수 사용) ### 순수 명의이전 케이스 (3, 5번) 주의사항 1. **상품용 체크**: Step 4에서 명의이전 직전 소유자가 상품용이면 return null - 상품용이면 이미 2번 또는 4번에서 처리되어야 함 2. **로직 순서 중요**: 상품용 케이스(2, 4번)보다 반드시 뒤에 실행 3. **DB 저장값**: step4OwnerName을 CAR_BSC_MTTR_INQ_FLNM에 저장