diff --git a/docs/자동차과태료_비교로직_정리.md b/docs/자동차과태료_비교로직_정리.md index d93a37b..3314d4e 100644 --- a/docs/자동차과태료_비교로직_정리.md +++ b/docs/자동차과태료_비교로직_정리.md @@ -10,20 +10,21 @@ ### 기본 설정 - 비교로직에 사용되는 API는 `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출 -- 날짜 유틸리티: `DateUtil.parseDate()`, `DateUtil.isDateBetween()`, `DateUtil.formatDateString()` 사용 +- 날짜 유틸리티: `DateUtil.parseDate()`, `DateUtil.formatDateString()` 사용 +- 일수 계산 기준: `DAYS_THRESHOLD = 31일` ### 문서 이력 | 일자 | 변경 내용 | 비고 | |------|----------|------| -| 2025-12-02 | 내사종결 로직 추가 및 상품용 조건 변경 | 명의이전 30일 이내 조건 추가 | +| 2025-12-03 | 명의이전 케이스 추가 (내사종결, 날짜 수정 후 부과) | 상품용과 순수 명의이전 분리 | +| 2025-12-02 | 내사종결 로직 추가 및 상품용 조건 변경 | 명의이전 31일 이내 조건 추가 | | 2025-01-XX | 상품용 로직 완전 변경 | API 호출 4단계, 소유자명 일치 확인 추가 | - ### 처리 규칙 > **중요**: 순서가 중요함! -> - 1. 상품용 검증 (Case1) → 2. 내사종결 상품용 검증 (Case2) → 3. 이첩 검증 +> 1. 상품용 → 2. 내사종결(상품용) → 3. 내사종결(명의이전) → 4. 날짜수정(상품용) → 5. 날짜수정(명의이전) → 6. 이첩 > - 조건에 걸리는 순간 다음 차량번호 비교로 진행 > - 각 비교 로직별로 개별 API 호출 수행 @@ -31,10 +32,10 @@ ## 비교 로직 상세 -### 1. 상품용 검증 Case1 - 검사일 소유자가 상품용 +### 1. 상품용 검증 - 검사일 소유자가 상품용 **처리상태코드**: `02` (상품용) -**메서드**: `checkProductUseCase1()` (ComparisonServiceImpl.java:113~306) +**메서드**: `checkProductUse()` #### API 호출 순서 @@ -45,79 +46,41 @@ | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=조건에맞는CHG_YMD` | `소유자명` | 명의이전 시점 소유자 확인 | -#### 비교 조건 (순차 실행) +#### 비교 조건 ```java -// 조건 1: 소유자명에 '상품용' 포함 여부 (Line 138-142) +// 조건 1: 소유자명에 '상품용' 포함 여부 if (!step1Record.소유자명.contains("상품용")) { return null; // 미충족 } -// 조건 2: 갑부 상세에서 조건에 맞는 명의이전 레코드 찾기 (Line 187-234) -LocalDate latestChgDate = null; -NewLedgerResponse.Record targetRecord = null; - -for (NewLedgerResponse.Record record : ledgerRecords) { - // 2-1. 명의이전 코드 확인 (Line 199) - if (!"11".equals(record.CHG_TASK_SE_CD)) { - continue; // 명의이전이 아니면 스킵 - } - - // 2-2. CHG_YMD <= 검사종료일자 (Line 208-213) - if (record.CHG_YMD > TB_CAR_FFNLG_TRGT.검사종료일자) { - continue; // 검사종료일자 이후면 스킵 - } - - // 주석처리됨: CHG_YMD <= 검사일 조건 (Line 215-221) - // → 검사일 이후 데이터도 포함 (검사종료일자까지만 제한) - - // 2-3. 가장 마지막 일자 찾기 (Line 224-228) - if (latestChgDate == null || record.CHG_YMD > latestChgDate) { - latestChgDate = record.CHG_YMD; - targetRecord = record; - } -} - -if (targetRecord == null) { - return null; // 조건에 맞는 레코드 없음 -} +// 조건 2: 갑부 상세에서 조건에 맞는 명의이전 레코드 찾기 +// - CHG_TASK_SE_CD == "11" (명의이전) +// - CHG_YMD <= 검사종료일자 +// - 가장 마지막 일자 -// 조건 3: 명의이전 시점 소유자명 == 검사일 기준 소유자명 (Line 257-260) +// 조건 3: 명의이전 시점 소유자명 == 검사일 기준 소유자명 if (!step4Record.소유자명.equals(step1Record.소유자명)) { return null; // 소유자명 불일치 } -// 주석처리됨: CHG_YMD 기간 범위 확인 (Line 264-273) -// → 기간 범위 체크하지 않음 - return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE; // 상품용 조건 충족 ``` -#### 주요 변경사항 - -1. **주석처리된 조건**: - - ~~`CHG_YMD <= 검사일` (검사일 이전일자 중)~~ → 제거됨 - - ~~`CHG_YMD가 유효기간만료일 ~ 검사종료일자 범위 내`~~ → 제거됨 - -2. **적용되는 조건**: - - `CHG_YMD <= 검사종료일자` (검사종료일자 이전) - - 명의이전(코드 11) 레코드 중 가장 마지막 일자 - - 소유자명 일치 확인 - #### 결과 처리 - **업무 처리 상태 코드**: `02` (상품용) -- **비고 컬럼 형식**: (Line 732-761, 대부분 주석처리됨) +- **비고 컬럼 형식**: ``` 상품용 - 상품용검사 ``` --- -### 2. 내사종결 상품용 검증 Case2 - 명의이전 후 상품용 +### 2. 내사종결 검증 - 명의이전 전 소유자 상품용 (31일 이내) **처리상태코드**: `04` (내사종결) -**메서드**: `checkCloseProductUse()` (ComparisonServiceImpl.java:329~515) +**메서드**: `checkInvestigationClosedByProductUseWithin31Days()` #### API 호출 순서 @@ -128,66 +91,44 @@ return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE; // 상품용 조 | 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List | 명의이전 이력 조회 | | 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 | -#### 비교 조건 (순차 실행) +#### 비교 조건 ```java -// 조건 1: 소유자명에 '상품용' 미포함 (Line 354-358) +// 조건 1: 검사일 소유자명에 '상품용' 미포함 (상품용은 1번에서 처리됨) if (step1Record.소유자명 == null || step1Record.소유자명.contains("상품용")) { - return null; // 미충족 (검사일 소유자가 상품용이면 Case1에서 처리됨) -} - -// 조건 2: 갑부 상세에서 조건에 맞는 명의이전 레코드 찾기 (Line 403-437) -LocalDate latestChgDate = null; -NewLedgerResponse.Record targetRecord = null; - -for (NewLedgerResponse.Record record : ledgerRecords) { - // 2-1. 명의이전 코드 확인 (Line 415) - if (!"11".equals(record.CHG_TASK_SE_CD)) { - continue; // 명의이전이 아니면 스킵 - } - - // 2-2. CHG_YMD <= 검사일자 (Line 424-429) - if (record.CHG_YMD > TB_CAR_FFNLG_TRGT.검사일자) { - continue; // 검사일자 이후면 스킵 - } - - // 2-3. 가장 마지막 일자 찾기 (Line 432-436) - if (latestChgDate == null || record.CHG_YMD > latestChgDate) { - latestChgDate = record.CHG_YMD; - targetRecord = record; - } + return null; } -if (targetRecord == null) { - return null; // 조건에 맞는 레코드 없음 -} +// 조건 2: 갑부 상세에서 검사일 이전 가장 마지막 명의이전(11) 레코드 찾기 +// - CHG_TASK_SE_CD == "11" (명의이전) +// - CHG_YMD <= 검사일자 +// - 가장 마지막 일자 -// 조건 3: 명의이전일자가 검사일의 30일 이내인지 확인 (Line 444-450) +// 조건 3: 명의이전일자가 검사일의 31일 이내 long daysBetween = ChronoUnit.DAYS.between(latestChgDate, 검사일); -if (daysBetween < 0 || daysBetween > 30) { - return null; // 30일 초과 +if (daysBetween < 0 || daysBetween > 31) { + return null; // 31일 초과 } -// 조건 4: 명의이전 직전 시점(CHG_YMD-1일) 소유자명에 '상품용' 포함 (Line 476-482) -// Step 4 API: 부과일자 = CHG_YMD - 1일 (Line 457-464) +// 조건 4: 명의이전 직전 시점(CHG_YMD-1일) 소유자명에 '상품용' 포함 if (step4Record.소유자명 == null || !step4Record.소유자명.contains("상품용")) { return null; // 명의이전 직전 소유자가 상품용이 아님 } -return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; // 내사종결 조건 충족 +return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; ``` #### 로직 설명 -이 로직은 **검사일 당시에는 상품용이 아니었지만, 명의이전 이전(30일 이내)에는 상품용이었던 경우**를 처리합니다: -1. 검사일 소유자가 상품용이 아님 -2. 검사일 이전 30일 이내에 명의이전(코드 11) 발생 +**검사일 당시에는 상품용이 아니었지만, 명의이전 이전(31일 이내)에는 상품용이었던 경우** +1. 검사일 소유자가 상품용 아님 +2. 검사일 이전 31일 이내에 명의이전(코드 11) 발생 3. 명의이전 직전 소유자가 상품용 #### 결과 처리 - **업무 처리 상태 코드**: `04` (내사종결) -- **비고 컬럼 형식**: (Line 784-818) +- **비고 컬럼 형식**: ``` 명의이전(25.9.3.) 이전소유자 상품용 22루2283 @@ -195,21 +136,195 @@ return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED; // 내 - 검사일: 2024-08-15 - 명의이전: 2024-09-03 - 상품용: 2024-09-03 +일수차이: 19일 ``` --- -### 3. 이첩 검증 (이첩-1, 이첩-2 병합 로직) +### 3. 내사종결 검증 - 순수 명의이전 (31일 이내) -**처리상태코드**: `03` (이첩) -**메서드**: `checkTransferCase115Day()` (ComparisonServiceImpl.java:531~648) +**처리상태코드**: `04` (내사종결) +**메서드**: `checkInvestigationClosedByOwnerChangeWithin31Days()` -**필요 API**: 자동차기본정보 +#### 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; // textFile 일수 +int dayCnt = TB_CAR_FFNLG_TRGT.DAYCNT; if (dayCnt > 115) { // 이첩-2 @@ -220,57 +335,15 @@ if (dayCnt > 115) { } ``` -#### API 호출 +#### 법정동코드 비교 ```java -// 부과기준일 기준으로 자동차기본정보 API 호출 -BasicResponse response = 자동차기본정보API.call(부과기준일, 차량번호); -``` +// 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 +String legalDong4 = useStrnghldLegaldongCode.substring(0, 4); +String userOrg4 = userInfo.getOrgCd().substring(0, 4); -#### 법정동코드 비교 로직 (공통) - -```java -/** - * 이첩 조건: 법정동코드 불일치 검증 - * 사용본거지법정동코드 앞 4자리 != 사용자 조직코드 앞 4자리 - */ -private boolean checkTransferCondition_LegalDongMismatch( - BasicResponse.Record basicInfo, - String userId, - String vhclno -) { - String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode(); - - // 1. 법정동코드 유효성 검사 - if (useStrnghldLegaldongCode == null || useStrnghldLegaldongCode.length() < 4) { - log.debug("[이첩] 법정동코드 없음. 차량번호: {}", vhclno); - return false; - } - - // 2. 사용자 정보 조회 - SystemUserVO userInfo = userMapper.selectUser(userId); - if (userInfo == null || userInfo.getOrgCd() == null) { - log.debug("[이첩] 사용자 정보 없음. 사용자ID: {}", userId); - return false; - } - - // 3. 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 비교 - String legalDong4 = useStrnghldLegaldongCode.substring(0, 4); - String userOrgCd = userInfo.getOrgCd(); - String userOrg4 = userOrgCd.length() >= 4 - ? userOrgCd.substring(0, 4) - : userOrgCd; - - // 4. 일치 여부 판단 - if (legalDong4.equals(userOrg4)) { - log.debug("[이첩] 법정동코드 일치. 차량번호: {}, 법정동: {}, 조직: {}", - vhclno, legalDong4, userOrg4); - return false; // 일치하면 이첩 대상 아님 - } - - log.info("[이첩] 법정동코드 불일치! 차량번호: {}, 법정동: {}, 조직: {}", - vhclno, legalDong4, userOrg4); - return true; // 불일치하면 이첩 대상 +if (!legalDong4.equals(userOrg4)) { + return TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER; // 이첩 } ``` @@ -283,26 +356,6 @@ private boolean checkTransferCondition_LegalDongMismatch( --- -## 데이터 모델 - -### TB_CAR_FFNLG_TRGT (과태료 대상 테이블) - -| 컬럼명 | 설명 | 용도 | -|--------|------|------| -| 검사일 | 검사 기준일 | API 호출 파라미터 | -| 검사종료일자 | 검사 종료 일자 | 115일 계산 기준 | -| 유효기간만료일 | 유효기간 만료일 | 상품용 갑부 비교 시작일 | -| DAYCNT | textFile 일수 | 이첩-1/2 분기 조건 | -| 비고 | 검증 결과 메시지 | 결과 저장 | - -### 코드 정의 - -| 코드 | 코드값 | 설명 | -|------|--------|------| -| CHANGE_JOB_SE_CODE | 11 | 명의이전 | - ---- - ## 처리 흐름도 ``` @@ -312,166 +365,80 @@ private boolean checkTransferCondition_LegalDongMismatch( [차량번호 조회] │ ▼ -┌────────────────────────────────────────────────────────────┐ -│ 1. 상품용 Case1 - 검사일 소유자가 상품용 │ -├────────────────────────────────────────────────────────────┤ -│ Step 1: 자동차기본정보(차량번호, 검사일) │ -│ → 소유자명에 "상품용" 포함? │ -│ │ │ -│ ├─ (No) ──────────────────────────────────┐ │ -│ │ │ │ -│ └─ (Yes) │ │ -│ ▼ │ │ -│ Step 2: 자동차기본정보(차대번호, 오늘일자) │ │ -│ → 현재 소유자 정보 │ │ -│ ▼ │ │ -│ Step 3: 자동차등록원부(갑) 조회 │ │ -│ → 갑부 상세 List │ │ -│ ▼ │ │ -│ 검사종료일자 이전 가장 마지막 명의이전(11) │ │ -│ 레코드 찾기 (CHG_YMD <= 검사종료일자) │ │ -│ │ │ │ -│ ├─ (없음) ─────────────────────────────┐ │ │ -│ │ │ │ │ -│ └─ (발견) │ │ │ -│ ▼ │ │ │ -│ Step 4: 자동차기본정보(차대번호, CHG_YMD) │ │ │ -│ → 명의이전 시점 소유자명 │ │ │ -│ │ │ │ │ -│ ▼ │ │ │ -│ 소유자명 일치? │ │ │ -│ │ │ │ │ -│ ├─ (No) ─────────────────────────┐ │ │ │ -│ │ │ │ │ │ -│ └─ (Yes) │ │ │ │ -│ ▼ │ │ │ │ -│ [상품용(02) 처리 완료] │ │ │ │ -│ │ │ │ │ │ -│ └──> [다음 차량] <────────────────┴─────┴───┘ │ -└────────────────────────────────────────────────────────────┘ - │ +┌─────────────────────────────────────┐ +│ 1. 상품용 (검사일 소유자가 상품용) │ +│ → 조건 충족: 상품용(02) │ +└─────────────────────────────────────┘ │ (조건 미충족) ▼ -┌────────────────────────────────────────────────────────────┐ -│ 2. 내사종결 Case2 - 명의이전 후 상품용 │ -├────────────────────────────────────────────────────────────┤ -│ Step 1: 자동차기본정보(차량번호, 검사일) │ -│ → 소유자명에 "상품용" 미포함? │ -│ │ │ -│ ├─ (No) ──────────────────────────────────┐ │ -│ │ │ │ -│ └─ (Yes) │ │ -│ ▼ │ │ -│ Step 2: 자동차기본정보(차대번호, 오늘일자) │ │ -│ → 현재 소유자 정보 │ │ -│ ▼ │ │ -│ Step 3: 자동차등록원부(갑) 조회 │ │ -│ → 갑부 상세 List │ │ -│ ▼ │ │ -│ 검사일 이전 가장 마지막 명의이전(11) │ │ -│ 레코드 찾기 (CHG_YMD <= 검사일) │ │ -│ │ │ │ -│ ├─ (없음) ─────────────────────────────┐ │ │ -│ │ │ │ │ -│ └─ (발견) │ │ │ -│ ▼ │ │ │ -│ 명의이전일자가 검사일의 30일 이내? │ │ │ -│ │ │ │ │ -│ ├─ (No) ─────────────────────────┐ │ │ │ -│ │ │ │ │ │ -│ └─ (Yes) │ │ │ │ -│ ▼ │ │ │ │ -│ Step 4: 자동차기본정보(차대번호, CHG_YMD-1일) │ │ │ │ -│ → 명의이전 직전 소유자명 │ │ │ │ -│ │ │ │ │ │ -│ ▼ │ │ │ │ -│ 소유자명에 "상품용" 포함? │ │ │ │ -│ │ │ │ │ │ -│ ├─ (No) ───────────────────┐ │ │ │ │ -│ │ │ │ │ │ │ -│ └─ (Yes) │ │ │ │ │ -│ ▼ │ │ │ │ │ -│ [내사종결(04) 처리 완료] │ │ │ │ │ -│ │ │ │ │ │ │ -│ └──> [다음 차량] <──────────┴─────┴─────┴───┘ │ -└────────────────────────────────────────────────────────────┘ - │ +┌─────────────────────────────────────┐ +│ 2. 내사종결 - 명의이전 전 상품용 │ +│ (31일 이내) │ +│ → 조건 충족: 내사종결(04) │ +└─────────────────────────────────────┘ + │ (조건 미충족) + ▼ +┌─────────────────────────────────────┐ +│ 3. 내사종결 - 순수 명의이전 │ +│ (31일 이내, 상품용 아님) │ +│ → 조건 충족: 내사종결(04) │ +└─────────────────────────────────────┘ │ (조건 미충족) ▼ -┌────────────────────────────────────────────────────────────┐ -│ 3. 이첩 검증 (이첩-1, 이첩-2) │ -├────────────────────────────────────────────────────────────┤ -│ DAYCNT 확인 │ -│ │ │ -│ ├─ (> 115) ──> [이첩-2: 부과기준일 = 검사종료일+115일] │ -│ │ │ -│ └─ (<= 115) ─> [이첩-1: 부과기준일 = 검사일] │ -│ │ │ -│ ▼ │ -│ [자동차기본정보 API 호출] │ -│ │ │ -│ ▼ │ -│ [법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리] │ -│ │ │ -│ ├─ (불일치) ──> [이첩(03) 처리 완료] ──> [다음 차량] │ -│ │ │ -│ └─ (일치) ────> [조건 미충족] ──> [다음 차량] │ -└────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────┐ +│ 4. 날짜수정 - 명의이전 전 상품용 │ +│ (31일 초과) │ +│ → 조건 충족: 날짜수정후부과(05) │ +└─────────────────────────────────────┘ + │ (조건 미충족) + ▼ +┌─────────────────────────────────────┐ +│ 5. 날짜수정 - 순수 명의이전 │ +│ (31일 초과, 상품용 아님) │ +│ → 조건 충족: 날짜수정후부과(05) │ +└─────────────────────────────────────┘ + │ (조건 미충족) + ▼ +┌─────────────────────────────────────┐ +│ 6. 이첩 (법정동코드 불일치) │ +│ → 조건 충족: 이첩(03) │ +└─────────────────────────────────────┘ + │ (조건 미충족) + ▼ +[다음 차량] ``` --- -## 구현 시 주의사항 - -### 공통 주의사항 -1. **API 호출 순서 준수**: 각 검증 단계별로 필요한 API만 호출 -2. **조건 우선순위**: 상품용 Case1 > 내사종결 Case2 > 이첩 순서로 검증 -3. **조기 종료**: 조건 충족 시 즉시 다음 차량으로 이동 -4. **비고 컬럼**: 각 조건별 정해진 형식으로 기록 -5. **날짜 형식 지원**: yyyyMMdd, yyyy-MM-dd 두 형식 모두 자동 처리 (`DateUtil` 사용) - -### 상품용 Case1 주의사항 -1. **4단계 API 호출 필수**: Step 1~4 모두 순차적으로 실행 -2. **가장 마지막 명의이전 레코드**: 검사종료일자 이전일자 중 CHG_YMD가 가장 큰 값 선택 -3. **소유자명 정확한 일치**: Step1 소유자명 == Step4 소유자명 (완전 일치) -4. **주석처리된 조건**: - - ~~검사일 이전일자 제한~~ → 검사종료일자 이전일자만 제한 - - ~~기간 범위 확인~~ → 제거됨 -5. **로그 상세 기록**: 각 단계별 결과를 상세히 로깅하여 디버깅 지원 - -### 내사종결 Case2 주의사항 -1. **검사일 소유자 확인**: 검사일 소유자가 상품용이 아니어야 함 (Case1과 구분) -2. **30일 이내 조건**: 명의이전일자가 검사일의 30일 이내여야 함 -3. **CHG_YMD-1일 조회**: Step 4에서 명의이전 직전 시점(CHG_YMD - 1일) 조회 -4. **검사일 기준**: 갑부 레코드는 검사일 이전일자 중 가장 마지막 일자 선택 -5. **비고 형식**: 명의이전 날짜, 차량번호, 검사기간 등 상세 정보 포함 - -### 이첩 검증 주의사항 -1. **법정동코드 길이 검증**: 최소 4자리 이상 필요 -2. **DAYCNT 기준 명확**: > 115 (초과), <= 115 (이하) 구분 -3. **사용자 조직코드 유효성**: null 체크 및 길이 확인 필수 - - - - ## 요약 정리 -### 비교 로직 실행 순서 (executeComparison) +### 비교 로직 실행 순서 ```java -// ComparisonServiceImpl.java Line 50-85 public String executeComparison(CarFfnlgTrgtVO existingData) { - // 1. 상품용 Case1 - 검사일 소유자가 상품용 - String result1 = checkProductUseCase1(existingData); + // 1. 상품용 + String result1 = checkProductUse(existingData); if (result1 != null) return result1; // "02" - // 2. 내사종결 Case2 - 명의이전 후 상품용 - String result2 = checkCloseProductUse(existingData); + // 2. 내사종결 - 명의이전 전 소유자 상품용 (31일 이내) + String result2 = checkInvestigationClosedByProductUseWithin31Days(existingData); if (result2 != null) return result2; // "04" - // 3. 이첩 검증 - String result3 = checkTransferCase115Day(existingData); - if (result3 != null) return result3; // "03" + // 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; @@ -482,41 +449,67 @@ public String executeComparison(CarFfnlgTrgtVO existingData) { | 코드 | 상태명 | 처리 로직 | 메서드 | |------|--------|----------|--------| -| 02 | 상품용 | 검사일 소유자가 상품용 | `checkProductUseCase1()` | -| 04 | 내사종결 | 명의이전(30일 이내) 후 상품용 | `checkCloseProductUse()` | +| 02 | 상품용 | 검사일 소유자가 상품용 | `checkProductUse()` | +| 04 | 내사종결 | 명의이전 전 상품용 (31일 이내) | `checkInvestigationClosedByProductUseWithin31Days()` | +| 04 | 내사종결 | 순수 명의이전 (31일 이내) | `checkInvestigationClosedByOwnerChangeWithin31Days()` | +| 05 | 날짜 수정 후 부과 | 명의이전 전 상품용 (31일 초과) | `checkDateModifiedLevyByProductUseOver31Days()` | +| 05 | 날짜 수정 후 부과 | 순수 명의이전 (31일 초과) | `checkDateModifiedLevyByOwnerChangeOver31Days()` | | 03 | 이첩 | 법정동코드 불일치 | `checkTransferCase115Day()` | -### 주요 차이점 정리 +### 상품용 vs 순수 명의이전 비교 -| 구분 | Case1 (상품용) | Case2 (내사종결) | -|------|---------------|-----------------| -| 검사일 소유자 | 상품용 포함 | 상품용 미포함 | -| 갑부 레코드 기준 | CHG_YMD <= 검사종료일자 | CHG_YMD <= 검사일 | -| 추가 조건 | - | 명의이전일자가 검사일의 30일 이내 | -| Step 4 조회일 | CHG_YMD | CHG_YMD - 1일 | -| Step 4 확인사항 | 소유자명 일치 | 소유자명에 "상품용" 포함 | -| 처리상태코드 | 02 | 04 | +| 구분 | 상품용 케이스 (2, 4번) | 순수 명의이전 케이스 (3, 5번) | +|------|----------------------|----------------------------| +| 검사일 소유자 | 상품용 아님 | 상품용 아님 | +| 명의이전 직전 소유자 | 상품용 **포함** | 상품용 **아님** | +| Step 4 확인사항 | 상품용 포함 여부 | 상품용 미포함 여부 (상품용이면 이미 2, 4번에서 처리) | +| 일수 기준 | 31일 이내/초과 | 31일 이내/초과 | +| 처리상태코드 | 31일 이내: 04, 초과: 05 | 31일 이내: 04, 초과: 05 | --- ## 구현 완료 상태 -### ✅ 상품용 Case1 로직 -- **구현 완료**: 신규 로직으로 구현 완료 -- **구현 위치**: `ComparisonServiceImpl.checkProductUseCase1()` (Line 113-306) -- **주요 변경사항**: - - 검사일 이전일자 조건 주석처리 → 검사종료일자 이전일자만 제한 - - 기간 범위 확인 조건 주석처리 → 제거됨 - - 비고 내용 간소화 - -### ✅ 내사종결 Case2 로직 (신규 추가) -- **구현 완료**: 신규 로직 추가 완료 -- **구현 위치**: `ComparisonServiceImpl.checkCloseProductUse()` (Line 329-515) -- **주요 특징**: - - 명의이전(30일 이내) 후 상품용인 경우 처리 - - CHG_YMD - 1일 기준 조회 - - 상세한 비고 정보 생성 - -### ✅ 이첩 로직 -- **구현 완료**: 이첩-1, 이첩-2 병합 로직 구현 완료 -- **구현 위치**: `ComparisonServiceImpl.checkTransferCase115Day()` (Line 531-648) \ No newline at end of file +### ✅ 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에 저장 diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/ComparisonServiceImpl.java b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/ComparisonServiceImpl.java index 0bb632d..dbe90fa 100644 --- a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/ComparisonServiceImpl.java +++ b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/ComparisonServiceImpl.java @@ -537,20 +537,23 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co } /** - * 3. 내사종결 검증 - 명의이전, 31일 이내 + * 3. 내사종결 검증 - 순수 명의이전 (31일 이내) * - * API 호출 3단계: + * API 호출 4단계: * 1) 자동차기본정보(차량번호, 부과일자=검사일) → 차대번호, 소유자명 * 2) 자동차기본정보(차대번호, 부과일자=오늘일자) → 차량번호, 성명, 민원인주민번호, 민원인법정동코드 * 3) 자동차등록원부(갑)(차량번호, 성명, 민원인주민번호, 민원인법정동코드) → 갑부 상세 List + * 4) 자동차기본정보(차대번호, 부과일자=CHG_YMD - 1일) → 명의이전 직전 소유자명 * * 비교조건: - * - 1단계: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(코드 11) 레코드 찾기 + * - 1단계: 검사일 소유자명에 "상품용" 미포함 (상품용은 1번에서 처리) + * - 2단계: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(코드 11) 레코드 찾기 * • 유효기간만료일 <= CHG_YMD <= 검사종료일자 * • CHG_TASK_SE_CD == "11" (명의이전) - * - 2단계: 명의이전일자와 검사일 비교 + * - 3단계: 명의이전일자와 검사일 비교 * • 명의이전일자 ~ 검사일까지의 일수 계산 * • 31일 이내인 경우 + * - 4단계: 명의이전 직전 소유자가 상품용 아님 (상품용이면 2번에서 처리됨) * * @param existingData 과태료 대상 데이터 * @return 04 (내사종결) 또는 null (미적용) @@ -580,13 +583,13 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co log.info("[내사종결-명의이전] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); - // 조건: 소유자명에 "상품용" 미포함 여부 확인 (명의이전 케이스는 상품용이 아니어야 함) + // 검사일 소유자가 상품용 아님 if (step1OwnerName != null && step1OwnerName.contains("상품용")) { - log.debug("[내사종결-명의이전] 소유자명에 '상품용' 포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + log.debug("[내사종결-명의이전] 검사일 소유자가 상품용 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); return null; } - log.info("[내사종결-명의이전] 소유자명에 '상품용' 미포함 확인 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + log.info("[내사종결-명의이전] 검사일 소유자가 상품용 아님 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== String today = LocalDate.now().format(DATE_FORMATTER); @@ -700,13 +703,13 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co log.info("[내사종결-명의이전] Step 4 결과 - 소유자명: {}", step4OwnerName); - // ========== 소유자명 비교 - 명의이전 전 소유자가 상품용이어야 함 ========== - if (step4OwnerName == null || !step4OwnerName.contains("상품용")) { - log.debug("[내사종결-명의이전] 명의이전 전 소유자명에 '상품용' 미포함 - Step4 소유자명: {}", step4OwnerName); + // 명의이전 전 소유자가 상품용 아님 + if (step4OwnerName == null || step4OwnerName.contains("상품용")) { + log.debug("[내사종결-명의이전] 명의이전 전 소유자가 상품용 - Step4 소유자명: {}", step4OwnerName); return null; } - log.info("[내사종결-명의이전] 명의이전 - 소유자명: {}", step4OwnerName); + log.info("[내사종결-명의이전] 명의이전 전 소유자가 상품용 아님 - 소유자명: {}", step4OwnerName); // ========== 비고 생성 ========== String rmrk = ComparisonRemarkBuilder.buildOwnerChangeRemark( @@ -951,20 +954,23 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co } /** - * 5. 날짜 수정 후 부과 검증 - 명의이전, 31일 초과 + * 5. 날짜 수정 후 부과 검증 - 순수 명의이전 (31일 초과) * - * API 호출 3단계: + * API 호출 4단계: * 1) 자동차기본정보(차량번호, 부과일자=검사일) → 차대번호, 소유자명 * 2) 자동차기본정보(차대번호, 부과일자=오늘일자) → 차량번호, 성명, 민원인주민번호, 민원인법정동코드 * 3) 자동차등록원부(갑)(차량번호, 성명, 민원인주민번호, 민원인법정동코드) → 갑부 상세 List + * 4) 자동차기본정보(차대번호, 부과일자=CHG_YMD - 1일) → 명의이전 직전 소유자명 * * 비교조건: - * - 1단계: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(코드 11) 레코드 찾기 + * - 1단계: 검사일 소유자명에 "상품용" 미포함 (상품용은 1번에서 처리) + * - 2단계: 갑부 상세에서 검사기간(유효기간만료일 ~ 검사종료일자) 내 명의이전(코드 11) 레코드 찾기 * • 유효기간만료일 <= CHG_YMD <= 검사종료일자 * • CHG_TASK_SE_CD == "11" (명의이전) - * - 2단계: 명의이전일자와 검사일 비교 + * - 3단계: 명의이전일자와 검사일 비교 * • 명의이전일자 ~ 검사일까지의 일수 계산 * • 31일 초과인 경우 + * - 4단계: 명의이전 직전 소유자가 상품용 아님 (상품용이면 4번에서 처리됨) * * @param existingData 과태료 대상 데이터 * @return 05 (날짜 수정 후 부과) 또는 null (미적용) @@ -994,13 +1000,13 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co log.info("[날짜수정후부과-명의이전] Step 1 결과 - 차대번호: {}, 소유자명: {}", vin, step1OwnerName); - // 조건: 소유자명에 "상품용" 미포함 여부 확인 (명의이전 케이스는 상품용이 아니어야 함) + // 검사일 소유자가 상품용 아님 if (step1OwnerName != null && step1OwnerName.contains("상품용")) { - log.debug("[날짜수정후부과-명의이전] 소유자명에 '상품용' 포함 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + log.debug("[날짜수정후부과-명의이전] 검사일 소유자가 상품용 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); return null; } - log.info("[날짜수정후부과-명의이전] 소유자명에 '상품용' 미포함 확인 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); + log.info("[날짜수정후부과-명의이전] 검사일 소유자가 상품용 아님 - 차량번호: {}, 소유자명: {}", vhclno, step1OwnerName); // ========== Step 2: 자동차기본정보 조회 (차대번호, 부과일자=오늘일자) ========== String today = LocalDate.now().format(DATE_FORMATTER); @@ -1114,13 +1120,13 @@ public class ComparisonServiceImpl extends EgovAbstractServiceImpl implements Co log.info("[날짜수정후부과-명의이전] Step 4 결과 - 소유자명: {}", step4OwnerName); - // ========== 소유자명 비교 - 명의이전 전 소유자가 상품용이어야 함 ========== + // 명의이전 전 소유자가 상품용 아님 if (step4OwnerName == null || step4OwnerName.contains("상품용")) { - log.debug("[날짜수정후부과-명의이전] 명의이전 전 소유자명에 '상품용' 포함 - Step4 소유자명: {}", step4OwnerName); + log.debug("[날짜수정후부과-명의이전] 명의이전 전 소유자가 상품용 - Step4 소유자명: {}", step4OwnerName); return null; } - log.info("[날짜수정후부과-명의이전] 명의이전 - 소유자명: {}", step4OwnerName); + log.info("[날짜수정후부과-명의이전] 명의이전 전 소유자가 상품용 아님 - 소유자명: {}", step4OwnerName); // ========== 비고 생성 ========== String rmrk = ComparisonRemarkBuilder.buildOwnerChangeRemark( diff --git a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp index 3215652..b8eb2a0 100644 --- a/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp +++ b/src/main/webapp/WEB-INF/views/carInspectionPenalty/registration/list.jsp @@ -339,6 +339,11 @@ } }, { header: '매매상품', name: 'trdGds', align: 'center', width: 100 }, + { header: '처리일자', name: 'taskPrcsYmd', align: 'center', width: 100, + formatter: function(e) { + return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : ''; + } + }, { header: '처리상태', name: 'taskPrcsSttsCd', @@ -365,11 +370,6 @@ return code ? code.nm : value; } }, - { header: '처리일자', name: 'taskPrcsYmd', align: 'center', width: 100, - formatter: function(e) { - return e.value ? moment(e.value, 'YYYYMMDD').format('YYYY-MM-DD') : ''; - } - }, { header: '비고', name: 'rmrk',