Compare commits

...

131 Commits

Author SHA1 Message Date
박성영 bc0d2e4128 ### feat: MariaDB 데이터베이스 백업 문서 추가
- **백업 가이드 추가**
  - `docs/용인 장애인 서버에서 관리 방법.md`에 **MariaDB 데이터베이스 백업** 섹션(`6.`) 추가:
    - 백업 디렉토리 생성 방법 명시.
    - 백업 스크립트 작성 및 권한 부여 절차 설명.
    - Crontab 설정을 통한 자동화 백업 방법(매일 밤 12시) 추가.

- **백업 스크립트 상세**
  - 데이터베이스 백업 파일 생성 및 압축, 30일 지난 백업 파일 삭제를 포함한 스크립트 제공.
  - 성공 및 실패 로그 기록 기능 포함.

- **관리 명령어 제공**
  - 수동 백업 수행, 백업 파일 확인, 로그 조회, 복원 명령어 등 추가 명세.

- **기타**
  - 백업 스크립트 실행 및 관리 명령어 내용을 문서에 명확하게 기록.
  - MariaDB 관리와 백업 프로세스를 쉽게 이해할 수 있도록 섹션 및 내용 구성.
1 day ago
박성영 93b15f3db0 ### feat: `Step 0` 및 `Step 1` 로직 공통화 및 비교 체커 로직 개선
- **`Step 0` 및 `Step 1` 공통 로직 추가**
  - `ComparisonOmServiceImpl`에서 `Step 0` 및 `Step 1` API 호출을 공통화:
    - `step0Response`와 `step1Response` 객체 생성 및 유효성 검증 추가.
    - 오류 발생 시 적절한 `log.warn` 및 `log.error` 출력.
    - 모든 검사 로직에서 공통 응답 데이터를 재사용하도록 수정.

- **체커(`Checker`) 클래스의 메서드 시그니처 수정**
  - `check` 메서드가 공통화된 응답 데이터(`step0Response`, `step1Response`)를 전달받도록 변경:
    - 기존 `check(CarFfnlgTrgtIncmpVO existingData, String userOrgCd)` →
      `check(CarFfnlgTrgtIncmpVO existingData, String userOrgCd, NewBasicResponse step0Response, NewBasicResponse step1Response)`.
    - `ProductUseOmChecker`, `OwnerTransferOmChecker`, `TransferOmChecker` 등 모든 비교 체커 반영.
    - 메서드 내 중복된 API 호출 제거 및 기존 응답 데이터 활용.

- **API 호출 로직 개선**
  - `createBasicRequest` 메서드 추가:
    - `vhrno`, `vin`, `levyCrtrYmd` 기반의 API 요청 객체를 생성하는 로직 캡슐화.
    - 호출부 간소화 및 재사용성 향상.

- **코드 및 처리 흐름 정리**
  - 기존 각 체커 내부에서 수행되던 API 호출 제거:
    - `Step 0` 및 `Step 1`은 메인 서비스에서 호출 후 응답 전달 방식으로 수정.
    - 주석 보완 및 로잉 일관성 개선.

- **불필요한 호출 및 데이터 처리 제거**
  - 중복된 API 호출 및 데이터 검증 로직 제거:
    - 기존 `step0Response`, `step1Response` 응답을 재활용하여 불필요한 호출 제거.
    - 검사 결과 처리 흐름을 단계별로 정리하여 가독성 강화.

- **기타**
  - 추가된 API 호출 및 유효성 검증으로 로직 안정성 강화.
  - 메인 서비스와 각 비교 로직 간 호출 체계를 명확히 구분.
1 day ago
박성영 e113a20b23 ### feat: `Step 1` 로직 공통화 및 `delay_checker` 메서드 시그니처 수정
- **`Step 1` 공통 로직 도입**
  - `ComparisonServiceImpl`에서 차대번호 기반 API 호출(`Step 1`)을 공통화:
    - `step1Response` 생성 및 응답 데이터 유효성 검증 추가.
    - 오류 처리 및 로깅(`log.warn`, `log.error`) 강화.
    - 응답 객체를 각 검사 로직에서 재사용하도록 수정.

- **`delay_checker` 메서드 시그니처 업데이트**
  - 기존 `check(CarFfnlgTrgtVO existingData, String userOrgCd, NewBasicResponse step0Response)` →
    `check(CarFfnlgTrgtVO existingData, String userOrgCd, NewBasicResponse step0Response, NewBasicResponse step1Response)`로 변경:
    - 모든 `delay_checker`(`OwnerCloseWithin31Checker`, `ProductUseChecker`, `TransferCase115DayChecker` 등)에 반영.
    - 공통 API 호출(`Step 1`) 결과를 재활용하여 불필요한 중복 호출 제거.
    - `createBasicRequest`, `bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx` 처리 로직 정리.

- **로직 순서 정리 및 가독성 개선**
  - 처리 흐름 단순화:
    - 검사 단계 순서를 공통 체크 → 내사종결 → 날짜 수정 후 부과로 정리.
    - 각 체커 로직에서 호출 단계별 주석 추가.

- **`ComparisonOmServiceImpl` 로직 재정렬**
  - 명의이전 로직(`ownerTransferOmChecker`) 순서를 뒤로 이동:
    - 기존 이첩 로직 다음 단계로 조정하여 처리 가독성 강화.

- **불필요한 호출 제거**
  - `delay_checker` 내 중복된 `Step 1` 호출 제거:
    - 공통화된 `step1Response` 객체를 활용하도록 모든 로직 간소화.

- **기타**
  - 주석 및 로깅 일관성 개선:
    - 호출 단계별 로그 추가 및 기존 주석 정리.
    - `step1Response` 응답 유효성 검증 및 오류 시 로깅 처리 강화.
4 days ago
박성영 eb4a466a3c ### feat: `Step 0` 공통 로직 추가 및 delay_checker 업데이트
- **`Step 0` 공통 로직 도입**
  - `ComparisonServiceImpl`에 차량번호 기반 API 호출(`Step 0`) 공통화:
    - API 호출하여 `step0Response` 객체 생성.
    - 결과 데이터 유효성 검증 후 체크 로직에서 재사용.
    - 필요 시 null 반환 및 적절한 로그 출력(`log.warn` / `log.error`).

- **`delay_checker` 클래스 메서드 시그니처 수정**
  - 기존 `check(CarFfnlgTrgtVO existingData, String userOrgCd)` →
    `check(CarFfnlgTrgtVO existingData, String userOrgCd, NewBasicResponse step0Response)`로 변경:
    - `OwnerCloseWithin31Checker`, `ProductUseChecker`, `TransferCase115DayChecker` 등 전범위 반영.
    - `step0Response`를 활용하여 기존 중복 API 호출 제거.
    - 불필요한 `Step 0` 호출 및 데이터 매핑 로직 제거.

- **로직 개선 및 코드 정리**
  - 중복 호출 제거 및 API 응답 데이터 재사용을 위해 호출 순서 정리:
    - 공통 체크(상품용/명의이전 등) → 내사종결 → 날짜 수정 및 부과 순서로 간소화.
    - 각 단계별 로직 정리 및 부적절한 중복 호출 제거.

- **기타**
  - 불필요한 주석 제거 및 메서드 내 주석 추가로 가독성 강화.
  - `createBasicRequest` 메서드 추가로 API 호출 로직 캡슐화.
4 days ago
박성영 bccccf3966 ### feat: 명의이전 날짜 비교와 비고 생성 로직 개선
- **명의이전 날짜 비교 조건 추가**
  - `ProductLevyOver31Checker`, `ProductCloseWithin31Checker` 로직 수정:
    - `유효기간만료일 - 90일 <= 변경일자 <= 검사종료일자` 조건으로 비교 강화.
    - 기존 검사 종료일자와의 비교 조건을 대체.
    - 날짜 비교 로직 내 `vldPrdExpryDate`, `vldPrdExpryDateMinus90`, `inspEndDate` 변수를 추가하여 명확한 기준 설정.

- **비고 상세 생성 개선**
  - `ComparisonRemarkBuilder` 로직 확장:
    - `유효기간만료일`, `검사종료일자` 등을 비고 상세 정보로 추가.
    - 검색 조건, 날짜
4 days ago
박성영 045c6ec816 ### feat: 차량번호 대신 차대번호 활용 및 Step 0 로직 추가
- **차량번호 대신 차대번호 활용**
  - `OwnerCloseWithin31Checker`, `ProductLevyOver31Checker`, `ProductUseChecker` 등 여러 `delay_checker` 및 `om_checker` 클래스:
    - Step 0 단계에서 API 호출을 통해 차대번호(`vin`)를 조회하도록 수정.
    - Step 1 단계 및 이후 요청에서는 차량번호 대신 차대번호를 사용.

- **Step 0 로직 추가**
  - 명의이전 및 상품용 지연 케이스 모두 Step 0 단계 신설:
    - API 호출로 차대번호 조회.
    - 응답 데이터 유효성 검증 추가:
      - 응답 데이터가 없거나 비정상이면 null 반환.
    - 로깅(`log.info` 및 `log.warn`) 강화:
      - 오류나 응답 없음 등 상태에 따라 적절히 로그 출력.
    - `bassMatterLogService.updateCarFfnlgTrgtIdByTxIdNewTx` 호출로 로그 데이터 초기화.

- **JSP 검색 조건 로직 수정**
  - 검색 조건에서 날짜 포맷 관련 `.replace("-", "")` 제거:
    - 원본 값을 그대로 활용.
    - `searchCond.schRcptYmdStart`, `searchCond.schRcptYmdEnd` 등 검색 타임라인 유지.

- **서비스 로직 유지보수**
  - `inspYmd` 또는 `levyCrtrYmd`와 같은 기존 요청 포맷에 따라 Step 0의 차대번호를 전달하도록 기존 메서드 호출 방식 수정.
  - 불필요한 중복 호출 및 데이터 매핑 로직 제거.

- **기타**
  - 공통화된 API 호출 및 처리 로직 반영.
  - 관련 로직 가독성 강화를 위한 주석 및 변수명 수정.
4 days ago
박성영 11456d7340 ### refactor: 날짜 형식 처리 로직 및 데이터 저장 변경
- **`OwnerTransferOmChecker` 및 `ProductUseOmChecker` 클래스 수정**
  - 날짜 데이터에서 불필요한 `.replace("-", "")` 호출 제거 및 기존 포맷 그대로 사용하도록 변경.
  - 날짜 형식을 처리하기 위해 `inspVldPrd`, `levyCrtrYmd` 등 필드에 처리 로직 조정.
  - `DATE_FORMATTER` → `DATE_FORMATTER_DASH` 상수로 변경하여 일관성 확보.

- **데이터 저장 로직 수정**
  - `taskPrcsYmd`, `carRegFrmbkChgYmd` 데이터에 기존 포맷 유지:
    - `LocalDate.now().format(DATE_FORMATTER_DASH)` 호출로 저장 포맷 명확화.
    - 기존 `.replace("-", "")` 변환 로직 제거하여 데이터 원본 그대로 유지.

- **유지보수성 향상**
  - 코드 중복 제거 및 주석 추가로 가독성 및 유지보수성 강화.
  - 기존 메서드에서 불필요한 호출 로직 제거 및 필요한 부분만 변수 재활용.

- **기타**
  - 기존 데이터 처리 로직과 연계, 호환성 확인.
4 days ago
박성영 33fcb754fc ### feat: 날짜 형식 개선 및 불필요한 페이징 제거
- **날짜 형식 포맷 통일**
  - 기존 `yyyyMMdd` → `yyyy-MM-dd` 포맷 변경.
  - `AbstractComparisonChecker` 및 `AbstractComparisonOmChecker`에 `DATE_FORMATTER_DASH` 상수 추가.
  - 모든 `delay_checker` 및 `om_checker` 클래스에서 `DATE_FORMATTER_DASH` 적용.

- **JSP 초기값 수정**
  - `schRcptYmdStart` 검색 기간 초기값 조정:
    - 기존 `-15일` → `-10일`로 변경.
  - 초기화 버튼 클릭 시 변경된 초기값 반영.

- **페이징 UI 및 로직 제거**
  - 불필요한 페이징 관련 코드 삭제:
    - JSP에서 `perPageSelect`, `currentPage`, `totalPages` 삭제.
    - JS에서 관련 전역 변수(`GRID_PAGINATION_INFO`) 및 `setOptPageOptions` 코드 제거.
    - `readData` 메서드 호출 시 항상 첫 페이지(`1`) 로드하도록 변경.
  - 그리드에서 일련번호 계산 단순화:
    - 기존 전체 페이지 기반 → 현재 페이지의 키 값만 사용.

- **DB 변경**
  - `tb_car_ffnlg_trgt_incmp` 스키마에서 날짜 필드 길이 확장:
    - `RCPT_YMD`, `TASK_PRCS_YMD`, `CAR_REG_FRMBK_CHG_YMD` 등 날짜 관련 필드 길이 `8 → 10`.

- **기타**
  - JSP 및 JS에서 사용하지 않는 포맷팅 코드 제거:
    - `moment()` 호출 비활성화 및 주석화.
4 days ago
박성영 7051953c82 ### feat: 재검 여부 필터 추가 및 그리드 개선
- **재검 여부 필터 추가**
  - `검색 조건`에 재검 여부(`재검(Y)`, `미재검(N)`) 선택 기능 추가.
  - 재검 여부에 따른 데이터 필터링 로직 구현:
    - JSP: 검색 영역에 재검 여부 체크박스 추가.
    - JS: 검색 조건(`schReinspYn`)에 재검 여부 값 추가 및 다운로드 URL에 포함되도록 설정.
    - MyBatis: 쿼리에 재검 여부 필터 추가.

- **그리드 컬럼 및 데이터 처리 개선**
  - 결과 그리드에 `재검 여부` 열 추가:
    - JSP 및 그리드 설정에 관련 컬럼 반영.
    - MyBatis 또는 데이터 처리 단계에서 `재검 여부` 값 처리 및 가공 로직 구현.
  - 일련번호 계산 로직 단순화:
    - 기존 페이징 정보 활용 대신 로우 키만 활용하도록 변경.

- **서비스 및 내부 로직 변경**
  - 데이터 처리 단계에서 `재검 여부` 설정:
    - `ServiceImpl`: 일수(`daycnt`) 값에 `*` 포함 여부로 재검 여부 결정 후 저장.
  - VO 및 Mapper 수정:
    - `CarFfnlgTrgtVO`, `CarFfnlgTrgtExcelVO`: 재검 여부(`reinspYn`) 필드 추가.
    - DB: `tb_car_ffnlg_trgt` 테이블에 `REINSP_YN` 칼럼 추가, 마이그레이션 SQL 작성.

- **불필요한 페이징 관련 코드 제거**
  - 조회 페이징용 전역 변수 및 관련 UI 제거:
    - `perPageSelect`, `currentPage`, `totalPages` 등 페이징 UI 요소 삭제.
  - JS 및 컨트롤러에서 페이징 관련 전역 로직 제거 및 단순화.

- **기타 개선**
  - 검색 초기화 버튼에 재검 여부 관련 로직 추가.
  - 검색 조건 정리(`SEARCH_COND`) 및 관련 주석 추가.
  - DEBUG 로그 추가: `재검 여부` 설정 및 출력 내역 기록.
4 days ago
박성영 9a67cf6e14 ### feat: 날짜 형식 처리 로직 수정 및 불필요한 replace 제거
- **날짜 형식 처리 개선**
  - `inspYmd`, `vldPrdExpryYmd`, `inspEndYmd` 데이터에 대해 `.replace("-", "")` 호출로 날짜 형식을 변환하도록 수정.
  - 모든 `delay_checker` 관련 클래스(`OwnerCloseWithin31Checker`, `ProductLevyOver31Checker` 등)에서 동일한 `.replace` 로직 추가하여 입력 데이터의 통일성을 보장.

- **불필요한 replace 호출 제거**
  - `CarRegFrmbkChgYmd` 필드에서 `.replace("-", "")` 호출을 제거하여 데이터 원본 그대로 사용.

- **서비스 로직 정리**
  - `ComparisonServiceImpl`:
    - 날짜 검증 순서 중 재사용성이 높은 `transferCase115DayChecker` 로직 순서를 앞쪽으로 이동.
    - 기존 순서를 재정렬하여 처리 흐름을 명확하게 개선:
      - 이첩 로직 → 내사종결 → 날짜 수정 후 부과 순서로 정리.
    - 처리 단계별 주석 추가로 가독성 개선.

- **유지보수성 강화**
  - `ComparisonServiceImpl` 및 관련 `Checker` 간 호출 순서 명확히 정리.
  - 중복 코드를 완화하고 관련 주석 및 로깅 추가.
4 days ago
박성영 06f6a03178 ### feat: 차량번호 및 최종등록일 별표 처리 로직 추가
- **차량번호 및 최종등록일 별표(`*`) 처리 로직 구현**
  - 차량번호와 최종등록일 앞에 별표가 포함된 경우, 별표를 별도의 필드(`vhclno-asterisk`, `last-reg-ymd-asterisk`)로 분리하여 처리.
  - 별표 제거 후 본 데이터와 함께 파싱 및 조립 로직 반영:
    - `CarFfnlgTrgtServiceImpl`에 별표 분리 및 데이터 처리 코드 추가.
    - 별표가 포함된 경우 다시 앞부분에 별표를 붙이는 로직 구현.

- **`application.yml` 필드 길이 설정 변경**
  - `vhclno-asterisk`, `last-reg-ymd-asterisk` 필드 추가 및 길이(1바이트) 정의.
  - 기존 필드들의 바이트 길이 수정:
    - `vhclno`: 13 → 12 (2바이트), 12 → 11 (3바이트)
    - `last-reg-ymd`: 12 → 11 (2바이트), 11 → 10 (3바이트)
    - 기타 관련 필드 길이 조정.

- **DB 스키마 변경**
  - `tb_car_ffnlg_trgt` 테이블:
    - 날짜 필드(`RCPT_YMD`, `INSP_YMD`, `LAST_REG_YMD`, 등)의 길이 기존 8 → 10으로 확장.
    - `DAYCNT` 필드 길이 4 → 5, 유효기간 만료일자(`VLD_PRD_EXPRY_YMD`) 등 관련 필드 길이 변경.

- **로깅 개선**
  - 별표 존재 여부 및 최종 데이터 확인용 DEBUG 로그 추가:
    - 차량번호 및 최종등록일 시 별표 처리 전후 내역 상세 기록.

- **유지보수성 및 가독성 향상**
  - 반복적인 고정폭 필드 처리 로직 리팩토링:
    - `padRightBytes` 호출부 정리.
    - 중복 및 불필요한 변수 제거, 관련 주석 정리.
4 days ago
박성영 545832040b ### feat: 날짜 형식 처리 로직 수정 및 검증 로직 제거
- **날짜 형식 처리 방식 변경**
  - 기존 YYYYMMDD → YYYY-MM-DD 변환 로직 제거:
    - JSP 화면, Excel 다운로드, DB 쿼리에서 날짜 변환 없이 원본 날짜 값을 그대로 사용하도록 수정.
    - `formatYmd` 및 관련 변환 메서드 제거.

- **날짜 형식 검증 로직 비활성화**
  - 입력 데이터에서 '*' 포함된 특수 형식이 있는 경우를 고려하여 날짜 검증 로직 삭제:
    - `isValidDate` 호출 제거 및 주석 처리.
    - 검사일자, 검사종료일자, 유효기간만료일자 등에 대한 검증 로직 비활성화.

- **DB 스키마 변경**
  - `tb_car_ffnlg_trgt`, `tb_car_ffnlg_trgt_incmp` 테이블:
    - `RMRK_DTL` 컬럼 타입 `MODIFY COLUMN`으로 변경.

- **MyBatis 매퍼 수정**
  - `CarFfnlgTrgtMapper_maria.xml`:
    - SELECT 구문에서 DATE_FORMAT 제거, 원본 날짜 필드 그대로 반환하도록 수정.

- **서비스 로직 개선**
  - `CarFfnlgTrgtServiceImpl`:
    - `DateTimeFormatter` 추가 및 사용하지 않는 변수를 명확히 삭제.
    - `padRightBytes`, `padLeftBytes` 호출부에서 날짜 형식 그대로 처리.

- **기타**
  - `application.yml`에서 주석 정리 및 일관성 유지.
  - 코멘트 추가 및 불필요한 변환 관련 로직 제거.
4 days ago
박성영 cadf5af5a3 ### feat: 비고 상세 생성 로직 개선 및 데이터 처리 로직 확장
- **비고 상세 생성 및 업데이트**
  - `ComparisonOmRemarkBuilder` 및 `ComparisonRemarkBuilder` 수정:
    - 비고 상세 생성 시 API 호출 및 비교 과정, 판정 근거 등 추가 기록.
    - 각 판정 유형별로 세부 정보를 포함하도록 형식 개선.

- **Checker 로직 개선**
  - `OwnerCloseWithin31Checker`, `OwnerLevyOver31Checker` 등 `delay_checker` 클래스:
    - 명의이전일자 및 검사일 간 조건 비교 로직 확장.
    - 추가 쿼리 조건 및 상세한 레코드 비교 로직 포함.

- **DB 스키마 변경**
  - `tb_car_ffnlg_trgt`, `tb_car_ffnlg_trgt_incmp` 테이블:
    - `RMRK_DTL` 컬럼 타입을 `TEXT`로 변경.
    - `RMRK_DTL` 컬럼 신규 추가로 비고 상세 저장 로직을 지원.

- **API 수정**
  - `ComparisonRemarkBuilder.buildOwnerLevyOver31RemarkDetail` 메서드:
    - `vldPrdExpryYmd`, `inspEndYmd` 등 추가 파라미터 처리.
    - 검사종료일 및 유효기간만료일에 따른 추가 비교 로직 반영.

- **기타**
  - 기존 비고 생성 로직 리팩토링 및 상세 기록을 위한 주석 추가.
  - 변경 사항을 반영한 추가 SQL 파일 작성 및 테이블 변경 적용.
5 days ago
박성영 f6c70be983 ### feat: 사용자 정보 병렬 처리 지원 및 비교 로직 수정
- **병렬 처리 시 사용자 정보 전달 구현**
  - 세션 기반 사용자 정보 조회 로직 추가: `SessionUtil.getLoginUser()`.
  - 사용자 정보를 병렬 처리의 각 작업으로 전달하기 위해 메소드 서명 수정:
    - `processOneTarget`, `executeComparison`에 `LoginUserVO` 매개변수 추가.

- **Comparison 서비스 로직 업데이트**
  - 기존 세션 종속 비교 로직을 호출부에서 사용자 정보를 전달하도록 수정:
    - `ComparisonOmServiceImpl`, `ComparisonServiceImpl` 코드 변경.
    - `ComparisonOmChecker`, `delay_checker`의 호출 방식 업데이트.

- **코드 리팩토링**
  - 중복된 세션 접근 코드를 `try-catch` 처리로 통합.
  - 불필요한 세션 의존성 최소화 및 호출부 주입 방식으로 변경.

- **병렬 처리 개선**
  - `CompletableFuture` 기반 병렬 처리에 사용자 정보가 올바르게 전달되도록 수정.
  - 스레드 풀 크기 최적화: `(CPU 코어 수 * 2)`.

- **기타**
  - 주석 추가 및 로그 개선: 세션 조회 실패 경고 로그 및 사용자 정보 전달 내역 로깅.
  - 기존 트랜잭션 처리 방식과 병렬 처리 간의 연계 유지.
5 days ago
박성영 edc0aa5e89 ### feat: 사용자 조직코드 기반 비교 로직 추가 및 세션 의존성 제거
- **사용자 조직코드 추가**
  - `ComparisonChecker` 및 `ComparisonOmChecker`의 `check` 메서드에 `userOrgCd` 매개변수 추가.
  - 세션에서 사용자 정보를 가져오던 방식 제거 후, 호출부에서 조직코드를 전달하도록 변경.

- **Checker 클래스 수정**
  - `ProductUseChecker`, `OwnerCloseWithin31Checker`, `OwnerLevyOver31Checker` 등 모든 `delay_checker` 및 `om_checker` 클래스에서 새로운 매개변수(`userOrgCd`)를 활용하는 방식으로 수정.
  - 사용자 조직코드 유효성 검사 추가.

- **세션 의존성 제거**
  - 불필요한 `SessionUtil` 및 `LoginUserVO` 관련 코드 삭제.
  - 사용자 정보의 외부 주입 방식을 통해 세션 로그인을 사용하지 않는 호출도 가능하도록 개선.

- **비교 서비스 로직 수정**
  - `ComparisonServiceImpl` 및 `ComparisonOmServiceImpl`에서 세션 기반 사용자 정보 조회 후 조직코드 추출 및 모든 관련 호출로 전달.
  - 비교 로직 유지보수성 강화.

- **기타**
  - 중복 코드 제거 및 코드 정리.
  - 주석 업데이트.
5 days ago
박성영 9c6f16e4b2 ### feat: 명의이전일자 및 검사일 비교 로직 수정
- **명의이전일자 ~ 검사일 계산 조건 보완**
  - `명의이전일자 < 검사종료일` 조건을 추가하여 검사종료일에 따른 비교 로직 세분화.
  - 검사일과 명의이전일자 간 기준 일수 비교 조건 명확화:
    - 검사종료일 기준 조건 추가.
    - 명의이전일자가 검사일 내 포함되는지 확인 로직 개선.

- **적용 대상**
  - `OwnerCloseWithin31Checker`
  - `OwnerLevyOver31Checker`
  - `ProductCloseWithin31Checker`
  - `ProductLevyOver31Checker`

- **로그 레벨 조정**
  - `TransferOmChecker` 관련 `log.debug`를 `log.info`로 변경하여 로그 가시성 강화.

- **문서 업데이트**
  - `자동차과태료_비교로직_정리-[지연].md`에 관련 변경사항 반영:
    - 명의이전일자 비교 기준 및 로직 설명 수정.

- **기타**
  - 중복 코드 제거 및 가독성을 위한 리팩토링.
  - 검사일자의 파싱 처리 및 최신 명의이전일자 계산 로직 정리.
5 days ago
박성영 28fc8c4b87 ### feat: 비고(Remark) 생성 로직 개선 및 메서드 분리
- **비고 생성 메서드 수정 및 분리**
  - `ComparisonOmRemarkBuilder` 및 `ComparisonRemarkBuilder`:
    - `buildProductCloseLevyRemark` 메서드를 `buildProductCloseRemark`와 `buildProductLevyRemark`로 각각 분리.
    - 비고 형식과 요구사항에 따라 명확히 구분된 메서드 제공.
  - `OwnerTransferRemarkBuilder`와 관련된 비고 생성 메서드 개선:
    - 파라미터 추가(`sggNm`, `ownerNm`)로 상세 정보 포함 가능.

- **Checker 클래스 로직 수정**
  - `ProductLevyOver31Checker`, `ProductCloseWithin31Checker`, `OwnerTransferOmChecker`:
    - 분리된 비고 생성 메서드 호출 방식으로 변경.
    - 불필요 매개변수 제거 및 명확성 증가.

- **비고 형식 변경**
  - 31일 이내: `명의이전(YYYY.MM.DD) 이전소유자 상품용`
  - 31일 초과: `시군구명/소유자명, 미수검명의이전(YYYY.MM.DD)`

- **기타**
  - 중복된 메서드 제거 및 가독성을 위한 리팩토링.
  - 기존 로직 및 형식에 맞춰 코드 정리.
5 days ago
박성영 fb63a0bc2b ### feat: 비고 상세 생성 및 관리 기능 추가
- **주요 변경 사항**
  - `RMRK_DTL`(비고 상세) 컬럼 추가:
    - `tb_car_ffnlg_trgt`, `tb_car_ffnlg_trgt_incmp` 테이블에 `RMRK_DTL`(비고 상세, 최대 4000자) 컬럼 추가.
  - MyBatis 매퍼 및 VO 수정:
    - 관련 쿼리(`SELECT`, `INSERT`, `UPDATE`)에 `RMRK_DTL` 필드 추가.
    - `CarFfnlgTrgtVO`, `CarFfnlgTrgtExcelVO` 등 데이터 모델에 `RMRK_DTL` 속성 추가.
  - 비고 상세 생성 로직 추가:
    - `ComparisonRemarkBuilder`, `ComparisonOmRemarkBuilder`에 비고 상세 생성 메서드 다수 추가(`buildProductUseRemarkDetail` 등).
    - 로직 내 기존 `RMRK` 생성 메서드와 함께 호출하여 상세 내용을 추가 저장.
  - Checker 개선:
    - `delay_checker`, `om_checker` 등 모든 체크 로직에서 비고 상세(`RMRK_DTL`) 생성 후 DB 업데이트.

- **화면 및 UI 변경**
  - JSP 업데이트:
    - 상세 화면 및 Excel 다운로드 시 ‘비고 상세’ 열 추가.
    - ‘비고’는 너비 증가(`200px → 300px`), ‘비고 상세’는 신규 추가(`200px`).
    - 내용이 길 경우 30자 요약 표시 및 팝업 기능 추가.

- **기타**
  - SQL 정리 및 누락된 매퍼 반영.
  - 가독성 개선을 위한 코드 리팩토링.
  - 주석 및 문서 업데이트 (`자동차과태료_비교로직_정리-[미필].md`).
5 days ago
박성영 4691fb8073 ### feat: 비고(Remark) 생성 로직 개선 및 코드 단순화
- **비고 형식 변경 및 단순화**
  - 불필요한 다중 매개변수를 제거하고 단일 매개변수 처리로 변경
  - 상품용, 명의이전, 이첩 등의 비고 생성 로직에서 복잡한 문자열 빌더 사용 제거, 단순 문자열 반환 방식으로 수정

- **시군구명 및 소유자명 추가**
  - 명의이전 및 이첩 비고 생성 시 시군구명(`sggNm`)과 소유자명(`ownerNm`)을 파라미터로 추가
  - `carFfnlgTrgtMapper.selectSggNmBySggCd` 호출을 통해 시군구명 조회 로직 적용

- **불필요 메서드 삭제**
  - `NewBasicResponse` 및 `NewBasicResponse.Record` 관련 비고 생성 메서드 제거 (`buildProductUseRemark`, `buildProductUseChangeRemark` 등)

- **Checker 로직 수정**
  - `OmChecker`, `delay_checker` 내 비고 생성 로직 모두 단일화된 메서드 호출로 변경
  - 모든 비고 생성 로직에 시군구명 및 소유자명 적용
  - 호출 매개변수 최소화 및 유지보수성 강화

- **기타**
  - 기존에 사용되었던 복잡한 비고 생성 포맷 정리 및 관련 주석 제거
  - 중복 코드 제거 및 가독성을 위한 리팩토링
5 days ago
박성영 dde9bf4c02 feat: 과태료 대상 비교 로직 병렬 처리 및 트랜잭션 관리 개선
- **병렬 처리 도입**
  - 대상 데이터 처리 작업을 병렬화하여 성능 최적화
  - CPU 코어 수의 2배 스레드 풀 생성 및 `CompletableFuture` 활용

- **트랜잭션 단위 변경**
  - 기존: 전체 트랜잭션으로 관리, 하나의 실패 시 전체 롤백
  - 변경: 개별 대상별 트랜잭션 적용, 실패 데이터만 롤백

- **통계 및 로그 추가**
  - 성공/실패, 유형별 처리 건수(상품용, 이첩, 정상 등) 통계 데이터 제공
  - 병렬 처리 상태 및 작업 내역 로그 추가

- **기타**
  - `TransactionTemplate` 도입 및 가독성을 위한 코드 리팩토링
  - 문서 업데이트 (`자동차과태료_비교로직_정리-[미필].md`, `자동차과태료_비교로직_정리-[지연].md`)
5 days ago
박성영 914f61256b ### feat: 차량번호 및 소유자명 처리 로직 개선, 특수문자 제거 처리 추가
- **소유자명 검증 보완**
  - 소유자명은 null 또는 공백 입력도 가능하도록 로직 수정
  - 길이 초과(75자) 제한만 유효하게 유지

- **특수문자 '*' 제거 처리 추가**
  - 차량번호 및 일수(`daycnt`)에서 전출차량 및 재검여부를 나타내는 '*' 제거 후 검증
  - `createLedgerRequest`, `createBasicRequest` 등 차량번호를 사용하는 API 호출 로직에 적용
  - 숫자값 검증 및 비교 수행 시 특수문자 제거 처리 추가

- **차량번호 형식 검증 로직 보완**
  - '*' 특수문자 포함 여부를 허용하도록 정규식 및 검증 프로세스 수정
  - '*'만 포함된 경우는 유효하지 않도록 처리

- **Comparison 로직 수정**
  - 비교 로직 시작 시 차량번호 및 일수에서 '*' 제거 기능 추가

- **샘플 데이터 수정**
  - 샘플 데이터 파일 수정 (`docs/지연-샘플용-utf-8-1.txt`)
  - 차량번호 및 일수에서 전출차량 및 재검여부를 나타내는 '*' 적용 예시 추가

- **기타**
  - 가독성을 위한 주석 정리 및 로직 구조 개선
5 days ago
박성영 ad168358cb feat: 과태료 대상 비교 로직 병렬 처리 구현 및 성능 최적화
- **병렬 처리 도입**
  - 대상 데이터별로 처리 작업을 병렬화하여 성능 개선
  - I/O 작업 처리 시 CPU 코어 수의 2배 스레드 풀 적용

- **트랜잭션 단위 변경**
  - 기존: 전체가 하나의 트랜잭션으로 관리되어 하나의 대상 실패 시 전체 롤백 처리
  - 변경: 개별 데이터에 대해 독립적인 트랜잭션 적용, 실패 데이터만 롤백

- **통계 데이터 추가**
  - 성공/실패, 유형별 처리 건수(상품용, 이첩, 정상) 통계 제공

- **기타**
  - `TransactionTemplate` 및 `CompletableFuture` 활용
  - 병렬 처리 로그 및 예외 처리 추가
  - 코드 주석 및 가독성 개선
5 days ago
박성영 0dbd34c40c feat: API 초당 호출 제한 및 DB 스키마 수정
- **API Rate Limit 조정**
  - 초당 호출 제한(`permits-per-second`)을 5.0에서 0으로 변경 (무제한 호출 가능)

- **DB 스키마 변경**
  - `tb_car_ledger_frmbk_dtl` 테이블
    - `FLAG` 컬럼 크기를 `varchar(3)`에서 `varchar(1000)`로 확장 (데이터 저장 용량 증가)
5 days ago
박성영 d5286a509e feat: 상품용 및 명의이전 로직 보완
- **상품용(ProductUseOmChecker)**
  - 부과일자 소유자가 상품용인 경우의 검증 로직 보완
  - 명의이전(11) 레코드 조건에 `CHG_YMD <= 검사유효기간 종료일 + 31일` 추가

- **명의이전(OwnerTransferOmChecker)**
  - 명의이전 검증 시 `CHG_YMD > 검사유효기간 종료일 + 31일` 로 변경

- **문서 업데이트**
  - 비교로직 문서 (`자동차과태료_비교로직_정리-[미필].md`) 수정
  - 주요 변경사항 및 로직 설명 수정

- **기타**
  - 불필요 파일(CarFfnlgTrgtIncmpController.java.bak) 삭제
7 days ago
박성영 6b70444b07 디자인 수정 7 days ago
박성영 fb120ed4a5 로직 오류 수정 및 보완 7 days ago
박성영 45041c36de 필요없는 파일 삭제 및 sql 재정의 1 week ago
박성영 0f246eb867 del_yn 필드 적용 1 week ago
박성영 36a3041a36 미필 명의이전 오류 수정 1 week ago
박성영 d41a2975d3 미필 비교로직 MD 파일 작성 1 week ago
박성영 7be3d97430 지연 - 로그 및 주석 로직에 맞춰 수정 1 week ago
박성영 733986b838 지연 - 명의이전 - 일단 접수 02 상태 그대로 1 week ago
박성영 7985785bd4 지연 - 비교로직 재정의 1 week ago
박성영 c85deef5ee 지연 - 비교로직 재정의 1 week ago
박성영 9d8c306b59 미필 : 상품용 - 2단계: 1번 API 소유자 = 2번 API 소유자
비교로직 추가
1 week ago
박성영 64d367a8ef 미필 : 이첩 케이스-2 번 수정 1 week ago
박성영 e8e362f4c0 미필 : 이첩 케이스-2 번 적용 1 week ago
박성영 b72525e0f6 미필 주석 145 -> 146일로 일단 변경 1 week ago
박성영 a25c8ff359 내사종결-명의이전, 날짜수정후부과-명의이전
일자 검색조건 변경
// 조건: 유효기간만료일 <= CHG_YMD <= 검사종료일자
// 조건: 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자
1 week ago
박성영 6a08b6164f 미필 작업 진행 중... 2 weeks ago
박성영 36e1831153 미필 작업 진행 중... 2 weeks ago
박성영 a98ec94638 미필 작업 진행 중... 2 weeks ago
박성영 7e3fa07327 미필 작업 진행 중... 2 weeks ago
박성영 3885a218bc 미필 작업 진행 중... 2 weeks ago
박성영 983f987f6a 미필 일단 업로드까진 완료 2 weeks ago
박성영 20312693c6 미필 일단 업로드까진 완료 2 weeks ago
박성영 40d8518a50 미필 초기 작업 진행중.... 2 weeks ago
박성영 fdcf38bb31 대상 구분 컬럼 삭제 2 weeks ago
박성영 b157c28e1e 대상 구분 컬럼 삭제 2 weeks ago
박성영 6521e3e09b PRN 파일 확장자명으로 수정 2 weeks ago
박성영 3eff7edc9d PRN 파일 확장자명으로 수정 2 weeks ago
박성영 d738063b50 대상구분 대응소스 작업 진행중 2 weeks ago
박성영 26129a1dbc 신규로직 상품용-변경등록 2 weeks ago
박성영 3b9a2476b2 신규로직 상품용-변경등록 2 weeks ago
박성영 22f74b7bb3 비고 Builder 통일화 2 weeks ago
박성영 84ed820cfe 비고 자세히 볼수 있도록 레이어팝업 추가 2 weeks ago
박성영 5d1a3d31d0 순수 명의이전 로직 처리 변경중 2 weeks ago
박성영 4d605f943c 순수 명의이전 로직 처리 변경중 2 weeks ago
박성영 aa0bc73ee0 로직처리 변경중 2 weeks ago
박성영 958366b765 처리상태 검색조건 checkbox 로 변경 2 weeks ago
박성영 95d81d3de8 그리드 줄바꿈 되지 않아 , 표현 2 weeks ago
박성영 575a7fd231 그리드 줄바꿈 되지 않아 , 표현 2 weeks ago
박성영 d89f284c8a 넓이 조절 필요.... 실제 데이터 표현된 화면 봐야할듯 2 weeks ago
박성영 7bafc261a5 넓이 조절 필요.... 실제 데이터 표현된 화면 봐야할듯 2 weeks ago
박성영 0ba2b8e863 비고 정리중... 2 weeks ago
박성영 cba0b44cd6 doc 정리 진행중.... 2 weeks ago
박성영 8a620d1004 비고처리 명칭변경 2 weeks ago
박성영 54bdce574f 소스 완전 분리 처리 2 weeks ago
박성영 57eaef2809 일단... 커밋... 2 weeks ago
박성영 40100a8f37 비교 로직 정의 중... 2 weeks ago
박성영 abaa4ed946 3. 날짜 수정 후 부과 체크 - 명의이전 후 상품용인 경우 (31일 초과) 로직 추가 2 weeks ago
박성영 65200c68c4 3. 날짜 수정 후 부과 체크 - 명의이전 후 상품용인 경우 (31일 초과) 로직 추가 2 weeks ago
박성영 83b49b5a9d 3. 날짜 수정 후 부과 체크 - 명의이전 후 상품용인 경우 (31일 초과) 로직 추가 2 weeks ago
박성영 51266395e8 중간 커밋 2 weeks ago
박성영 36d13ac671 테이블컬럼 재정의
비교 로직 소스 정리 진행중...
2 weeks ago
박성영 791926ba6a CAR_BSC_MTTR_INQ_FLNM:자동차 기본사항조회 성명
마지막 조회자의 성명 update 추가
2 weeks ago
박성영 80348e881f 엑셀다운로드: 주민등록번호 포함 2 weeks ago
박성영 e20aeba146 엑셀다운로드 추가, 로직 md 파일 재정의 2 weeks ago
박성영 8e13313d57 명의이전 상품용 적용 2 weeks ago
박성영 c4e5f0419a 명의이전 상품용 적용 2 weeks ago
박성영 f5c924ce86 중간 커밋... 상품용 작업 중 2 weeks ago
박성영 a75f03fe3e 비고 메시지 정리 2 weeks ago
박성영 06ec4d3f04 자동차검사 과태료 API 호출이 '처리상태 : 접수,01'이 아닐경우에는 호출되지 않도록 변경
메시지 표출 변경
2 weeks ago
박성영 f41ee27f1a 자동차검사 과태료 API 호출이 '처리상태 : 접수,01'이 아닐경우에는 호출되지 않도록 변경 2 weeks ago
박성영 5f9e573f71 자동차검사 과태료 파비콘 추가.. AI 생성 이미지 2 weeks ago
박성영 ee0c76a7c6 자동차검사 과태료 파비콘 추가.. AI 생성 이미지 2 weeks ago
박성영 2a70a2d118 과태료 대상 목록 조회 보완 3 weeks ago
박성영 ddb6bcf081 과태료대상ID 클릭시 해당 API 호출 이력 보여주기 3 weeks ago
박성영 58bff37e5e 처리상태, 비고 수정로직 추가 3 weeks ago
박성영 2745927092 상품용 비교 로직 재수정... 진행중... 3 weeks ago
박성영 158d9f438c Date관련 함수 수정 3 weeks ago
박성영 f1551ab525 서버관리 명령어 정리,
간단한 소스 수정,
dump 파일 재등록
3 weeks ago
박성영 40a33790ea 연계결과 표출 최상단으로 순서 변경 3 weeks ago
박성영 eeda47f292 신규갑부정보에 맞춰 컬럼 자리수 변경 3 weeks ago
박성영 23613c8a08 신규갑부정보에 맞춰 컬럼 자리수 변경 3 weeks ago
박성영 04db9a7100 문구수정 3 weeks ago
박성영 a67be85f21 비교로직 및 보완, 로그, 트래잭션 등 수정 3 weeks ago
박성영 05874175d5 비교로직 및 보완, 로그, 트래잭션 등 수정 3 weeks ago
박성영 cdc37293f2 비교로직 보완 전 초기작업 3 weeks ago
박성영 d190e4dcf6 업로드 UTF-8 -> EUC-KR
다운로드 EUC-KR -> UTF-8
TODO 참조
3 weeks ago
박성영 c8a14271ff LOG LEVEL 조정 3 weeks ago
박성영 44b31ca509 ECU-KR 3 weeks ago
박성영 c8af0cccbe 주석 위치 수정 3 weeks ago
박성영 59605d6717 셋팅 변경 3 weeks ago
박성영 52edf2caf1 셋팅 변경 3 weeks ago
박성영 69d93f29f4 셋팅 변경 3 weeks ago
박성영 86b5c6047d 셋팅 변경 3 weeks ago
박성영 daffa75db9 셋팅 변경 3 weeks ago
박성영 4a4b7eedee 셋팅 변경 3 weeks ago
박성영 00508ded0f 셋팅 변경 3 weeks ago
박성영 eae319bc20 비교 로직 테스트 진행중.... 3 weeks ago
박성영 4bfa444fd8 비교 로직 테스트 진행중.... 3 weeks ago
박성영 bb08f61bb1 비교 로직 테스트 진행중.... 3 weeks ago
박성영 7ccd94a93d 다시 정리중... 3 weeks ago
박성영 8c0899a0d1 prd 실서버 내용 추가, 일단은 주석처리 4 weeks ago
박성영 4b598bef03 회사 내부 vmware test 4 weeks ago
박성영 d2cd43a951 회사 내부 vmware test 4 weeks ago
박성영 f6f2cc5a16 전체적인 old, new 구조로 변경 및 통합model 적용 진행중... 4 weeks ago
박성영 fb29fc8b31 전체적인 old, new 구조로 변경 및 통합model 적용 진행중... 4 weeks ago
박성영 cde26ea796 전체적인 old, new 구조로 변경 및 통합model 적용 진행중... 4 weeks ago
박성영 0004c7119f 전체적은 old, new 구조로 변경 및 통합model 적용 진행중... 4 weeks ago
박성영 0efc6eb361 야물파일에 맞춰 프로퍼티 구조 변경 4 weeks ago
박성영 7883cb3b42 전체적인 데이터 구조 작업 진행중...
현재 임시 커밋.. 나머지 로직 수정예정,
현재 부팅안됨
4 weeks ago
박성영 9480c30bf3 table 신규 api 정보에 맞게 정리 4 weeks ago
박성영 bbfdb1fe2d table 정리 4 weeks ago
박성영 05cbaab436 소스 정리 4 weeks ago
박성영 704083da07 외부호출만 있는 프로젝트로 변경 작업 진행중... 4 weeks ago
박성영 e128e126be 외부호출만 있는 프로젝트로 변경 작업 진행중... 4 weeks ago
박성영 298f1fe27f 비교로직 정리 4 weeks ago
박성영 b3b4bb2034 비교로직 정리 4 weeks ago
박성영 708544506f 비교로직 정리 4 weeks ago

7
.gitignore vendored

@ -29,16 +29,9 @@ replay_pid*
/build/
/.idea/
/.gradle/
/.idea/
/src/main/UbiService/logs/
/src/main/UbiService/results/UBIHTML/
/src/main/webapp/ubi4/logs/
/.vscode/settings.json
/CLAUDE.md
/DEV-SERVER-REPORT-FILE/UbiService/logs/
/DEV-SERVER-REPORT-FILE/UbiService/results/UBIHTML/
/.claude/settings.local.json
/DEV-SERVER-REPORT-FILE/UbiService/results/
/gradle/wrapper/gradle-wrapper.properties
/gradlew
/gradlew.bat

@ -0,0 +1,13 @@
-- 자동차 과태료 대상 ID 시퀀스
-- 테이블: seq_car_ffnlg_trgt_incmp_id
-- 컬럼: CAR_FFNLG_TRGT_INCMP_ID (varchar(20))
-- 시퀀스 삭제 (존재하는 경우)
DROP SEQUENCE IF EXISTS seq_car_ffnlg_trgt_incmp_id;
-- 시퀀스 생성
CREATE SEQUENCE seq_car_ffnlg_trgt_incmp_id
START WITH 1000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 99999999999999999;

@ -1,106 +1,108 @@
create table tb_car_bass_matter_inqire
(
CAR_BASS_MATTER_INQIRE_ID varchar(20) not null comment '자동차 기본 사항 조회 ID'
CAR_BASS_MATTER_INQIRE_ID varchar(20) not null comment '자동차 기본 사항 조회 ID'
primary key,
INFO_SYS_ID varchar(6) null comment '정보 시스템 ID',
INFO_SYS_IP varchar(23) null comment '정보 시스템 IP',
SIGUNGU_CODE varchar(5) null comment '시군구 코드',
CNTC_INFO_CODE varchar(15) null comment '연계 정보 코드',
CHARGER_ID varchar(15) null comment '담당자 ID',
CHARGER_IP varchar(23) null comment '담당자 IP',
CHARGER_NM varchar(75) null comment '담당자명',
DMND_LEVY_STDDE varchar(8) null comment '요청 부과 기준일',
DMND_INQIRE_SE_CODE varchar(1) null comment '요청 조회 구분 코드',
DMND_VHRNO varchar(30) null comment '요청 자동차등록번호',
DMND_VIN varchar(17) null comment '요청 차대번호',
CNTC_RESULT_CODE varchar(8) null comment '연계 결과 코드',
CNTC_RESULT_DTLS varchar(4000) null comment '연계 결과 상세',
PRYE varchar(4) null comment '연식',
REGIST_DE varchar(8) null comment '등록일',
ERSR_REGIST_SE_CODE varchar(4) null comment '말소 등록 구분 코드',
ERSR_REGIST_SE_NM varchar(30) null comment '말소 등록 구분명',
ERSR_REGIST_DE varchar(8) null comment '말소 등록일',
REGIST_DETAIL_CODE varchar(3) null comment '등록 상세 코드',
DSPLVL varchar(6) null comment '배기량',
USE_STRNGHLD_LEGALDONG_CODE varchar(10) null comment '사용 본거지 법정동 코드',
USE_STRNGHLD_ADSTRD_CODE varchar(10) null comment '사용 본거지 행정동 코드',
USE_STRNGHLD_MNTN varchar(2) null comment '사용 본거지 산',
USE_STRNGHLD_LNBR varchar(4) null comment '사용 본거지 번지',
USE_STRNGHLD_HO varchar(4) null comment '사용 본거지 호',
USE_STRNGHLD_ADRES_NM varchar(300) null comment '사용 본거지 상세주소',
USE_STRNGHLD_ROAD_NM_CODE varchar(12) null comment '사용 본거지 도로명 코드',
USGSRHLD_UNDGRND_BULD_SE_CODE varchar(1) null comment '사용 본거지 지하 건물 구분 코드',
USE_STRNGHLD_BULD_MAIN_NO varchar(5) null comment '사용 본거지 건물 주요 번호',
USE_STRNGHLD_BULD_SUB_NO varchar(5) null comment '사용 본거지 건물 부 번호',
USGSRHLD_ADRES_FULL varchar(750) null comment '사용 본거지 전체주소',
MBER_SE_CODE varchar(2) null comment '대표소유자 회원 구분 코드',
MBER_SE_NO varchar(100) null comment '대표소유자 회원 번호',
TELNO varchar(20) null comment '대표소유자 전화번호',
OWNER_LEGALDONG_CODE varchar(10) null comment '소유자 법정동 코드',
OWNER_ADSTRD_CODE varchar(10) null comment '소유자 행정동 코드',
OWNER_MNTN varchar(2) null comment '소유자 산',
OWNER_LNBR varchar(4) null comment '소유자 번지',
OWNER_HO varchar(4) null comment '소유자 호',
OWNER_ADRES_NM varchar(300) null comment '소유자 상세주소',
OWNER_ROAD_NM_CODE varchar(12) null comment '소유자 도로명 코드',
OWNER_UNDGRND_BULD_SE_CODE varchar(1) null comment '소유자 지하건물 구분 코드',
OWNER_BULD_MAIN_NO varchar(5) null comment '소유자 건물 주요 번호',
OWNER_BULD_SUB_NO varchar(5) null comment '소유자 건물 부 번호',
OWNR_WHOLADDR varchar(750) null comment '소유자 전체주소',
AFTR_VHRNO varchar(30) null comment '신 차량번호',
USE_FUEL_CODE varchar(1) null comment '사용 연료 코드',
PRPOS_SE_CODE varchar(2) null comment '용도 구분 코드',
MTRS_FOM_NM varchar(75) null comment '원동기 형식명',
FRNT_VHRNO varchar(30) null comment '이전 차량번호',
VHRNO varchar(30) null comment '차량번호',
VIN varchar(17) null comment '차대번호',
CNM varchar(75) null comment '차명',
VHCLE_TOT_WT varchar(6) null comment '차량 총 중량',
CAAG_ENDDE varchar(8) null comment '차령 만료일자',
CHANGE_DE varchar(8) null comment '차번호 변경시기',
VHCTY_ASORT_CODE varchar(1) null comment '차종 종별 코드',
VHCTY_TY_CODE varchar(1) null comment '차종 유형 코드',
VHCTY_SE_CODE varchar(1) null comment '차종 분류 코드',
MXMM_LDG varchar(10) null comment '최대 적재량',
VHCTY_ASORT_NM varchar(150) null comment '차종 종별명',
VHCTY_TY_NM varchar(150) null comment '차종 유형명',
VHCTY_SE_NM varchar(150) null comment '차종 분류명',
FRST_REGIST_DE varchar(8) null comment '최초 등록일',
FOM_NM varchar(75) null comment '형식',
ACQS_DE varchar(8) null comment '취득 일자',
ACQS_END_DE varchar(8) null comment '취득 종료일자',
YBL_MD varchar(8) null comment '제작 년월일',
TRANSR_REGIST_DE varchar(8) null comment '이전 등록일',
SPCF_REGIST_STTUS_CODE varchar(6) null comment '제원 등록 상태 코드',
COLOR_NM varchar(75) null comment '색상명',
MRTG_CO varchar(9) null comment '저당수',
SEIZR_CO varchar(9) null comment '압류건수',
STMD_CO varchar(9) null comment '구조변경수',
NMPL_CSDY_AT varchar(1) null comment '번호판 영치 여부',
NMPL_CSDY_REMNR_DE varchar(8) null comment '번호판 영치 최고일',
ORIGIN_SE_CODE varchar(1) null comment '출처 구분 코드',
NMPL_STNDRD_CODE varchar(1) null comment '번호판 규격 코드',
ACQS_AMOUNT varchar(18) null comment '취득 금액',
INSPT_VALID_PD_BGNDE varchar(8) null comment '검사 유효 기간 시작일',
INSPT_VALID_PD_ENDDE varchar(8) null comment '검사 유효 기간 종료일',
USE_STRNGHLD_GRC_CODE varchar(4) null comment '사용 본거지 관청 코드',
TKCAR_PSCAP_CO varchar(3) null comment '승차정원수',
SPMNNO varchar(17) null comment '제원관리번호',
TRVL_DSTNC varchar(10) null comment '주행거리',
FRST_REGIST_RQRCNO varchar(20) null comment '최초 등록 접수번호',
VLNT_ERSR_PRVNTC_NTICE_DE varchar(8) null comment '예고통지일',
REGIST_INSTT_NM varchar(150) null comment '등록 기관명',
PROCESS_IMPRTY_RESN_CODE varchar(2) null comment '처리 불가 사유 코드',
PROCESS_IMPRTY_RESN_DTLS varchar(75) null comment '처리 불가 사유 명세',
CBD_LT varchar(10) null comment '차체 길이',
CBD_BT varchar(10) null comment '차체 너비',
CBD_HG varchar(10) null comment '차체 높이',
FRST_MXMM_LDG varchar(10) null comment '최초 최대 적재량',
FUEL_CNSMP_RT varchar(5) null comment '연료 소비율',
ELCTY_CMPND_FUEL_CNSMP_RT varchar(5) null comment '전기 복합 연료 소비율',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자',
MBER_NM varchar(75) null comment '대표소유자 성명'
TX_ID varchar(25) null comment '트랜잭션 ID',
INFO_SYS_ID varchar(6) null comment '정보 시스템 ID',
INFO_SYS_IP_ADDR varchar(23) null comment '정보 시스템 IP',
SGG_CD varchar(5) null comment '시군구 코드',
LINK_INFO_CD varchar(15) null comment '연계 정보 코드',
PIC_ID varchar(15) null comment '담당자 ID',
PIC_IP_ADDR varchar(23) null comment '담당자 IP',
PIC_NM varchar(75) null comment '담당자명',
DMND_LEVY_CRTR_YMD varchar(8) null comment '요청 부과 기준일',
DMND_INQ_SE_CD varchar(1) null comment '요청 조회 구분 코드',
DMND_VHRNO varchar(30) null comment '요청 자동차등록번호',
DMND_VIN varchar(17) null comment '요청 차대번호',
LINK_RSLT_CD varchar(10) null comment '연계 결과 코드',
LINK_RSLT_DTL varchar(4000) null comment '연계 결과 상세',
YRIDNW varchar(4) null comment '연식',
REG_YMD varchar(10) null comment '등록일',
ERSR_REG_SE_CD varchar(4) null comment '말소 등록 구분 코드',
ERSR_REG_SE_NM varchar(30) null comment '말소 등록 구분명',
ERSR_REG_YMD varchar(10) null comment '말소 등록일',
REG_DTL_CD varchar(3) null comment '등록 상세 코드',
DSPLVL varchar(10) null comment '배기량',
USGSRHLD_STDG_CD varchar(10) null comment '사용 본거지 법정동 코드',
USGSRHLD_DONG_CD varchar(10) null comment '사용 본거지 행정동 코드',
USGSRHLD_MTN_YN varchar(2) null comment '사용 본거지 산',
USGSRHLD_LNBR varchar(4) null comment '사용 본거지 번지',
USGSRHLD_HO varchar(4) null comment '사용 본거지 호',
USGSRHLD_ADDR_NM varchar(300) null comment '사용 본거지 상세주소',
USGSRHLD_ROAD_NM_CD varchar(12) null comment '사용 본거지 도로명 코드',
USGSRHLD_UDGD_BLDG_SE_CD varchar(1) null comment '사용 본거지 지하 건물 구분 코드',
USGSRHLD_BMNO varchar(5) null comment '사용 본거지 건물 주요 번호',
USGSRHLD_BSNO varchar(5) null comment '사용 본거지 건물 부 번호',
USGSRHLD_WHOL_ADDR varchar(750) null comment '사용 본거지 전체주소',
RPRS_OWNR_MBR_SE_CD varchar(2) null comment '대표소유자 회원 구분 코드',
RPRS_OWNR_NM varchar(75) null comment '대표소유자 성명',
RPRSV_OWNR_IDECNO varchar(100) null comment '대표소유자 회원 번호',
RPRS_OWNR_TELNO varchar(25) null comment '대표소유자 전화번호',
OWNR_STDG_CD varchar(10) null comment '소유자 법정동 코드',
OWNR_DONG_CD varchar(10) null comment '소유자 행정동 코드',
OWNR_MTN_YN varchar(2) null comment '소유자 산',
OWNR_LNBR varchar(4) null comment '소유자 번지',
OWNR_HO varchar(4) null comment '소유자 호',
OWNR_ADDR_NM varchar(300) null comment '소유자 상세주소',
OWNR_ROAD_NM_CD varchar(12) null comment '소유자 도로명 코드',
OWNR_UDGD_BLDG_SE_CD varchar(1) null comment '소유자 지하건물 구분 코드',
OWNR_BMNO varchar(5) null comment '소유자 건물 주요 번호',
OWNR_BSNO varchar(5) null comment '소유자 건물 부 번호',
OWNR_WHOL_ADDR varchar(750) null comment '소유자 전체주소',
REAR_VHRNO varchar(30) null comment '신 차량번호',
USE_FUEL_CD varchar(1) null comment '사용 연료 코드',
USG_SE_CD varchar(2) null comment '용도 구분 코드',
MTRS_FOM_NM varchar(75) null comment '원동기 형식명',
BFR_VHRNO varchar(30) null comment '이전 차량번호',
VHRNO varchar(30) null comment '차량번호',
VIN varchar(17) null comment '차대번호',
ATMB_NM varchar(75) null comment '차명',
VHCL_TOTL_WT varchar(6) null comment '차량 총 중량',
VEAG_END_YMD varchar(10) null comment '차령 만료일자',
CHG_YMD varchar(10) null comment '차번호 변경시기',
CARMDL_ASORT_CD varchar(1) null comment '차종 종별 코드',
CARMDL_TYPE_CD varchar(1) null comment '차종 유형 코드',
CARMDL_SE_CD varchar(1) null comment '차종 분류 코드',
MXMM_LDG varchar(10) null comment '최대 적재량',
CARMDL_ASORT_NM varchar(150) null comment '차종 종별명',
CARMDL_TYPE_NM varchar(150) null comment '차종 유형명',
CARMDL_CLSF_NM varchar(150) null comment '차종 분류명',
FRST_REG_YMD varchar(10) null comment '최초 등록일',
FOM_NM varchar(75) null comment '형식',
ACQS_YMD varchar(10) null comment '취득 일자',
ACQS_END_YMD varchar(10) null comment '취득 종료일자',
FBCTN_YMD varchar(10) null comment '제작 년월일',
TRANSR_REG_YMD varchar(10) null comment '이전 등록일',
SPCF_REG_STTS_CD varchar(6) null comment '제원 등록 상태 코드',
COLOR_NM varchar(75) null comment '색상명',
MRTG_CNT varchar(9) null comment '저당수',
SZR_CNT varchar(9) null comment '압류건수',
STRCT_CHG_CNT varchar(9) null comment '구조변경수',
NOPLT_CSDY_YN varchar(1) null comment '번호판 영치 여부',
NOPLT_CSDY_AVTSMT_YMD varchar(10) null comment '번호판 영치 최고일',
SRC_SE_CD varchar(1) null comment '출처 구분 코드',
NOPLT_SPCFCT_CD varchar(1) null comment '번호판 규격 코드',
ACQS_AMT varchar(100) null comment '취득 금액',
INSP_VLD_PD_BGNG_YMD varchar(10) null comment '검사 유효 기간 시작일',
INSP_VLD_PD_END_YMD varchar(10) null comment '검사 유효 기간 종료일',
USGSRHLD_GRC_CD varchar(4) null comment '사용 본거지 관청 코드',
RDCPCT_CNT varchar(3) null comment '승차정원수',
SPMNNO varchar(25) null comment '제원관리번호',
DRVNG_DSTNC varchar(10) null comment '주행거리',
FRST_REG_APLY_RCPT_NO varchar(25) null comment '최초 등록 접수번호',
VLNT_ERSR_PRVNTC_AVTSMT_YMD varchar(10) null comment '예고통지일',
OGNZ_NM varchar(150) null comment '등록 기관명',
PRCS_IMPRTY_RSN_CD varchar(2) null comment '처리 불가 사유 코드',
PRCS_IMPRTY_RSN_DTLS varchar(75) null comment '처리 불가 사유 명세',
CBD_LT varchar(10) null comment '차체 길이',
CBD_BT varchar(10) null comment '차체 너비',
CBD_HG varchar(10) null comment '차체 높이',
FRST_MXMM_LDG varchar(10) null comment '최초 최대 적재량',
FUEL_CNSMPRT varchar(5) null comment '연료 소비율',
ELCTY_CMPND_FUEL_CNSMPRT varchar(5) null comment '전기 복합 연료 소비율',
CAR_FFNLG_TRGT_ID varchar(20) null comment '자동차 과태료 대상 ID',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자'
)
comment '자동차 기본 사항 조회';

@ -1,28 +1,28 @@
-- auto-generated definition
create table tb_car_ffnlg_trgt
(
CAR_FFNLG_TRGT_ID varchar(20) not null comment '자동차 과태료 대상 ID'
primary key,
RCPT_YMD varchar(8) null comment '접수 일자',
FFNLG_TRGT_SE_CD varchar(1) null comment '과태료 대상 구분 코드',
RCPT_YMD varchar(10) null comment '접수 일자',
INSPSTN_CD varchar(8) null comment '검사소 코드',
INSP_YMD varchar(8) null comment '검사 일자',
INSP_YMD varchar(10) null comment '검사 일자',
VHCLNO varchar(30) null comment '차량번호',
OWNR_NM varchar(75) null comment '소유자 명',
RRNO varchar(100) null comment '주민등록번호',
CAR_NM varchar(100) null comment '자동차 명',
CAR_KND varchar(100) null comment '자동차 종류',
CAR_USG varchar(100) null comment '자동차 용도',
INSP_END_YMD varchar(8) null comment '검사 종료 일자',
INSP_END_YMD varchar(10) null comment '검사 종료 일자',
DAYCNT varchar(5) null comment '일수',
REINSP_YN varchar(1) null comment '재검 여부',
FFNLG_AMT varchar(10) null comment '과태료 금액',
LAST_REG_YMD varchar(8) null comment '최종 등록 일자',
LAST_REG_YMD varchar(11) null comment '최종 등록 일자',
ADDR varchar(600) null comment '주소',
VLD_PRD_EXPRY_YMD varchar(8) null comment '유효 기간 만료 일자',
VLD_PRD_EXPRY_YMD varchar(10) null comment '유효 기간 만료 일자',
TRD_GDS varchar(100) null comment '매매 상품',
TASK_PRCS_STTS_CD varchar(2) null comment '업무 처리 상태 코드',
TASK_PRCS_YMD varchar(8) null comment '업무 처리 일자',
TASK_PRCS_YMD varchar(10) null comment '업무 처리 일자',
RMRK varchar(4000) null comment '비고',
RMRK_DTL text null comment '비고 상세',
CAR_BASS_MATTER_INQIRE_ID varchar(20) null comment '자동차 기본 사항 조회 ID',
CAR_LEDGER_FRMBK_ID varchar(20) null comment '자동차 등록 원부 갑 ID',
CAR_BSC_MTTR_INQ_FLNM varchar(75) null comment '자동차 기본 사항 조회 성명',
@ -30,7 +30,7 @@ create table tb_car_ffnlg_trgt
CAR_BSC_MTTR_INQ_SGG_NM varchar(75) null comment '자동차 기본 사항 조회 시군구 명',
CAR_REG_FRMBK_CHG_TASK_SE_CD varchar(2) null comment '자동차 등록 원부갑 변경 업무 구분 코드',
CAR_REG_FRMBK_CHG_TASK_SE_NM varchar(75) null comment '자동차 등록 원부갑 변경 업무 구분 명',
CAR_REG_FRMBK_CHG_YMD varchar(8) null comment '자동차 등록 원부갑 변경 일자',
CAR_REG_FRMBK_CHG_YMD varchar(10) null comment '자동차 등록 원부갑 변경 일자',
CAR_REG_FRMBK_DTL varchar(2000) null comment '자동차 등록 원부갑 상세',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자',
@ -38,4 +38,5 @@ create table tb_car_ffnlg_trgt
DEL_DT datetime null comment '삭제 일시',
DLTR varchar(11) null comment '삭제자'
)
comment '자동차 과태료 대상';
comment '자동차 과태료 대상';

@ -0,0 +1,41 @@
create table tb_car_ffnlg_trgt_incmp
(
CAR_FFNLG_TRGT_INCMP_ID varchar(20) not null comment '자동차 과태료 대상 미필 ID'
primary key,
RCPT_YMD varchar(10) null comment '접수 일자',
PRGRM_ID varchar(10) null comment '프로그램 ID',
PRCS_YMD varchar(100) null comment '처리 일자',
OTPT_DT varchar(100) null comment '출력 일시',
NO decimal null comment '번호',
VHCLNO varchar(30) null comment '차량번호',
OWNR_NM varchar(75) null comment '소유자 명',
RRNO varchar(100) null comment '주민등록번호',
CAR_NM varchar(100) null comment '자동차 명',
USE_STRHLD_ADDR varchar(600) null comment '사용 본거지 주소',
USE_STRHLD_ADDR_DTL varchar(600) null comment '사용 본거지 주소 상세',
INSP_VLD_PRD varchar(30) null comment '검사 유효 기간',
TASK_PRCS_STTS_CD varchar(2) null comment '업무 처리 상태 코드',
TASK_PRCS_YMD varchar(10) null comment '업무 처리 일자',
RMRK varchar(4000) null comment '비고',
RMRK_DTL TEXT null comment '비고 상세',
CAR_BASS_MATTER_INQIRE_ID varchar(20) null comment '자동차 기본 사항 조회 ID',
CAR_LEDGER_FRMBK_ID varchar(20) null comment '자동차 등록 원부 갑 ID',
CAR_BSC_MTTR_INQ_FLNM varchar(75) null comment '자동차 기본 사항 조회 성명',
CAR_BSC_MTTR_INQ_SGG_CD varchar(5) null comment '자동차 기본 사항 조회 시군구 코드',
CAR_BSC_MTTR_INQ_SGG_NM varchar(75) null comment '자동차 기본 사항 조회 시군구 명',
CAR_REG_FRMBK_CHG_TASK_SE_CD varchar(2) null comment '자동차 등록 원부갑 변경 업무 구분 코드',
CAR_REG_FRMBK_CHG_TASK_SE_NM varchar(75) null comment '자동차 등록 원부갑 변경 업무 구분 명',
CAR_REG_FRMBK_CHG_YMD varchar(10) null comment '자동차 등록 원부갑 변경 일자',
CAR_REG_FRMBK_DTL varchar(2000) null comment '자동차 등록 원부갑 상세',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자',
DEL_YN char not null comment '삭제 여부',
DEL_DT datetime null comment '삭제 일시',
DLTR varchar(11) null comment '삭제자'
)
comment '자동차 과태료 대상 미필';
ALTER TABLE tb_car_ffnlg_trgt_incmp
MODIFY COLUMN RMRK_DTL TEXT NULL COMMENT '비고 상세';

@ -1,87 +1,90 @@
create table tb_car_ledger_frmbk
(
CAR_LEDGER_FRMBK_ID varchar(20) not null comment '자동차 등록 원부 갑 ID'
CAR_LEDGER_FRMBK_ID varchar(20) not null comment '자동차 등록 원부 갑 ID'
primary key,
INFO_SYS_ID varchar(6) null comment '정보 시스템 ID',
INFO_SYS_IP varchar(23) null comment '정보 시스템 IP',
SIGUNGU_CODE varchar(5) null comment '시군구 코드',
CNTC_INFO_CODE varchar(15) null comment '연계 정보 코드',
CHARGER_ID varchar(15) null comment '담당자 ID',
CHARGER_IP varchar(23) null comment '담당자 IP',
CHARGER_NM varchar(75) null comment '담당자명',
DMND_VHRNO varchar(30) null comment '요청 자동차등록번호',
DMND_ONES_INFORMATION_OPEN varchar(1) null comment '요청 개인 정보 공개',
DMND_CPTTR_NM varchar(75) null comment '요청 민원인 성명',
DMND_CPTTR_IHIDNUM varchar(100) null comment '요청 민원인 주민번호',
DMND_CPTTR_LEGALDONG_CODE varchar(10) null comment '요청 민원인 법정동 코드',
DMND_ROUTE_SE_CODE varchar(1) null comment '요청 경로 구분 코드',
DMND_DETAIL_EXPRESSION varchar(1) null comment '요청 내역 표시',
DMND_INQIRE_SE_CODE varchar(1) null comment '요청 조회 구분 코드',
CNTC_RESULT_CODE varchar(8) null comment '연계 결과 코드',
CNTC_RESULT_DTLS varchar(4000) null comment '연계 결과 상세',
LEDGER_GROUP_NO varchar(6) null comment '원부 그룹 번호',
LEDGER_INDVDLZ_NO varchar(6) null comment '원부 개별 번호',
VHMNO varchar(20) null comment '차량관리번호',
VHRNO varchar(30) null comment '차량등록번호',
VIN varchar(17) null comment '차대번호',
VHCTY_ASORT_CODE varchar(1) null comment '차종 종별 코드',
VHCTY_ASORT_NM varchar(30) null comment '차종 종별명',
CNM varchar(75) null comment '차명',
COLOR_CODE varchar(2) null comment '색상 코드',
COLOR_NM varchar(30) null comment '색상명',
NMPL_STNDRD_CODE varchar(1) null comment '번호판 규격 코드',
NMPL_STNDRD_NM varchar(30) null comment '번호판 규격명',
PRPOS_SE_CODE varchar(2) null comment '용도 구분 코드',
PRPOS_SE_NM varchar(20) null comment '용도 구분명',
MTRS_FOM_NM varchar(75) null comment '원동기 형식명',
FOM_NM varchar(75) null comment '형식명',
ACQS_AMOUNT varchar(50) null comment '취득 금액',
REGIST_DETAIL_CODE varchar(8) null comment '등록 상세 코드',
REGIST_DETAIL_NM varchar(30) null comment '등록 상세명',
FRST_REGIST_DE varchar(8) null comment '최초 등록일',
CAAG_ENDDE varchar(8) null comment '차령 종료일',
PRYE varchar(4) null comment '연식',
SPMNNO1 varchar(3) null comment '제원관리번호1',
SPMNNO2 varchar(14) null comment '제원관리번호2',
YBL_MD varchar(8) null comment '제작 년월일',
TRVL_DSTNC varchar(10) null comment '주행 거리',
INSPT_VALID_PD_BGNDE varchar(8) null comment '검사 유효 기간 시작일',
INSPT_VALID_PD_ENDDE varchar(8) null comment '검사 유효 기간 종료일',
CHCK_VALID_PD_BGNDE varchar(8) null comment '점검 유효 기간 시작일',
CHCK_VALID_PD_ENDDE varchar(8) null comment '점검 유효 기간 종료일',
REGIST_REQST_SE_NM varchar(75) null comment '등록 신청 구분명',
FRST_REGIST_RQRCNO varchar(20) null comment '최초 등록 접수번호',
NMPL_CSDY_REMNR_DE varchar(8) null comment '번호판 영치 최고일',
NMPL_CSDY_AT varchar(1) null comment '번호판 영치 여부',
BSS_USE_PD varchar(30) null comment '사업용 사용 기간',
OCTHT_ERSR_PRVNTC_NTICE_DE varchar(8) null comment '직권 말소 예고 통지일',
ERSR_REGIST_DE varchar(8) null comment '말소 등록일',
ERSR_REGIST_SE_CODE varchar(4) null comment '말소 등록 구분 코드',
ERSR_REGIST_SE_NM varchar(200) null comment '말소 등록 구분명',
MRTGCNT varchar(4) null comment '저당수',
VHCLECNT varchar(4) null comment '압류건수',
STMDCNT varchar(4) null comment '구조변경수',
ADRES1 varchar(750) null comment '사용 본거지 주소',
ADRES_NM1 varchar(300) null comment '사용 본거지 주소상세',
ADRES varchar(750) null comment '소유자 주소',
ADRES_NM varchar(300) null comment '소유자 주소상세',
INDVDL_BSNM_AT varchar(1) null comment '개인 사업자 여부',
TELNO varchar(30) null comment '대표소유자 전화번호',
MBER_NM varchar(75) null comment '대표소유자 성명',
MBER_SE_CODE varchar(2) null comment '대표소유자 회원 구분 코드',
MBER_SE_NO varchar(100) null comment '대표소유자 회원 번호',
TAXXMPT_TRGTER_SE_CODE varchar(2) null comment '비과세 대상 구분 코드',
TAXXMPT_TRGTER_SE_CODE_NM varchar(30) null comment '비과세 대상 구분 코드명',
CNT_MATTER varchar(5) null comment '특기사항 건수',
EMD_NM varchar(75) null comment '사용 본거지 행정동명',
PRVNTCCNT varchar(4) null comment '예고수',
XPORT_FLFL_AT_STTEMNT_DE varchar(8) null comment '수출 이행 여부 신고일',
PARTN_RQRCNO varchar(13) null comment '발급번호',
FRST_TRNSFR_DE varchar(8) null comment '최초 양도일',
PROCESS_IMPRTY_RESN_CODE varchar(2) null comment '처리 불가 사유 코드',
PROCESS_IMPRTY_RESN_DTLS varchar(200) null comment '처리 불가 사유 명세',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자'
TX_ID varchar(25) null comment '트랜잭션 ID',
INFO_SYS_ID varchar(6) null comment '정보 시스템 ID',
INFO_SYS_IP_ADDR varchar(23) null comment '정보 시스템 IP',
SGG_CD varchar(5) null comment '시군구 코드',
LINK_INFO_CD varchar(15) null comment '연계 정보 코드',
PIC_ID varchar(15) null comment '담당자 ID',
PIC_IP_ADDR varchar(23) null comment '담당자 IP',
PIC_NM varchar(75) null comment '담당자명',
DMND_VHRNO varchar(30) null comment '요청 자동차등록번호',
DMND_PRVC_RLS varchar(1) null comment '요청 개인 정보 공개',
DMND_CVLPR_NM varchar(75) null comment '요청 민원인 성명',
DMND_CVLPR_IDECNO varchar(100) null comment '요청 민원인 주민번호',
DMND_CVLPR_STDG_CD varchar(10) null comment '요청 민원인 법정동 코드',
DMND_PATH_SE_CD varchar(1) null comment '요청 경로 구분 코드',
DMND_DSCTN_INDCT varchar(1) null comment '요청 내역 표시',
DMND_INQ_SE_CD varchar(1) null comment '요청 조회 구분 코드',
LINK_RSLT_CD varchar(8) null comment '연계 결과 코드',
LINK_RSLT_DTL varchar(4000) null comment '연계 결과 상세',
LEDGER_GROUP_NO varchar(10) null comment '원부 그룹 번호',
LEDGER_INDIV_NO varchar(10) null comment '원부 개별 번호',
VHMNO varchar(25) null comment '차량관리번호',
VHRNO varchar(30) null comment '차량등록번호',
VIN varchar(17) null comment '차대번호',
CARMDL_ASORT_CD varchar(1) null comment '차종 종별 코드',
CARMDL_ASORT_NM varchar(30) null comment '차종 종별명',
ATMB_NM varchar(75) null comment '차명',
COLOR_CD varchar(2) null comment '색상 코드',
COLOR_NM varchar(30) null comment '색상명',
NOPLT_SPCFCT_CD varchar(1) null comment '번호판 규격 코드',
NOPLT_SPCFCT_NM varchar(30) null comment '번호판 규격명',
USG_SE_CD varchar(2) null comment '용도 구분 코드',
USG_SE_NM varchar(20) null comment '용도 구분명',
MTRS_FOM_NM varchar(75) null comment '원동기 형식명',
FOM_NM varchar(75) null comment '형식명',
ACQS_AMT varchar(100) null comment '취득 금액',
REG_DTL_CD varchar(8) null comment '등록 상세 코드',
REG_DTL_NM varchar(30) null comment '등록 상세명',
FRST_REG_YMD varchar(10) null comment '최초 등록일',
VEAG_END_YMD varchar(10) null comment '차령 종료일',
YRIDNW varchar(4) null comment '연식',
SPMNNO_1 varchar(10) null comment '제원관리번호1',
SPMNNO_2 varchar(20) null comment '제원관리번호2',
FBCTN_YMD varchar(10) null comment '제작 년월일',
DRVNG_DSTNC varchar(10) null comment '주행 거리',
INSP_VLD_PD_BGNG_YMD varchar(10) null comment '검사 유효 기간 시작일',
INSP_VLD_PD_END_YMD varchar(10) null comment '검사 유효 기간 종료일',
CHCK_VLD_PD_BGNG_YMD varchar(10) null comment '점검 유효 기간 시작일',
CHCK_VLD_PD_END_YMD varchar(10) null comment '점검 유효 기간 종료일',
REG_APLY_SE_NM varchar(75) null comment '등록 신청 구분명',
FRST_REG_APLY_RCPT_NO varchar(20) null comment '최초 등록 접수번호',
NOPLT_CSDY_AVTSMT_YMD varchar(10) null comment '번호판 영치 최고일',
NOPLT_CSDY_YN varchar(1) null comment '번호판 영치 여부',
BSS_USE_PD_YMD varchar(30) null comment '사업용 사용 기간',
OCTHT_ERSR_PRVNTC_AVTSMT_YMD varchar(10) null comment '직권 말소 예고 통지일',
ERSR_REG_YMD varchar(10) null comment '말소 등록일',
ERSR_REG_SE_CD varchar(4) null comment '말소 등록 구분 코드',
ERSR_REG_SE_NM varchar(200) null comment '말소 등록 구분명',
MRTG_CNT varchar(4) null comment '저당수',
SZR_CNT varchar(4) null comment '압류건수',
STRCT_CHG_CNT varchar(4) null comment '구조변경수',
USGSRHLD_ADDR_1 varchar(750) null comment '사용 본거지 주소',
USGSRHLD_ADDR_DTL_1 varchar(300) null comment '사용 본거지 주소상세',
OWNR_ADDR varchar(750) null comment '소유자 주소',
OWNR_ADDR_DTL varchar(300) null comment '소유자 주소상세',
INDVDL_BZMN_YN varchar(1) null comment '개인 사업자 여부',
RPRS_OWNR_TELNO varchar(30) null comment '대표소유자 전화번호',
RPRS_OWNR_NM varchar(75) null comment '대표소유자 성명',
RPRS_OWNR_MBR_SE_CD varchar(2) null comment '대표소유자 회원 구분 코드',
RPRSV_OWNR_IDECNO varchar(100) null comment '대표소유자 회원 번호',
TAXXMPT_TRPR_SE_CD varchar(2) null comment '비과세 대상 구분 코드',
TAXXMPT_APLCN_SE_CD varchar(30) null comment '비과세 대상 구분 코드명',
SPCABL_MTTR_CNT varchar(5) null comment '특기사항 건수',
USGSRHLD_DONG_NM varchar(75) null comment '사용 본거지 행정동명',
PRVNTC_CNT varchar(4) null comment '예고수',
XPORT_FLFL_YN_DCLR_YMD varchar(10) null comment '수출 이행 여부 신고일',
ISSU_NO varchar(13) null comment '발급번호',
FRST_TRNSFR_YMD varchar(10) null comment '최초 양도일',
DRIV_SRGBTRY_IDNTF_NO varchar(20) null comment '구동축전지 식별번호',
PRCS_IMPRTY_RSN_CD varchar(2) null comment '처리 불가 사유 코드',
PRCS_IMPRTY_RSN_DTLS varchar(200) null comment '처리 불가 사유 명세',
CAR_FFNLG_TRGT_ID varchar(20) null comment '자동차 과태료 대상 ID',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자'
)
comment '자동차 등록 원부 갑';

@ -3,19 +3,21 @@ create table tb_car_ledger_frmbk_dtl
CAR_LEDGER_FRMBK_DTL_ID varchar(20) not null comment '자동차 등록 원부 갑 상세 ID'
primary key,
CAR_LEDGER_FRMBK_ID varchar(20) null comment '자동차 등록 원부 갑 ID',
MAINCHK varchar(2) null comment '해제여부',
CHANGE_JOB_SE_CODE varchar(2) null comment '변경 업무 구분 코드',
MAINNO varchar(4) null comment '주번호',
SUBNO varchar(4) null comment '부번호',
DTLS varchar(2000) null comment '사항란',
RQRCNO varchar(20) null comment '접수번호',
VHMNO varchar(20) null comment '차량관리번호',
LEDGER_GROUP_NO varchar(6) null comment '원부 그룹 번호',
LEDGER_INDVDLZ_NO varchar(6) null comment '원부 개별 번호',
GUBUN_NM varchar(75) null comment '변경 업무 구분명',
CHANGE_DE varchar(8) null comment '변경 일자',
DETAIL_SN varchar(6) null comment '상세 순번',
FLAG varchar(3) null comment '표기여부',
SZR_RMV_DTL_SN varchar(2) null comment '해제여부',
CHG_TASK_SE_CD varchar(2) null comment '변경 업무 구분 코드',
MAIN_NO varchar(10) null comment '주번호',
SNO varchar(10) null comment '부번호',
SPCABL_MTTR varchar(2000) null comment '사항란',
HSHLDR_NM varchar(75) null comment '세대주명',
HSHLDR_IDECNO varchar(500) null comment '세대주개인암호화번호',
APLY_RCPT_NO varchar(25) null comment '접수번호',
VHMNO varchar(25) null comment '차량관리번호',
LEDGER_GROUP_NO varchar(10) null comment '원부 그룹 번호',
LEDGER_INDIV_NO varchar(10) null comment '원부 개별 번호',
CHG_TASK_SE_NM varchar(75) null comment '변경 업무 구분명',
CHG_YMD varchar(10) null comment '변경 일자',
DTL_SN varchar(10) null comment '상세 순번',
FLAG varchar(1000) null comment '표기여부',
REG_DT datetime null comment '등록 일시',
RGTR varchar(11) null comment '등록자'
)

File diff suppressed because it is too large Load Diff

@ -140,10 +140,6 @@ dependencies {
// SQL datasource-proxy
implementation 'net.ttddyy:datasource-proxy:1.10.1'
// ===== VMIS =====
// GPKI ( API )
implementation files('lib/libgpkiapi_jni_1.5.jar')
// ===== =====
// Lombok - (Getter, Setter, Builder )
compileOnly 'org.projectlombok:lombok'

@ -1,958 +0,0 @@
# VMIS API 통합 아키텍처 문서
## 📋 목차
- [1. 개요](#1-개요)
- [2. 패키지 구조](#2-패키지-구조)
- [3. 아키텍처 패턴](#3-아키텍처-패턴)
- [4. Internal Mode 상세](#4-internal-mode-상세)
- [5. External Mode 상세](#5-external-mode-상세)
- [6. 공통 구성요소](#6-공통-구성요소)
- [7. 데이터 흐름](#7-데이터-흐름)
- [8. 설정 가이드](#8-설정-가이드)
- [9. 사용 예제](#9-사용-예제)
---
## 1. 개요
### 1.1 목적
VMIS(차량관리정보시스템) API 통합 모듈은 차량 정보 조회 기능을 제공하며, 두 가지 운영 모드를 지원합니다:
- **Internal Mode**: 내부 VMIS 모듈을 직접 호출 (성능 우선)
- **External Mode**: 외부 REST API 서버 호출 (분리된 아키텍처)
### 1.2 설계 원칙
- **Strategy Pattern**: 런타임에 알고리즘(구현체) 전환
- **Dependency Inversion**: 인터페이스에 의존, 구현체는 교체 가능
- **Single Responsibility**: 각 클래스는 단일 책임만 수행
- **Open-Closed**: 확장에는 열려있고 수정에는 닫혀있음
---
## 2. 패키지 구조
```
src/main/java/go/kr/project/api/
├── VehicleInfoService.java # 공통 인터페이스
├── config/ # 공통 설정
│ ├── ApiConstant.java # API 상수
│ ├── ApiMapperConfig.java # API Mapper 스캔 설정
│ ├── VmisIntegrationConfig.java # 통합 설정 및 모니터링
│ └── properties/
│ └── VmisProperties.java # 설정 바인딩 클래스
├── vo/ # 공통 응답 VO (공유)
│ ├── VehicleApiResponseVO.java # 최상위 응답 객체
│ ├── VehicleBasicInfoVO.java # 차량 기본정보 VO
│ ├── VehicleLedgerVO.java # 차량 등록원부 VO
│ ├── VehicleBasicRequestVO.java # 기본정보 요청 VO
│ └── VehicleLedgerRequestVO.java # 등록원부 요청 VO
├── internal/ # Internal Mode (내부 모듈)
│ ├── service/
│ │ ├── InternalVehicleInfoServiceImpl.java # 내부 모드 구현체 ⭐
│ │ ├── VmisCarBassMatterInqireService.java # 기본정보 조회 서비스
│ │ ├── VmisCarBassMatterInqireLogService.java # 기본정보 로그 서비스
│ │ ├── VmisCarLedgerFrmbkService.java # 등록원부 조회 서비스
│ │ └── VmisCarLedgerFrmbkLogService.java # 등록원부 로그 서비스
│ │
│ ├── client/
│ │ ├── GovernmentApi.java # 정부 API 인터페이스
│ │ └── GovernmentApiClient.java # 정부 API 클라이언트
│ │
│ ├── mapper/
│ │ ├── VmisCarBassMatterInqireMapper.java # 기본정보 Mapper
│ │ └── VmisCarLedgerFrmbkMapper.java # 등록원부 Mapper
│ │
│ ├── model/ # Internal 전용 모델
│ │ ├── basic/
│ │ │ ├── BasicRequest.java # 기본정보 요청
│ │ │ ├── BasicResponse.java # 기본정보 응답
│ │ │ └── VmisCarBassMatterInqireVO.java
│ │ ├── ledger/
│ │ │ ├── LedgerRequest.java # 등록원부 요청
│ │ │ ├── LedgerResponse.java # 등록원부 응답
│ │ │ ├── VmisCarLedgerFrmbkVO.java
│ │ │ └── VmisCarLedgerFrmbkDtlVO.java
│ │ └── common/
│ │ └── Envelope.java # 공통 Envelope
│ │
│ ├── config/
│ │ ├── HttpClientConfig.java # RestTemplate 설정
│ │ ├── OpenApiConfig.java # OpenAPI 설정
│ │ ├── PropertiesConfig.java # Properties 빈 등록
│ │ ├── GpkiConfig.java # GPKI 설정
│ │ └── Globals.java # 전역 상수
│ │
│ ├── controller/
│ │ └── VehicleInterfaceController.java # REST API 컨트롤러
│ │
│ ├── enricher/
│ │ └── VmisRequestEnricher.java # 요청 데이터 자동 보강
│ │
│ ├── gpki/
│ │ ├── GpkiService.java # GPKI 인터페이스
│ │ ├── RealGpkiService.java # 실제 GPKI 구현
│ │ └── NoopGpkiService.java # GPKI 비활성화 구현
│ │
│ └── util/
│ └── VehicleResponseMapper.java # 내부↔외부 VO 변환
└── external/ # External Mode (외부 API)
└── service/
├── ExternalVehicleInfoServiceImpl.java # 외부 모드 구현체 ⭐
└── ExternalVehicleApiService.java # 외부 API 호출 서비스
```
---
## 3. 아키텍처 패턴
### 3.1 Strategy Pattern (전략 패턴)
```java
// 공통 인터페이스
public interface VehicleInfoService {
VehicleApiResponseVO getVehicleInfo(String vehicleNumber);
List<VehicleApiResponseVO> getVehiclesInfo(List<String> vehicleNumbers);
}
// Internal Mode 구현체
@Service
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal")
public class InternalVehicleInfoServiceImpl implements VehicleInfoService {
// 내부 VMIS 모듈 직접 호출
}
// External Mode 구현체
@Service
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true)
public class ExternalVehicleInfoServiceImpl implements VehicleInfoService {
// 외부 REST API 호출
}
```
**장점:**
- 런타임에 구현체 전환 가능
- 클라이언트 코드는 인터페이스만 의존
- 새로운 전략 추가 시 기존 코드 수정 불필요
### 3.2 의존성 주입 (Dependency Injection)
```yaml
# application.yml
vmis:
integration:
mode: internal # 또는 external
```
Spring의 `@ConditionalOnProperty`를 통해 설정값에 따라 자동으로 적절한 구현체가 Bean으로 등록됩니다.
```java
@Autowired
private VehicleInfoService vehicleInfoService;
// 설정에 따라 InternalVehicleInfoServiceImpl 또는
// ExternalVehicleInfoServiceImpl이 주입됨
```
### 3.3 계층화 아키텍처
```
┌─────────────────────────────────────┐
│ Controller Layer │ ← REST API 엔드포인트
├─────────────────────────────────────┤
│ Service Layer (VehicleInfoService)│ ← 비즈니스 로직
├─────────────────────────────────────┤
│ Client/Mapper Layer │ ← 데이터 접근
├─────────────────────────────────────┤
│ External System (정부 API/DB) │ ← 외부 시스템
└─────────────────────────────────────┘
```
---
## 4. Internal Mode 상세
### 4.1 개요
내부 VMIS 모듈을 직접 사용하여 정부 시스템(MOLIT)과 통신합니다.
**장점:**
- 네트워크 오버헤드 감소 (외부 서버 호출 불필요)
- 빠른 응답 속도
- 단일 애플리케이션으로 운영 가능
**단점:**
- GPKI 인증서 관리 필요
- 정부 API 연동 복잡도 증가
### 4.2 주요 구성요소
#### 4.2.1 InternalVehicleInfoServiceImpl
```java
@Service
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal")
public class InternalVehicleInfoServiceImpl implements VehicleInfoService {
private final VmisCarBassMatterInqireService carBassMatterInqireService;
private final VmisCarLedgerFrmbkService carLedgerFrmbkService;
@Override
public VehicleApiResponseVO getVehicleInfo(String vehicleNumber) {
// 1. 기본정보 조회
VehicleBasicInfoVO basicInfo = getBasicInfo(vehicleNumber);
// 2. 등록원부 조회
VehicleLedgerVO ledgerInfo = getLedgerInfo(vehicleNumber);
// 3. 결과 통합
return VehicleApiResponseVO.builder()
.vhrno(vehicleNumber)
.basicInfo(basicInfo)
.ledgerInfo(ledgerInfo)
.build();
}
}
```
#### 4.2.2 GovernmentApiClient
정부 API와 실제 HTTP 통신을 수행합니다.
```java
@Component
@RequiredArgsConstructor
public class GovernmentApiClient implements GovernmentApi {
@Qualifier("vmisRestTemplate")
private final RestTemplate restTemplate;
@Override
public ResponseEntity<String> basic(String requestBody) {
String url = baseUrl + basicServicePath;
return restTemplate.postForEntity(url, requestBody, String.class);
}
}
```
#### 4.2.3 VmisRequestEnricher
요청 데이터를 자동으로 보강합니다.
```java
@Component
public class VmisRequestEnricher {
public void enrichBasicRequest(BasicRequest request) {
// 시스템 정보 자동 설정
request.setInfoSysId(vmisProperties.getSystem().getInfoSysId());
request.setInfoSysIp(vmisProperties.getSystem().getInfoSysIp());
request.setRegionCode(vmisProperties.getSystem().getRegionCode());
// ...
}
}
```
#### 4.2.4 GPKI 암호화
운영 환경에서 정부 API 통신 시 GPKI 암호화를 사용합니다.
```java
public interface GpkiService {
String encrypt(String plainText);
String decrypt(String encryptedText);
}
// 개발: NoopGpkiService (암호화 없음)
// 운영: RealGpkiService (실제 GPKI 암호화)
```
### 4.3 데이터 흐름
```
[Client]
[InternalVehicleInfoServiceImpl]
├─► [VmisCarBassMatterInqireService]
│ │
│ ├─► [VmisRequestEnricher] ──► Request 보강
│ ├─► [GovernmentApiClient] ──► HTTP POST
│ │ │
│ │ ▼
│ │ [정부 API - 기본정보]
│ │ │
│ │ ▼
│ ├─► [GpkiService] ──► 암호화/복호화
│ ├─► [VmisCarBassMatterInqireMapper] ──► DB 저장
│ └─► BasicResponse
├─► [VmisCarLedgerFrmbkService]
│ │
│ └─► (기본정보와 동일한 흐름)
└─► [VehicleResponseMapper] ──► 내부 VO → 외부 VO 변환
VehicleApiResponseVO
```
### 4.4 설정 예시
```yaml
vmis:
integration:
mode: internal
system:
infoSysId: "41-345"
infoSysIp: "105.19.10.135"
regionCode: "41460"
gov:
scheme: "http"
host: "10.188.225.94:29001"
basePath: "/piss/api/molit"
services:
basic:
path: "/SignguCarBassMatterInqireService"
apiKey: "${GOV_API_KEY_BASIC}"
ledger:
path: "/SignguCarLedgerFrmbkService"
apiKey: "${GOV_API_KEY_LEDGER}"
gpki:
enabled: "Y" # 운영: Y, 개발: N
certServerId: "SVR5640020001"
targetServerId: "SVR1500000015"
```
---
## 5. External Mode 상세
### 5.1 개요
외부 VMIS-interface 서버의 REST API를 호출하여 차량 정보를 조회합니다.
**장점:**
- 마이크로서비스 아키텍처 지원
- GPKI 관리를 외부 서버에 위임
- 간단한 설정
**단점:**
- 네트워크 오버헤드 증가
- 외부 서버 의존성
### 5.2 주요 구성요소
#### 5.2.1 ExternalVehicleInfoServiceImpl
```java
@Service
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true)
public class ExternalVehicleInfoServiceImpl implements VehicleInfoService {
private final ExternalVehicleApiService externalVehicleApiService;
@Override
public VehicleApiResponseVO getVehicleInfo(String vehicleNumber) {
// 외부 API 서비스에 위임
return externalVehicleApiService.getVehicleInfo(vehicleNumber);
}
}
```
#### 5.2.2 ExternalVehicleApiService
외부 REST API를 호출합니다.
```java
@Service
@RequiredArgsConstructor
public class ExternalVehicleApiService {
private final RestTemplate restTemplate;
public VehicleApiResponseVO getVehicleInfo(String vehicleNumber) {
String url = vmisProperties.getExternal().getApi().getUrl();
VehicleBasicRequestVO request = new VehicleBasicRequestVO();
request.setVhrno(vehicleNumber);
return restTemplate.postForObject(url + "/basic", request, VehicleApiResponseVO.class);
}
}
```
### 5.3 데이터 흐름
```
[Client]
[ExternalVehicleInfoServiceImpl]
[ExternalVehicleApiService]
[RestTemplate] ──► HTTP POST
[VMIS-interface Server] (외부)
├─► 정부 API 호출
├─► GPKI 암호화/복호화
├─► DB 저장
└─► VehicleApiResponseVO 반환
[Client]
```
### 5.4 설정 예시
```yaml
vmis:
integration:
mode: external
external:
api:
url: "http://localhost:8081/api/v1/vehicles"
connectTimeoutMillis: 5000
readTimeoutMillis: 10000
```
---
## 6. 공통 구성요소
### 6.1 VehicleInfoService 인터페이스
```java
public interface VehicleInfoService {
// 단일 차량 조회
VehicleApiResponseVO getVehicleInfo(String vehicleNumber);
// 일괄 조회
List<VehicleApiResponseVO> getVehiclesInfo(List<String> vehicleNumbers);
}
```
### 6.2 공통 VO (vo 패키지)
#### VehicleApiResponseVO
```java
@Data
@Builder
public class VehicleApiResponseVO {
private String vhrno; // 차량번호
private boolean success; // 성공 여부
private String message; // 메시지
private VehicleBasicInfoVO basicInfo; // 기본정보
private VehicleLedgerVO ledgerInfo; // 등록원부 정보
}
```
#### VehicleBasicInfoVO
차량 기본정보 (소유자, 차종, 연식 등)
#### VehicleLedgerVO
자동차 등록원부 정보 (등록일, 변경이력 등)
### 6.3 VmisProperties
application.yml의 설정을 바인딩하는 클래스입니다.
```java
@Data
@Component
@ConfigurationProperties(prefix = "vmis")
public class VmisProperties {
private Integration integration;
private System system;
private Gov gov;
private Gpki gpki;
private External external;
}
```
### 6.4 ApiMapperConfig
API 전용 MyBatis Mapper를 스캔합니다.
```java
@Configuration
@MapperScan(basePackages = "go.kr.project.api.internal.mapper")
public class ApiMapperConfig {
}
```
---
## 7. 데이터 흐름
### 7.1 Internal Mode 전체 흐름
```
┌─────────────────────────────────────────────────────────────────────┐
│ 1. Client Request │
│ vehicleInfoService.getVehicleInfo("12가3456") │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 2. InternalVehicleInfoServiceImpl │
│ - BasicRequest 생성 (vhrno="12가3456") │
│ - LedgerRequest 생성 (vhrno="12가3456") │
└────────────────────────┬────────────────────────────────────────────┘
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 3-1. Basic Service │ │ 3-2. Ledger Service │
│ - VmisRequestEnricher│ │ - VmisRequestEnricher│
│ (시스템 정보 자동 설정)│ │ (시스템 정보 자동 설정)│
└──────┬───────────────┘ └──────┬───────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 4-1. 정부 API 호출 │ │ 4-2. 정부 API 호출 │
│ - GovernmentApiClient│ │ - GovernmentApiClient│
│ - GPKI 암호화 (운영) │ │ - GPKI 암호화 (운영) │
│ - HTTP POST │ │ - HTTP POST │
└──────┬───────────────┘ └──────┬───────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 5-1. 응답 처리 │ │ 5-2. 응답 처리 │
│ - GPKI 복호화 │ │ - GPKI 복호화 │
│ - JSON 파싱 │ │ - JSON 파싱 │
│ - DB 저장 (Mapper) │ │ - DB 저장 (Mapper) │
└──────┬───────────────┘ └──────┬───────────────┘
│ │
└───────────────┬───────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 6. VehicleResponseMapper │
│ - BasicResponse → VehicleBasicInfoVO 변환 │
│ - LedgerResponse → VehicleLedgerVO 변환 │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 7. Response │
│ VehicleApiResponseVO { │
│ vhrno: "12가3456", │
│ success: true, │
│ basicInfo: { ... }, │
│ ledgerInfo: { ... } │
│ } │
└─────────────────────────────────────────────────────────────────────┘
```
### 7.2 External Mode 전체 흐름
```
┌─────────────────────────────────────────────────────────────────────┐
│ 1. Client Request │
│ vehicleInfoService.getVehicleInfo("12가3456") │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 2. ExternalVehicleInfoServiceImpl │
│ - ExternalVehicleApiService에 위임 │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 3. ExternalVehicleApiService │
│ - VehicleBasicRequestVO 생성 │
│ - RestTemplate으로 외부 API 호출 │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 4. HTTP Request to External Server │
│ POST http://localhost:8081/api/v1/vehicles/basic │
│ Body: { vhrno: "12가3456" } │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 5. VMIS-interface Server (외부) │
│ - 정부 API 호출 │
│ - GPKI 처리 │
│ - DB 저장 │
│ - VehicleApiResponseVO 반환 │
└────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 6. Response │
│ VehicleApiResponseVO { │
│ vhrno: "12가3456", │
│ success: true, │
│ basicInfo: { ... }, │
│ ledgerInfo: { ... } │
│ } │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 8. 설정 가이드
### 8.1 Internal Mode 설정
```yaml
vmis:
integration:
mode: internal # ⭐ Internal Mode 활성화
# 시스템 정보 (요청 헤더에 자동 포함)
system:
infoSysId: "41-345"
infoSysIp: "${SERVER_IP:105.19.10.135}"
regionCode: "41460"
departmentCode: ""
chargerId: ""
chargerIp: ""
chargerNm: ""
# 정부 API 연동 설정
gov:
scheme: "http"
host: "10.188.225.94:29001"
basePath: "/piss/api/molit"
connectTimeoutMillis: 5000
readTimeoutMillis: 10000
services:
basic:
path: "/SignguCarBassMatterInqireService"
cntcInfoCode: "AC1_FD11_01"
apiKey: "${GOV_API_KEY_BASIC}"
ledger:
path: "/SignguCarLedgerFrmbkService"
cntcInfoCode: "AC1_FD11_02"
apiKey: "${GOV_API_KEY_LEDGER}"
# GPKI 암호화 (운영 환경 필수)
gpki:
enabled: "Y" # Y: 암호화 사용, N: 암호화 미사용
certServerId: "SVR5640020001"
targetServerId: "SVR1500000015"
envCertFilePathName: "src/GPKI/certs/SVR5640020001_env.cer"
sigCertFilePathName: "src/GPKI/certs/SVR5640020001_sig.cer"
```
### 8.2 External Mode 설정
```yaml
vmis:
integration:
mode: external # ⭐ External Mode 활성화
# 외부 API 서버 설정
external:
api:
url: "http://localhost:8081/api/v1/vehicles"
connectTimeoutMillis: 5000
readTimeoutMillis: 10000
```
### 8.3 개발 환경 설정 (GPKI 비활성화)
```yaml
vmis:
integration:
mode: internal
gpki:
enabled: "N" # ⭐ GPKI 비활성화
```
---
## 9. 사용 예제
### 9.1 기본 사용법
```java
@RestController
@RequestMapping("/api/vehicles")
@RequiredArgsConstructor
public class VehicleController {
private final VehicleInfoService vehicleInfoService;
/**
* 단일 차량 조회
*/
@GetMapping("/{vehicleNumber}")
public ResponseEntity<VehicleApiResponseVO> getVehicle(
@PathVariable String vehicleNumber) {
VehicleApiResponseVO response = vehicleInfoService.getVehicleInfo(vehicleNumber);
if (response.isSuccess()) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.badRequest().body(response);
}
}
/**
* 일괄 조회
*/
@PostMapping("/batch")
public ResponseEntity<List<VehicleApiResponseVO>> getVehicles(
@RequestBody List<String> vehicleNumbers) {
List<VehicleApiResponseVO> responses =
vehicleInfoService.getVehiclesInfo(vehicleNumbers);
return ResponseEntity.ok(responses);
}
}
```
### 9.2 Service Layer에서 사용
```java
@Service
@RequiredArgsConstructor
public class InspectionService {
private final VehicleInfoService vehicleInfoService;
public void performInspection(String vehicleNumber) {
// 1. 차량 정보 조회
VehicleApiResponseVO vehicleInfo =
vehicleInfoService.getVehicleInfo(vehicleNumber);
if (!vehicleInfo.isSuccess()) {
throw new VehicleNotFoundException(vehicleNumber);
}
// 2. 차량 정보를 사용한 비즈니스 로직
VehicleBasicInfoVO basicInfo = vehicleInfo.getBasicInfo();
String ownerName = basicInfo.getVhrNm(); // 소유자명
String modelYear = basicInfo.getYridnw(); // 연식
// 3. 검사 수행
// ...
}
}
```
### 9.3 응답 데이터 활용
```java
VehicleApiResponseVO response = vehicleInfoService.getVehicleInfo("12가3456");
if (response.isSuccess()) {
// 기본정보 활용
VehicleBasicInfoVO basic = response.getBasicInfo();
System.out.println("차량번호: " + basic.getVhrno());
System.out.println("소유자: " + basic.getVhrNm());
System.out.println("차종: " + basic.getVhctyNm());
System.out.println("용도: " + basic.getVhclPrposNm());
// 등록원부 활용
VehicleLedgerVO ledger = response.getLedgerInfo();
System.out.println("최초등록일: " + ledger.getFrstRegistDe());
System.out.println("등록상태: " + ledger.getRegistSttusNm());
} else {
System.err.println("조회 실패: " + response.getMessage());
}
```
### 9.4 일괄 조회 예제
```java
List<String> vehicleNumbers = Arrays.asList(
"12가3456",
"34나5678",
"56다7890"
);
List<VehicleApiResponseVO> responses =
vehicleInfoService.getVehiclesInfo(vehicleNumbers);
// 성공/실패 분류
List<VehicleApiResponseVO> successful = responses.stream()
.filter(VehicleApiResponseVO::isSuccess)
.collect(Collectors.toList());
List<VehicleApiResponseVO> failed = responses.stream()
.filter(r -> !r.isSuccess())
.collect(Collectors.toList());
System.out.println("성공: " + successful.size());
System.out.println("실패: " + failed.size());
```
---
## 10. 모드 전환 시나리오
### 10.1 개발 → 운영 전환
```yaml
# 개발 환경 (application-local.yml)
vmis:
integration:
mode: internal
gpki:
enabled: "N" # GPKI 비활성화
```
```yaml
# 운영 환경 (application-prod.yml)
vmis:
integration:
mode: internal
gpki:
enabled: "Y" # GPKI 활성화
system:
infoSysIp: "${SERVER_IP}" # 실제 서버 IP
```
### 10.2 Monolithic → MSA 전환
```yaml
# Phase 1: 단일 애플리케이션 (Internal Mode)
vmis:
integration:
mode: internal
```
```yaml
# Phase 2: 마이크로서비스 (External Mode)
vmis:
integration:
mode: external
external:
api:
url: "http://vmis-service:8080/api/v1/vehicles"
```
**코드 변경 없이 설정만으로 전환 가능!**
---
## 11. 트러블슈팅
### 11.1 Bean 충돌 오류
**증상:**
```
expected single matching bean but found 2:
internalVehicleInfoServiceImpl, externalVehicleInfoServiceImpl
```
**원인:**
`vmis.integration.mode` 설정이 잘못되었거나 누락됨
**해결:**
```yaml
vmis:
integration:
mode: internal # 또는 external (반드시 지정)
```
### 11.2 Mapper 중복 스캔 경고
**증상:**
```
Skipping MapperFactoryBean with name 'vmisCarBassMatterInqireMapper'
Bean already defined with the same name!
```
**원인:**
여러 `@MapperScan`에서 같은 패키지를 중복 스캔
**해결:**
`ApiMapperConfig`에서 API 전용 Mapper만 스캔하도록 분리되어 있음
### 11.3 GPKI 오류 (운영 환경)
**증상:**
```
GPKI 초기화 실패: 인증서 파일을 찾을 수 없습니다
```
**해결:**
1. GPKI 인증서 파일 경로 확인
2. 개발 환경에서는 `gpki.enabled: "N"` 설정
---
## 12. 성능 최적화
### 12.1 Internal Mode 최적화
- RestTemplate Connection Pool 설정 활용
- GPKI 세션 재사용
- DB 커넥션 풀 튜닝
### 12.2 External Mode 최적화
- 외부 API 서버와 동일 네트워크 배치
- Circuit Breaker 패턴 적용 (향후)
- 캐싱 전략 (향후)
---
## 13. 보안 고려사항
### 13.1 GPKI 인증서 관리
- 인증서 파일 접근 권한 제한
- 패스워드는 환경변수로 관리
- 인증서 만료 모니터링
### 13.2 API Key 관리
```yaml
vmis:
gov:
services:
basic:
apiKey: "${GOV_API_KEY_BASIC}" # 환경변수 사용
```
### 13.3 로깅 주의사항
- 차량번호 등 개인정보는 마스킹 처리
- 요청/응답 전문은 DEBUG 레벨로 출력
---
## 14. 확장 가능성
### 14.1 새로운 모드 추가
Strategy Pattern을 사용하므로 새로운 구현체 추가 가능:
```java
@Service
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "cache")
public class CachedVehicleInfoServiceImpl implements VehicleInfoService {
// 캐시 기반 구현
}
```
### 14.2 새로운 차량 정보 조회 API 추가
1. 정부 API 인터페이스 메서드 추가
2. Service 계층에 메서드 추가
3. Mapper 및 Model 추가
4. VehicleInfoService 인터페이스 확장
---
## 15. 참고 자료
### 15.1 관련 설정 파일
- `src/main/resources/application.yml`
- `src/main/resources/mybatis/mapper/api-internal/`
### 15.2 관련 문서
- Spring Boot Conditional Beans
- Strategy Pattern
- MyBatis Integration
---
**문서 버전:** 1.0
**최종 수정일:** 2025-11-07
**작성자:** VIPS Development Team

@ -0,0 +1,813 @@
# Rocky Linux 9.5 MariaDB 설치 및 설정 가이드
## 요구사항
1. Rocky Linux 9.5 version
2. root 권한 또는 sudo 권한이 있는 계정
3. 목표: MariaDB 설치, 보안 설정, 사용자 생성, 데이터베이스 생성, 백업 스키마 복원
---
## 1단계: MariaDB 설치
### 1.1 MariaDB 서버 설치
```bash
# MariaDB 서버 및 클라이언트 설치
dnf install -y mariadb-server mariadb
# 또는 yum 사용
yum install -y mariadb-server mariadb
```
### 1.2 MariaDB 서비스 시작 및 자동 시작 설정
```bash
# MariaDB 서비스 시작
systemctl start mariadb
# 부팅 시 자동 시작 설정
systemctl enable mariadb
# 서비스 상태 확인
systemctl status mariadb
```
### 1.3 MariaDB 버전 확인
```bash
# MariaDB 버전 확인
mysql --version
# 또는 MariaDB 접속 후 확인
mysql -e "SELECT VERSION();"
```
---
## 2단계: MariaDB 보안 설정
### 2.1 초기 보안 설정 실행
```bash
# MariaDB 보안 설정 스크립트 실행
mysql_secure_installation
```
### 2.2 보안 설정 질문 및 권장 답변
설정 중 아래와 같은 질문이 나타납니다:
```
1. Enter current password for root (enter for none):
→ 처음 설치 시 비밀번호가 없으므로 Enter 입력
2. Switch to unix_socket authentication [Y/n]
→ n (기존 방식 유지 권장)
3. Change the root password? [Y/n]
→ Y (root 비밀번호 설정)
→ 새 비밀번호 입력 및 확인
4. Remove anonymous users? [Y/n]
→ Y (익명 사용자 제거)
5. Disallow root login remotely? [Y/n]
→ Y (원격 root 로그인 차단 - 보안 강화)
→ n (원격 접속이 필요한 경우)
6. Remove test database and access to it? [Y/n]
→ Y (테스트 데이터베이스 제거)
7. Reload privilege tables now? [Y/n]
→ Y (권한 테이블 즉시 적용)
```
### 2.3 root로 MariaDB 접속 확인
```bash
# root 계정으로 MariaDB 접속
mysql -u root -p
# 비밀번호 입력 후 접속
# 접속 확인 후 종료
MariaDB [(none)]> show databases;
MariaDB [(none)]> exit;
```
---
## 3단계: 커스텀 포트 설정 (53306)
### 3.1 MariaDB 포트 변경
```bash
# MariaDB 설정 파일 편집
vi /etc/my.cnf.d/mariadb-server.cnf
```
설정 파일에 포트 추가:
```ini
[mysqld]
port=53306
```
설정 변경 후 서비스 재시작:
```bash
# MariaDB 재시작
systemctl restart mariadb
# 포트 확인
ss -tlnp | grep 53306
```
### 3.2 방화벽 설정 (외부 접속 허용)
```bash
# 커스텀 포트(53306) 방화벽 허용
firewall-cmd --permanent --add-port=53306/tcp
firewall-cmd --reload
# 방화벽 설정 확인
firewall-cmd --list-all
```
---
## 4단계: 데이터베이스 생성
### 4.1 MariaDB 접속
```bash
# root로 MariaDB 접속
mysql -u root -p
# 비밀번호 입력
```
### 4.2 데이터베이스 생성
```sql
-- VIPS 데이터베이스 생성
CREATE DATABASE vips CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 데이터베이스 목록 확인
SHOW DATABASES;
-- VIPS 데이터베이스 선택
USE vips;
```
### 4.3 데이터베이스 문자셋 확인
```sql
-- 데이터베이스 문자셋 확인
SELECT
SCHEMA_NAME,
DEFAULT_CHARACTER_SET_NAME,
DEFAULT_COLLATION_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = 'vips';
```
---
## 5단계: 사용자 생성 및 권한 부여
### 5.1 로컬 접속용 사용자 생성
```sql
-- VIPS 로컬 접속용 사용자 생성
CREATE USER 'vips'@'localhost' IDENTIFIED BY 'xit5811807';
-- VIPS 데이터베이스에 모든 권한 부여
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'localhost';
-- 권한 적용
FLUSH PRIVILEGES;
```
### 5.2 원격 접속용 사용자 생성
```sql
-- 특정 IP에서 접속 가능한 사용자 생성
CREATE USER 'vips'@'192.168.1.100' IDENTIFIED BY 'xit5811807';
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'192.168.1.100';
-- 모든 IP에서 접속 가능한 사용자 생성 (application-prd.yml 설정)
CREATE USER 'vips'@'%' IDENTIFIED BY 'xit5811807';
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'%';
-- 권한 적용
FLUSH PRIVILEGES;
```
### 5.3 읽기 전용 사용자 생성 (선택사항)
```sql
-- 읽기 전용 사용자 생성
CREATE USER 'vips_readonly'@'%' IDENTIFIED BY 'vips_readonly_password';
GRANT SELECT ON vips.* TO 'vips_readonly'@'%';
FLUSH PRIVILEGES;
```
### 5.4 사용자 목록 및 권한 확인
```sql
-- 사용자 목록 확인
SELECT User, Host FROM mysql.user;
-- VIPS 사용자의 권한 확인
SHOW GRANTS FOR 'vips'@'localhost';
SHOW GRANTS FOR 'vips'@'%';
-- 현재 사용자 확인
SELECT USER(), CURRENT_USER();
```
### 5.5 원격 접속을 위한 MariaDB 설정 변경
```bash
# MariaDB 설정 파일 편집
vi /etc/my.cnf.d/mariadb-server.cnf
```
설정 파일에서 다음 부분 수정:
```ini
[mysqld]
# bind-address를 주석 처리하거나 0.0.0.0으로 변경
# bind-address=127.0.0.1
bind-address=0.0.0.0
# 또는 특정 IP만 허용
# bind-address=192.168.1.50
```
설정 변경 후 서비스 재시작:
```bash
# MariaDB 재시작
systemctl restart mariadb
# 재시작 확인
systemctl status mariadb
```
---
## 6단계: 백업 스키마 업로드 (전체 데이터베이스 복원)
### 6.1 백업 파일 서버로 전송
**방법 1: SCP 사용 (로컬 PC에서 실행)**
```bash
# Windows PowerShell 또는 CMD에서 실행
scp C:\backup\vips_backup.sql root@서버IP:/tmp/
```
**방법 2: SFTP 사용**
```bash
# SFTP 접속
sftp root@서버IP
# 파일 전송
put vips_backup.sql /tmp/
exit
```
### 6.2 백업 파일 권한 설정
```bash
# 백업 파일 권한 확인
ls -la /tmp/vips_backup.sql
# 필요시 권한 변경
chmod 644 /tmp/vips_backup.sql
```
### 6.3 백업 파일 복원 - 방법 1 (명령행에서 직접 복원)
```bash
# 커스텀 포트 53306 사용하여 복원
mysql -u root -p -P 53306 vips < /tmp/vips_backup.sql
# 비밀번호 입력 없이 실행 (스크립트용)
mysql -u root -p비밀번호 -P 53306 vips < /tmp/vips_backup.sql
```
### 6.4 백업 파일 복원 - 방법 2 (MySQL 내부에서 복원)
```bash
# MariaDB 접속 (포트 53306 명시)
mysql -u root -p -P 53306
```
```sql
-- VIPS 데이터베이스 선택
USE vips;
-- 백업 파일 실행
SOURCE /tmp/vips_backup.sql;
-- 또는
\. /tmp/vips_backup.sql
```
### 6.5 백업 파일 복원 - 방법 3 (새 데이터베이스 생성 포함)
백업 파일에 CREATE DATABASE가 포함된 경우:
```bash
# 데이터베이스 이름 지정 없이 복원 (포트 53306)
mysql -u root -p -P 53306 < /tmp/vips_backup.sql
```
### 6.6 대용량 백업 파일 복원
대용량 파일의 경우 타임아웃 방지를 위한 옵션 추가:
```bash
# 타임아웃 설정 증가 (포트 53306)
mysql -u root -p -P 53306 \
--max_allowed_packet=512M \
--connect_timeout=3600 \
--wait_timeout=3600 \
vips < /tmp/vips_backup.sql
```
### 6.7 압축된 백업 파일 복원
```bash
# .gz 파일 복원 (포트 53306)
gunzip < /tmp/vips_backup.sql.gz | mysql -u root -p -P 53306 vips
# .zip 파일 복원 (포트 53306)
unzip -p /tmp/vips_backup.sql.zip | mysql -u root -p -P 53306 vips
```
---
## 7단계: 복원 결과 확인
### 7.1 MariaDB 접속
```bash
# VIPS 사용자로 접속 (포트 53306)
mysql -u vips -p -P 53306 vips
# 비밀번호 입력: xit5811807
```
### 7.2 데이터베이스 및 테이블 확인
```sql
-- 현재 데이터베이스 확인
SELECT DATABASE();
-- 테이블 목록 확인
SHOW TABLES;
-- 테이블 개수 확인
SELECT COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema = 'vips';
-- 각 테이블의 상세 정보 확인
SHOW TABLE STATUS FROM vips;
```
### 7.3 테이블 구조 확인
```sql
-- 특정 테이블 구조 확인 (VIPS 테이블 예시)
DESC tb_vehicle_info;
-- 또는
DESCRIBE tb_vehicle_info;
-- 또는
SHOW COLUMNS FROM tb_vehicle_info;
-- 테이블 생성 DDL 확인
SHOW CREATE TABLE tb_vehicle_info;
```
### 7.4 데이터 확인
```sql
-- 특정 테이블의 레코드 수 확인
SELECT COUNT(*) FROM tb_vehicle_info;
-- 상위 10개 데이터 조회
SELECT * FROM tb_vehicle_info LIMIT 10;
-- 모든 테이블의 레코드 수 확인
SELECT
TABLE_NAME,
TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'vips'
ORDER BY TABLE_ROWS DESC;
```
### 7.5 인덱스 및 제약조건 확인
```sql
-- 특정 테이블의 인덱스 확인
SHOW INDEX FROM tb_vehicle_info;
-- 외래키 제약조건 확인
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'vips'
AND REFERENCED_TABLE_NAME IS NOT NULL;
```
### 7.6 데이터베이스 용량 확인
```sql
-- VIPS 데이터베이스 전체 크기 확인
SELECT
table_schema AS 'Database',
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema = 'vips'
GROUP BY table_schema;
-- 테이블별 크기 확인
SELECT
table_name AS 'Table',
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema = 'vips'
ORDER BY (data_length + index_length) DESC;
```
### 7.7 문자셋 확인
```sql
-- 데이터베이스 문자셋 확인
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
-- 포트 확인
SHOW VARIABLES LIKE 'port';
-- VIPS 테이블의 문자셋 확인
SELECT
TABLE_NAME,
TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'vips';
```
---
## 8단계: 접속 테스트 및 문제 해결
### 8.1 로컬 접속 테스트
```bash
# VIPS 사용자로 로컬 접속 테스트 (포트 53306)
mysql -u vips -p -P 53306 vips
# 접속 후 간단한 쿼리 실행
mysql -u vips -pxit5811807 -P 53306 vips -e "SELECT COUNT(*) AS table_count FROM information_schema.tables WHERE table_schema='vips';"
```
### 8.2 원격 접속 테스트 (다른 서버에서)
```bash
# 원격 서버에서 접속 테스트 (application-prd.yml 설정과 동일)
mysql -h 211.119.124.117 -P 53306 -u vips -p vips
# 비밀번호 포함하여 테스트 (스크립트용)
mysql -h 211.119.124.117 -P 53306 -u vips -pxit5811807 vips -e "SHOW TABLES;"
```
### 8.3 접속 문제 해결
**문제 1: Access denied for user**
```sql
-- MariaDB에 root로 접속하여 사용자 확인
SELECT User, Host FROM mysql.user WHERE User='vips';
-- 호스트가 일치하지 않으면 재생성
DROP USER 'vips'@'잘못된호스트';
CREATE USER 'vips'@'올바른호스트' IDENTIFIED BY 'xit5811807';
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'올바른호스트';
FLUSH PRIVILEGES;
```
**문제 2: Can't connect to MySQL server**
```bash
# MariaDB 서비스 상태 확인
systemctl status mariadb
# 서비스가 중지되어 있으면 시작
systemctl start mariadb
# 커스텀 포트 53306 리스닝 확인
ss -tlnp | grep 53306
netstat -tlnp | grep 53306
# 포트 설정 확인
mysql -u root -p -e "SHOW VARIABLES LIKE 'port';"
# 방화벽 확인
firewall-cmd --list-all
```
**문제 3: 원격 접속 불가**
```bash
# bind-address 설정 확인
grep bind-address /etc/my.cnf.d/mariadb-server.cnf
# 0.0.0.0 또는 원하는 IP로 설정되어 있는지 확인
# 변경 후 재시작 필요
systemctl restart mariadb
```
**문제 4: 복원 중 에러 발생**
```bash
# 에러 로그 확인
tail -100 /var/log/mariadb/mariadb.log
# 또는
journalctl -u mariadb -n 100
# SQL 파일 문법 확인 (일부만 실행해보기)
head -100 /tmp/myapp_db_backup.sql
```
---
## 9단계: 백업 설정 (추가 운영 관리)
### 9.1 수동 백업 명령어
```bash
# VIPS 전체 데이터베이스 백업 (포트 53306)
mysqldump -u root -p -P 53306 vips > /backup/vips_$(date +%Y%m%d_%H%M%S).sql
# 압축 백업
mysqldump -u root -p -P 53306 vips | gzip > /backup/vips_$(date +%Y%m%d_%H%M%S).sql.gz
# 특정 테이블만 백업
mysqldump -u root -p -P 53306 vips tb_vehicle_info tb_user > /backup/vips_tables_$(date +%Y%m%d).sql
# 구조만 백업 (데이터 제외)
mysqldump -u root -p -P 53306 --no-data vips > /backup/vips_schema_only.sql
# 데이터만 백업 (구조 제외)
mysqldump -u root -p -P 53306 --no-create-info vips > /backup/vips_data_only.sql
```
### 9.2 백업 디렉토리 생성 및 권한 설정
```bash
# 백업 디렉토리 생성
mkdir -p /backup/mariadb
# 권한 설정
chown root:root /backup/mariadb
chmod 700 /backup/mariadb
```
### 9.3 자동 백업 스크립트 생성
```bash
# 백업 스크립트 생성
vi /backup/mariadb_backup.sh
```
스크립트 내용:
```bash
#!/bin/bash
# VIPS 데이터베이스 백업 스크립트
DB_NAME="vips"
DB_USER="root"
DB_PASS="your_root_password"
DB_PORT="53306"
BACKUP_DIR="/backup/mariadb"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
RETENTION_DAYS=7
# 백업 디렉토리 생성 (없으면)
mkdir -p $BACKUP_DIR
# 백업 실행
echo "Starting VIPS database backup at $(date)"
mysqldump -u $DB_USER -p$DB_PASS -P $DB_PORT $DB_NAME | gzip > $BACKUP_FILE
# 백업 결과 확인
if [ $? -eq 0 ]; then
echo "Backup completed successfully: $BACKUP_FILE"
# 파일 크기 확인
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "Backup file size: $FILE_SIZE"
# 오래된 백업 파일 삭제 (7일 이상)
find $BACKUP_DIR -name "${DB_NAME}_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Old backups cleaned up (retention: $RETENTION_DAYS days)"
else
echo "Backup failed!"
exit 1
fi
echo "Backup process finished at $(date)"
```
스크립트 실행 권한 부여:
```bash
chmod 700 /backup/mariadb_backup.sh
```
### 9.4 cron을 이용한 자동 백업 설정
```bash
# crontab 편집
crontab -e
```
cron 설정 추가:
```bash
# 매일 새벽 2시에 백업 실행
0 2 * * * /backup/mariadb_backup.sh >> /backup/mariadb/backup.log 2>&1
# 매주 일요일 새벽 3시에 백업 실행
0 3 * * 0 /backup/mariadb_backup.sh >> /backup/mariadb/backup.log 2>&1
# 매월 1일 새벽 4시에 백업 실행
0 4 1 * * /backup/mariadb_backup.sh >> /backup/mariadb/backup.log 2>&1
```
cron 설정 확인:
```bash
# crontab 목록 확인
crontab -l
# cron 서비스 상태 확인
systemctl status crond
# cron 로그 확인
tail -f /var/log/cron
```
---
## 전체 실행 순서 요약
```bash
# 1. MariaDB 설치
dnf install -y mariadb-server mariadb
systemctl start mariadb
systemctl enable mariadb
# 2. 보안 설정
mysql_secure_installation
# 3. 커스텀 포트 설정 (53306)
vi /etc/my.cnf.d/mariadb-server.cnf
# [mysqld] 섹션에 port=53306 추가
# 4. 원격 접속 설정
vi /etc/my.cnf.d/mariadb-server.cnf
# bind-address=0.0.0.0 설정
systemctl restart mariadb
# 5. 방화벽 설정
firewall-cmd --permanent --add-port=53306/tcp
firewall-cmd --reload
# 6. 포트 확인
ss -tlnp | grep 53306
# 7. MariaDB 접속하여 데이터베이스 및 사용자 생성
mysql -u root -p -P 53306
```
```sql
-- VIPS 데이터베이스 생성
CREATE DATABASE vips CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- VIPS 사용자 생성 및 권한 부여
CREATE USER 'vips'@'localhost' IDENTIFIED BY 'xit5811807';
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'localhost';
-- 원격 접속용 사용자 (application-prd.yml 설정)
CREATE USER 'vips'@'%' IDENTIFIED BY 'xit5811807';
GRANT ALL PRIVILEGES ON vips.* TO 'vips'@'%';
FLUSH PRIVILEGES;
exit;
```
```bash
# 8. 백업 파일 업로드 (로컬 PC에서)
scp vips_backup.sql root@서버IP:/tmp/
# 9. 백업 복원 (서버에서, 포트 53306 명시)
mysql -u root -p -P 53306 vips < /tmp/vips_backup.sql
# 10. 복원 확인
mysql -u vips -pxit5811807 -P 53306 vips
```
```sql
-- 테이블 확인
SHOW TABLES;
SELECT COUNT(*) AS table_count FROM information_schema.tables WHERE table_schema='vips';
exit;
```
---
## 유용한 MariaDB 관리 명령어
### 데이터베이스 관리
```sql
-- 모든 데이터베이스 목록
SHOW DATABASES;
-- 데이터베이스 삭제 (주의!)
DROP DATABASE vips;
-- 데이터베이스 이름 변경 (직접 지원 안함, 백업/복원 필요)
-- 방법: 백업 → 새 DB 생성 → 복원 → 기존 DB 삭제
```
### 사용자 관리
```sql
-- 모든 사용자 목록
SELECT User, Host FROM mysql.user;
-- VIPS 사용자 비밀번호 변경
ALTER USER 'vips'@'localhost' IDENTIFIED BY 'new_password';
ALTER USER 'vips'@'%' IDENTIFIED BY 'new_password';
-- 사용자 삭제
DROP USER 'vips'@'localhost';
DROP USER 'vips'@'%';
-- 특정 권한만 회수
REVOKE DELETE ON vips.* FROM 'vips'@'localhost';
-- 모든 권한 회수
REVOKE ALL PRIVILEGES ON vips.* FROM 'vips'@'localhost';
```
### 테이블 관리
```sql
-- 테이블 삭제
DROP TABLE table_name;
-- 테이블 비우기 (구조는 유지)
TRUNCATE TABLE table_name;
-- 테이블 이름 변경
RENAME TABLE old_name TO new_name;
-- 테이블 복사
CREATE TABLE new_table LIKE old_table;
INSERT INTO new_table SELECT * FROM old_table;
```
### 성능 모니터링
```sql
-- 현재 실행 중인 쿼리 확인
SHOW PROCESSLIST;
-- 특정 프로세스 강제 종료
KILL [process_id];
-- 상태 변수 확인
SHOW STATUS;
SHOW VARIABLES;
-- 느린 쿼리 확인
SHOW VARIABLES LIKE 'slow_query%';
```
---
## 보안 권장사항
1. **강력한 비밀번호 사용**
- 최소 12자 이상, 대소문자/숫자/특수문자 조합
2. **최소 권한 원칙**
- 애플리케이션 사용자에게 필요한 최소 권한만 부여
- 읽기 전용 작업에는 SELECT 권한만 부여
3. **원격 root 접속 차단**
- root는 localhost에서만 접속 허용
4. **정기적인 백업**
- 매일 자동 백업 설정
- 백업 파일 주기적 확인
5. **방화벽 설정**
- 필요한 IP만 53306 포트 접근 허용
- 특정 IP 대역만 접근 허용 설정
```bash
# 특정 IP만 허용 (예시)
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="53306" accept'
firewall-cmd --reload
```
6. **MariaDB 업데이트**
- 정기적인 보안 패치 적용
```bash
dnf update mariadb-server
```
---
## 참고사항
- **VIPS 프로젝트 설정**
- 데이터베이스: vips
- 사용자: vips
- 비밀번호: xit5811807
- 포트: 53306 (커스텀 포트)
- 문자셋: UTF-8 (utf8mb4)
- **MariaDB 기본 정보**
- 기본 포트: 3306 (VIPS는 53306 사용)
- 설정 파일 위치: `/etc/my.cnf`, `/etc/my.cnf.d/mariadb-server.cnf`
- 데이터 디렉토리: `/var/lib/mysql/`
- 로그 파일: `/var/log/mariadb/mariadb.log`
- 소켓 파일: `/var/lib/mysql/mysql.sock`
---
## 문의 및 추가 정보
- MariaDB 공식 문서: https://mariadb.com/kb/
- Rocky Linux 공식 문서: https://docs.rockylinux.org/

@ -14,31 +14,35 @@
### 1.1 그룹 생성
```bash
# 애플리케이션용 그룹 생성 (예: appgroup)
groupadd appgroup
# 애플리케이션용 그룹 생성 (예: xit)
groupadd xit
```
### 1.2 사용자 계정 생성
```bash
# 사용자 생성 (예: appuser)
# 사용자 생성 (예: xit)
# -m: 홈 디렉토리 자동 생성
# -g: 기본 그룹 지정
# -s: 기본 쉘 지정
useradd -m -g appgroup -s /bin/bash appuser
useradd -m -g xit -s /bin/bash xit
```
### 1.3 사용자 비밀번호 설정
```bash
# 비밀번호 설정
passwd appuser
# 프롬프트가 나오면 비밀번호를 두 번 입력
# xit 사용자 비밀번호 설정
passwd xit
# 프롬프트가 나오면 "xit5811807"을 입력합니다.
# root 사용자 비밀번호 설정 (필요시)
passwd root
# 프롬프트가 나오면 "xit5811807"을 입력합니다.
```
### 1.4 사용자 생성 확인
```bash
# 사용자 정보 확인
id appuser
# 출력 예시: uid=1001(appuser) gid=1001(appgroup) groups=1001(appgroup)
id xit
# 출력 예시: uid=1001(xit) gid=1001(xit) groups=1001(xit)
```
---
@ -124,8 +128,8 @@ ls -la /usr/lib/jvm/
### 3.1 사용자 profile 설정 파일 편집
```bash
# appuser의 .bash_profile 편집
vi /home/appuser/.bash_profile
# xit의 .bash_profile 편집
vi /home/xit/.bash_profile
```
### 3.2 .bash_profile 내용 추가
@ -138,7 +142,7 @@ export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PATH=$JAVA_HOME/bin:$PATH
# 애플리케이션 관련 환경 변수
export APP_HOME=/app
export APP_HOME=/app/VIPS
export PATH=$APP_HOME:$PATH
# umask 설정 (파일 생성시 775 권한)
@ -153,7 +157,7 @@ export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export PATH=$JAVA_HOME/bin:$PATH
# 애플리케이션 관련 환경 변수
export APP_HOME=/app
export APP_HOME=/app/VIPS
export PATH=$APP_HOME:$PATH
# umask 설정 (파일 생성시 775 권한)
@ -169,8 +173,8 @@ umask 002
### 3.3 .bashrc 파일도 설정 (선택사항)
```bash
# appuser의 .bashrc 편집
vi /home/appuser/.bashrc
# xit의 .bashrc 편집
vi /home/xit/.bashrc
```
파일 끝에 추가:
@ -179,24 +183,24 @@ vi /home/appuser/.bashrc
umask 002
# alias 설정 (편의용)
alias ll='ls -la'
alias app='cd /app'
alias ll=\'ls -la\'
alias app=\'cd /app/VIPS\'
```
### 3.4 설정 적용
```bash
# 설정 즉시 적용 (root에서 실행 시)
su - appuser -c "source ~/.bash_profile"
su - xit -c "source ~/.bash_profile"
# 또는 appuser로 전환 후 실행
su - appuser
# 또는 xit로 전환 후 실행
su - xit
source ~/.bash_profile
```
### 3.5 환경 변수 확인
```bash
# appuser로 전환
su - appuser
# xit로 전환
su - xit
# 환경 변수 확인
echo $JAVA_HOME
@ -216,33 +220,33 @@ umask
```bash
# root 계정으로 실행
# 애플리케이션 디렉토리 생성
mkdir -p /app
mkdir -p /app/logs
mkdir -p /app/config
mkdir -p /app/backup
mkdir -p /app/VIPS
mkdir -p /app/VIPS/logs
mkdir -p /app/VIPS/config
mkdir -p /app/VIPS/backup
```
### 4.2 디렉토리 소유권 변경
```bash
# appuser:appgroup 소유권으로 변경
chown -R appuser:appgroup /app
# xit:xit 소유권으로 변경
chown -R xit:xit /app/VIPS
```
### 4.3 디렉토리 권한 설정
```bash
# 775 권한 설정 (소유자/그룹: rwx, 기타: r-x)
chmod -R 775 /app
chmod -R 775 /app/VIPS
```
### 4.4 권한 확인
```bash
# 디렉토리 권한 및 소유권 확인
ls -la /app
ls -la /app/VIPS
# 출력 예시:
# drwxrwxr-x. 4 appuser appgroup 4096 Nov 19 10:00 .
# drwxrwxr-x. 2 appuser appgroup 4096 Nov 19 10:00 logs
# drwxrwxr-x. 2 appuser appgroup 4096 Nov 19 10:00 config
# drwxrwxr-x. 2 appuser appgroup 4096 Nov 19 10:00 backup
# drwxrwxr-x. 4 xit xit 4096 Nov 19 10:00 .
# drwxrwxr-x. 2 xit xit 4096 Nov 19 10:00 logs
# drwxrwxr-x. 2 xit xit 4096 Nov 19 10:00 config
# drwxrwxr-x. 2 xit xit 4096 Nov 19 10:00 backup
```
---
@ -254,43 +258,43 @@ ls -la /app
**방법 1: SCP 사용 (로컬 PC에서 실행)**
```bash
# Windows PowerShell 또는 CMD에서 실행
scp C:\path\to\your-app.jar appuser@서버IP:/app/
scp C:\path\to\VIPS-BOOT.war xit@서버IP:/app/VIPS/
```
**방법 2: SFTP 사용**
```bash
# SFTP 접속
sftp appuser@서버IP
sftp xit@서버IP
# 파일 전송
put your-app.jar /app/
put VIPS-BOOT.war /app/VIPS/
exit
```
**방법 3: 서버에서 직접 다운로드 (URL이 있는 경우)**
```bash
# appuser로 전환
su - appuser
# xit로 전환
su - xit
# 파일 다운로드
cd /app
curl -O http://your-server/your-app.jar
cd /app/VIPS
curl -O http://your-server/VIPS-BOOT.war
# 또는
wget http://your-server/your-app.jar
wget http://your-server/VIPS-BOOT.war
```
### 5.2 배포된 파일 확인
```bash
# appuser로 전환
su - appuser
# xit로 전환
su - xit
# 파일 확인
ls -la /app/
ls -la /app/VIPS/
```
### 5.3 실행 권한 부여 (필요시)
```bash
chmod 775 /app/your-app.jar
chmod 775 /app/VIPS/VIPS-BOOT.war
```
---
@ -299,23 +303,26 @@ chmod 775 /app/your-app.jar
### 6.1 기본 실행
```bash
# appuser로 전환
su - appuser
# xit로 전환
su - xit
# 애플리케이션 실행
cd /app
java -jar your-app.jar
cd /app/VIPS
java -jar VIPS-BOOT.war
```
### 6.2 백그라운드 실행 (터미널 종료 후에도 계속 실행)
```bash
# nohup으로 백그라운드 실행
nohup java -jar /app/your-app.jar > /app/logs/app.log 2>&1 &
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war > /app/VIPS/logs/app.log 2>&1 &
# 실행로그도 안남김.. 서버 부팅 로그 확인 이후
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war > /dev/null 2>&1 &
# 또는 출력 분리
nohup java -jar /app/your-app.jar \
1>/app/logs/stdout.log \
2>/app/logs/stderr.log &
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war \
1>/app/VIPS/logs/stdout.log \
2>/app/VIPS/logs/stderr.log &
```
### 6.3 JVM 옵션과 함께 실행
@ -324,19 +331,19 @@ nohup java -jar /app/your-app.jar \
nohup java \
-Xms512m \
-Xmx1024m \
-jar /app/your-app.jar \
-jar /app/VIPS/VIPS-BOOT.war \
--spring.profiles.active=prod \
> /app/logs/app.log 2>&1 &
> /app/VIPS/logs/app.log 2>&1 &
```
### 6.4 프로세스 ID 확인
```bash
# 실행 직후 PID 저장
echo $! > /app/app.pid
echo $! > /app/VIPS/app.pid
# 또는 프로세스 검색
ps -ef | grep java
ps -ef | grep your-app.jar
ps -ef | grep VIPS-BOOT.war
```
---
@ -348,40 +355,40 @@ ps -ef | grep your-app.jar
# Java 프로세스 확인
ps -ef | grep java
# 특정 포트 확인 (예: 8080)
ss -tlnp | grep 8080
# 특정 포트 확인 (예: 18080)
ss -tlnp | grep 18080
# 또는
netstat -tlnp | grep 8080
netstat -tlnp | grep 18080
```
### 7.2 로그 확인
```bash
# 실시간 로그 확인
tail -f /app/logs/app.log
tail -f /app/VIPS/logs/app.log
# 최근 100줄 확인
tail -100 /app/logs/app.log
tail -100 /app/VIPS/logs/app.log
# 로그에서 에러 검색
grep -i error /app/logs/app.log
grep -i exception /app/logs/app.log
grep -i error /app/VIPS/logs/app.log
grep -i exception /app/VIPS/logs/app.log
```
### 7.3 애플리케이션 응답 테스트
```bash
# HTTP 응답 확인 (포트 8080 예시)
curl -I http://localhost:8080
# HTTP 응답 확인 (포트 18080 예시)
curl -I http://localhost:18080
# API 엔드포인트 테스트
curl http://localhost:8080/actuator/health
curl http://localhost:8080/api/test
curl http://localhost:18080/actuator/health
curl http://localhost:18080/api/test
```
### 7.4 방화벽 설정 (외부 접속 허용)
```bash
# root 계정으로 실행
# 포트 8080 방화벽 허용
firewall-cmd --permanent --add-port=8080/tcp
# 포트 18080 방화벽 허용
firewall-cmd --permanent --add-port=18080/tcp
firewall-cmd --reload
# 방화벽 상태 확인
@ -395,10 +402,10 @@ firewall-cmd --list-all
### 8.1 애플리케이션 중지
```bash
# PID 파일로 중지
kill $(cat /app/app.pid)
kill $(cat /app/VIPS/app.pid)
# 또는 프로세스 검색 후 중지
ps -ef | grep your-app.jar
ps -ef | grep VIPS-BOOT.war
kill [PID번호]
# 강제 종료 (응답 없을 때)
@ -408,22 +415,23 @@ kill -9 [PID번호]
### 8.2 애플리케이션 재시작
```bash
# 중지 후 시작
kill $(cat /app/app.pid)
kill $(cat /app/VIPS/app.pid)
sleep 3
nohup java -jar /app/your-app.jar > /app/logs/app.log 2>&1 &
echo $! > /app/app.pid
# nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war > /app/VIPS/logs/app.log 2>&1 &
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war > /dev/null 2>&1 &
echo $! > /app/VIPS/app.pid
```
### 8.3 시작 스크립트 생성 (선택사항)
```bash
vi /app/start.sh
vi /app/VIPS/start.sh
```
스크립트 내용:
```bash
#!/bin/bash
APP_NAME="your-app.jar"
APP_HOME="/app"
APP_NAME="VIPS-BOOT.war"
APP_HOME="/app/VIPS"
LOG_DIR="$APP_HOME/logs"
PID_FILE="$APP_HOME/app.pid"
@ -438,25 +446,26 @@ fi
# 애플리케이션 시작
echo "Starting application..."
nohup java -Xms512m -Xmx1024m -jar $APP_HOME/$APP_NAME > $LOG_DIR/app.log 2>&1 &
# nohup java -Xms512m -Xmx1024m -jar -Dspring.profiles.active=prd $APP_HOME/$APP_NAME > $LOG_DIR/app.log 2>&1 &
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/$APP_HOME/$APP_NAME > /dev/null 2>&1 &
echo $! > $PID_FILE
echo "Application started (PID: $(cat $PID_FILE))"
```
스크립트 실행 권한 부여:
```bash
chmod 775 /app/start.sh
chmod 775 /app/VIPS/start.sh
```
### 8.4 중지 스크립트 생성 (선택사항)
```bash
vi /app/stop.sh
vi /app/VIPS/stop.sh
```
스크립트 내용:
```bash
#!/bin/bash
PID_FILE="/app/app.pid"
PID_FILE="/app/VIPS/app.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat $PID_FILE)
@ -481,7 +490,7 @@ fi
스크립트 실행 권한 부여:
```bash
chmod 775 /app/stop.sh
chmod 775 /app/VIPS/stop.sh
```
---
@ -491,29 +500,29 @@ chmod 775 /app/stop.sh
### 9.1 서비스 파일 생성
```bash
# root 계정으로 실행
vi /etc/systemd/system/myapp.service
vi /etc/systemd/system/vips.service
```
서비스 파일 내용:
```ini
[Unit]
Description=My Spring Boot Application
Description=VIPS Spring Boot Application
After=network.target
# 600초(10분) 동안의 재시작 횟수를 카운트, 최대 5번까지만 재시작 시도
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/app
ExecStart=/usr/lib/jvm/java-17-openjdk/bin/java -Xms512m -Xmx1024m -jar /app/your-app.jar
ExecStop=/bin/kill -15 $MAINPID
User=xit
Group=xit
WorkingDirectory=/app/VIPS
# ExecStart=/usr/lib/jvm/java-1.8.0-openjdk/bin/java -Xms512m -Xmx1024m -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war
ExecStart=/usr/lib/jvm/java-1.8.0-openjdk/bin/java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war
# ExecStop=/bin/kill -15 $MAINPID
Restart=on-failure
RestartSec=10
# 로그 설정
StandardOutput=append:/app/logs/app.log
StandardError=append:/app/logs/error.log
[Install]
WantedBy=multi-user.target
```
@ -524,28 +533,28 @@ WantedBy=multi-user.target
systemctl daemon-reload
# 서비스 시작
systemctl start myapp
systemctl start vips
# 부팅 시 자동 시작 설정
systemctl enable myapp
systemctl enable vips
```
### 9.3 서비스 관리 명령어
```bash
# 상태 확인
systemctl status myapp
systemctl status vips
# 시작
systemctl start myapp
systemctl start vips
# 중지
systemctl stop myapp
systemctl stop vips
# 재시작
systemctl restart myapp
systemctl restart vips
# 로그 확인
journalctl -u myapp -f
journalctl -u vips -f
```
---
@ -554,34 +563,34 @@ journalctl -u myapp -f
```bash
# 1. root로 로그인 후 그룹/사용자 생성
groupadd appgroup
useradd -m -g appgroup -s /bin/bash appuser
passwd appuser
groupadd xit
useradd -m -g xit -s /bin/bash xit
passwd xit
# 2. Java 설치
dnf install -y java-17-openjdk java-17-openjdk-devel
dnf install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel
# 3. 환경 변수 설정
vi /home/appuser/.bash_profile
vi /home/xit/.bash_profile
# (JAVA_HOME, PATH, umask 설정 추가)
# 4. 디렉토리 생성 및 권한 설정
mkdir -p /app/logs /app/config /app/backup
chown -R appuser:appgroup /app
chmod -R 775 /app
mkdir -p /app/VIPS/logs /app/VIPS/config /app/VIPS/backup
chown -R xit:xit /app/VIPS
chmod -R 775 /app/VIPS
# 5. 소스 배포 (외부에서 파일 전송)
# scp your-app.jar appuser@서버IP:/app/
# scp VIPS-BOOT.war xit@서버IP:/app/VIPS/
# 6. appuser로 전환 후 실행
su - appuser
cd /app
nohup java -jar your-app.jar > /app/logs/app.log 2>&1 &
# 6. xit로 전환 후 실행
su - xit
cd /app/VIPS
nohup java -jar VIPS-BOOT.war > /app/VIPS/logs/app.log 2>&1 &
# 7. 실행 확인
ps -ef | grep java
tail -f /app/logs/app.log
curl http://localhost:8080/actuator/health
tail -f /app/VIPS/logs/app.log
curl http://localhost:18080/actuator/health
```
---
@ -602,8 +611,8 @@ echo $PATH
### 포트가 이미 사용 중인 경우
```bash
# 포트 사용 프로세스 확인
ss -tlnp | grep 8080
lsof -i :8080
ss -tlnp | grep 18080
lsof -i :18080
# 해당 프로세스 종료
kill [PID]
@ -612,26 +621,26 @@ kill [PID]
### 권한 오류 발생 시
```bash
# 파일 권한 확인
ls -la /app/
ls -la /app/VIPS/
# 권한 재설정
chown -R appuser:appgroup /app
chmod -R 775 /app
chown -R xit:xit /app/VIPS
chmod -R 775 /app/VIPS
```
### 로그 파일이 생성되지 않는 경우
```bash
# 로그 디렉토리 권한 확인
ls -la /app/logs
ls -la /app/VIPS/logs
# 디렉토리가 없으면 생성
mkdir -p /app/logs
chown appuser:appgroup /app/logs
chmod 775 /app/logs
mkdir -p /app/VIPS/logs
chown xit:xit /app/VIPS/logs
chmod 775 /app/VIPS/logs
```
### 메모리 부족 오류 (OutOfMemoryError)
```bash
# JVM 메모리 옵션 조정
java -Xms1g -Xmx2g -jar your-app.jar
```
java -Xms1g -Xmx2g -jar VIPS-BOOT.war
```

@ -0,0 +1,23 @@
자동차 검사 경과 안내서 대상차량 대장
---------------------------------------
프로그램 ID : VGD01B
처 리 일 자 : 2025년07월11일~2025년07월11일 출 력 일 시 : 2025년12월04일17시01분
---------------------------------------------------------------------------------------------------------------------------------------------------------------
번호 자동차번호 소유자명 주민등록번호 차 명 사용 본거지 주소 검사유효기간
---------------------------------------------------------------------------------------------------------------------------------------------------------------
1 11라7770 정배영 카이엔 경기도 용인시 기흥구 연원로42번길 10 2023-07-12~2025-07-11
810301-1177777 126동 302호(마북동, 블루밍 구성 더센트럴)
2 15저5555 이유희 스파크 1.0 DOHC 경기도 용인시 기흥구 이현로29번길 72-33 2023-07-12~2025-07-11
810306-1637778 132동 902호(보정동, 성호샤인힐즈아파트)
3 22소5555 박윤성 쏘렌토 경기도 용인시 수지구 죽전로 155 2023-07-12~2025-07-11
670406-1777778 5층(죽전동)
4 33고3355 주식회사 올놀카(상품용) C220 d 4Matic 경기도 용인시 기흥구 중부대로 255 2023-07-12~2025-07-11
134511-0327770 A동 3층 S367호(영덕동, 오토허브)
5 65조4444 정문치 레이 경기도 용인시 기흥구 흥덕4로15번길 23-55 2021-07-12~2025-07-11
791203-1067771 304호(영덕동)
6 88조8888 고영유 그랜저(GRANDEUR) 경기도 용인시 수지구 성복2로 555 2023-07-12~2025-07-11
900426-1087772 2층 204호(성복동)
7 99모9999 주윤산업(주) 포터Ⅱ(PORTERⅡ) 경기도 용인시 기흥구 농서로153번길 55-1 2024-07-12~2025-07-11
134511-0347779 (농서동)
8 경기99아9999 (주)삼성에우앤유 마이티 경기도 용인시 수지구 손곡로 55 2024-07-12~2025-07-11
130111-0007776 (동천동)

@ -1,25 +0,0 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-11-01 경기11사2222 행주운수(주) 1111110081111 엠뱅크언더리프 특수차구난형소영업용 2021-01-05 1761 30만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H500 2025-11-01 22고2222 주식회사 아일공행산업 1111106811111 그랜드 스타렉스 화물차밴형소형자가용 2025-04-28 187 60만원
2025-10-01 경기도 용인시 처인구 포곡읍 포곡로 222-2, 202호 2025-03-28
H692 2025-11-01 33마3333 홍길동 7604092328316 SM6 승용차일반형중자가용 2025-07-14 110 56만원
2025-09-22 경기도 용인시 기흥구 관곡로 53, 605동 1802호(구갈동, 가현마을신안아파트) 2025-06-11
H271 2025-11-01 44구4444 제제제이엔지 주식회사 1111110064044 봉고Ⅲ 1톤 화물차일반형- 자가용 2025-08-25 68 28만원
2020-05-20 경기도 용인시 처인구 포곡읍 에버랜드로 444(0-44동(4층)) 2025-07-24
H420 2025-11-01 55서5555 김철수 5555261080555 아이오닉6 (IONI 승용차일반형중자가용 2025-08-25 68 28만원
2024-09-09 경기도 용인시 수지구 성복1로 55, 505동 505호(성오동, 성오역 서피오타치오) 2025-07-25

@ -1,25 +0,0 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-11-01 경기11사2222 행주운수(주) 1111110081111 엠뱅크언더리프 특수차구난형소영업용 2021-01-05 1761 30만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H500 2025-11-01 22고2222 주식회사 아일공행산업 111110681111 그랜드 스타렉스 화물차밴형소형자가용 2025-04-28 187 60만원
2025-10-01 경기도 용인시 처인구 포곡읍 포곡로 222-2, 202호 2025-03-28
H692 2025-11-01 33마3333 홍길동 7604092328316 SM6 승용차일반형중자가용 2025-07-14 110 56만원
2025-09-22 경기도 용인시 기흥구 관곡로 53, 605동 1802호(구갈동, 가현마을신안아파트) 2025-06-11
H271 2025-11-01 44구4444 제제제이엔지 주식회사 1111110064044 봉고Ⅲ 1톤 화물차일반형- 자가용 2025-08-25 68 28만원
2020-05-20 경기도 용인시 처인구 포곡읍 에버랜드로 444(0-44동(4층)) 2025-07-24
H420 2025-11-01 55서5555 김철수 5555261080555 아이오닉6 (IONI 승용차일반형중자가용 2025-08-25 68 28만원
2024-09-09 경기도 용인시 수지구 성복1로 55, 505동 505호(성오동, 성오역 서피오타치오) 2025-07-25

@ -0,0 +1,252 @@
# VIPS 서버 관리 가이드
## 목차
- [1. 서버 접속](#1-서버-접속)
- [1.1 최초 터미널 접속](#11-최초-터미널-접속)
- [1.2 root 계정 접속](#12-root-계정-접속)
- [1.3 xit 계정 접속](#13-xit-계정-접속)
- [2. MariaDB 접속](#2-mariadb-접속)
- [3. WAR 파일 배포](#3-war-파일-배포)
- [4. 애플리케이션 실행](#4-애플리케이션-실행)
- [4.1 소스 위치 정보](#41-소스-위치-정보)
- [4.2 직접 실행](#42-직접-실행)
- [4.3 백그라운드 실행](#43-백그라운드-실행)
- [4.4 백그라운드 프로세스 중지](#44-백그라운드-프로세스-중지)
- [5. Systemd 서비스 설정](#5-systemd-서비스-설정)
- [5.1 서비스 파일 설정](#51-서비스-파일-설정)
- [5.2 Systemd 명령어](#52-systemd-명령어)
- [6. MariaDB 데이터베이스 백업](#6-mariadb-데이터베이스-백업)
- [6.1 백업 디렉토리 생성](#61-백업-디렉토리-생성)
- [6.2 백업 스크립트 생성](#62-백업-스크립트-생성)
- [6.3 Crontab 설정 (매일 밤 12시 자동 백업)](#63-crontab-설정-매일-밤-12시-자동-백업)
- [6.4 백업 관련 명령어](#64-백업-관련-명령어)
---
## 1. 서버 접속
### 1.1 최초 터미널 접속
PuTTY를 이용하여 자동차 검사 서버에 접속합니다.
```bash
id: tune
password: dyddls12# # 한글명: 용인12#
```
### 1.2 root 계정 접속
```bash
su -
password: dyddls12# # 한글명: 용인12#
```
### 1.3 xit 계정 접속
```bash
su - xit
password: xit5811807
```
---
## 2. MariaDB 접속
리눅스 root 계정에서 실행합니다.
```bash
mysql -u vips -p -P 53306
password: xit5811807 # mariadb root 계정 비밀번호도 동일
```
```sql
use vips;
show tables;
```
---
## 3. WAR 파일 배포
Windows에서 Linux로 SCP를 이용하여 소스를 업로드합니다.
**Windows CMD에서 실행:**
```cmd
scp -P1922 c:\VIPS\VIPS-BOOT.war xit@105.19.10.124:/app/VIPS/
password: xit5811807
```
---
## 4. 애플리케이션 실행
리눅스 xit 계정에서 실행합니다.
### 4.1 소스 위치 정보
```bash
cd /app/VIPS/ # 애플리케이션 디렉토리
cd /app/VIPS/logs/ # 로그 디렉토리
```
### 4.2 직접 실행
실시간 로그 확인 가능, `Ctrl+C`로 즉시 중지
```bash
java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war
```
### 4.3 백그라운드 실행
```bash
nohup java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war > /dev/null 2>&1 &
```
### 4.4 백그라운드 프로세스 중지
```bash
# 프로세스 확인
ps -ef | grep VIPS
# PID 확인 후 종료
kill -9 [PID]
```
---
## 5. Systemd 서비스 설정
자동 재기동을 위한 systemd 서비스 설정입니다. **root 계정**에서 실행합니다.
### 5.1 서비스 파일 설정
```bash
vi /etc/systemd/system/vips.service
```
**서비스 파일 내용:**
```ini
[Unit]
Description=VIPS Spring Boot Application
After=network.target
# 600초(10분) 동안의 재시작 횟수를 카운트, 최대 5번까지만 재시작 시도
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=simple
User=xit
Group=xit
WorkingDirectory=/app/VIPS
# ExecStart=/usr/lib/jvm/java-1.8.0-openjdk/bin/java -Xms512m -Xmx1024m -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war
ExecStart=/usr/lib/jvm/java-1.8.0-openjdk/bin/java -Dspring.profiles.active=prd -jar /app/VIPS/VIPS-BOOT.war
# ExecStop=/bin/kill -15 $MAINPID
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
```
### 5.2 Systemd 명령어
```bash
# systemd 데몬 리로드 (서비스 파일 수정 후 실행)
systemctl daemon-reload
# 부팅 시 자동 시작 설정
systemctl enable vips
# 상태 확인
systemctl status vips
# 서비스 시작
systemctl start vips
# 서비스 중지
systemctl stop vips
# 서비스 재시작
systemctl restart vips
# 실시간 로그 확인
journalctl -u vips -f
```
---
## 6. MariaDB 데이터베이스 백업
**root 계정**에서 실행합니다.
### 6.1 백업 디렉토리 생성
```bash
mkdir -p /app/db/backup
chmod 755 /app/db/backup
```
### 6.2 백업 스크립트 생성
```bash
vi /app/db/backup/backup_vips.sh
```
**스크립트 내용:**
```bash
#!/bin/bash
# 백업 설정
BACKUP_DIR="/app/db/backup"
DB_NAME="vips"
DB_USER="vips"
DB_PASSWORD="xit5811807"
DB_PORT="53306"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql"
# 백업 실행
mysqldump -u ${DB_USER} -p${DB_PASSWORD} -P ${DB_PORT} ${DB_NAME} > ${BACKUP_FILE}
# 백업 파일 압축
gzip ${BACKUP_FILE}
# 30일 이상 된 백업 파일 삭제
find ${BACKUP_DIR} -name "${DB_NAME}_*.sql.gz" -type f -mtime +30 -delete
# 백업 결과 로그
if [ $? -eq 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - 백업 성공: ${BACKUP_FILE}.gz" >> ${BACKUP_DIR}/backup.log
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - 백업 실패" >> ${BACKUP_DIR}/backup.log
fi
```
**실행 권한 부여:**
```bash
chmod 700 /app/db/backup/backup_vips.sh
```
### 6.3 Crontab 설정 (매일 밤 12시 자동 백업)
```bash
# crontab 편집
crontab -e
# 아래 내용 추가
0 0 * * * /app/db/backup/backup_vips.sh
```
### 6.4 백업 관련 명령어
```bash
# 수동 백업 실행
/app/db/backup/backup_vips.sh
# 백업 파일 목록 확인
ls -lh /app/db/backup/
# 백업 로그 확인
tail -f /app/db/backup/backup.log
# crontab 설정 확인
crontab -l
# 백업 복원 (필요시)
gunzip /app/db/backup/vips_20240101_000000.sql.gz
mysql -u vips -p -P 53306 vips < /app/db/backup/vips_20240101_000000.sql
```

@ -1,25 +0,0 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-11-01 경기11사2222 행주운수(주) 1111110081111 엠뱅크언더리프 특수차구난형소영업용 2021-01-05 1761 30만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H500 2025-11-01 22고2222 주식회사 아일공행산업 1111106811111 그랜드 스타렉스 화물차밴형소형자가용 2025-04-28 187 60만원
2025-10-01 경기도 용인시 처인구 포곡읍 포곡로 222-2, 202호 2025-03-28
H692 2025-11-01 33마3333 홍길동 7604092328316 SM6 승용차일반형중자가용 2025-07-14 110 56만원
2025-09-22 경기도 용인시 기흥구 관곡로 53, 605동 1802호(구갈동, 가현마을신안아파트) 2025-06-11
H271 2025-11-01 44구4444 제제제이엔지 주식회사 1111110064044 봉고Ⅲ 1톤 화물차일반형- 자가용 2025-08-25 68 28만원
2020-05-20 경기도 용인시 처인구 포곡읍 에버랜드로 444(0-44동(4층)) 2025-07-24
H420 2025-11-01 55서5555 김철수 5555261080555 아이오닉6 (IONI 승용차일반형중자가용 2025-08-25 68 28만원
2024-09-09 경기도 용인시 수지구 성복1로 55, 505동 505호(성오동, 성오역 서피오타치오) 2025-07-25

@ -1,152 +0,0 @@
# 자동차 과태료 비교 로직 정리
## 개요
### 중요 사항
- ⚠️ **순서가 중요합니다!**
- 조건에 걸리는 순간 다음 차량번호 비교를 진행합니다.
### 로직 변경 사항
#### 변경 전
- 하나의 API를 호출해서 비교 로직 진행
#### 변경 후
1. **기본 호출**: `TB_CAR_FFNLG_TRGT.검사일` 기준으로 API 호출 (기본정보 + 갑부정보)
2. **추가 호출**: 각 비교 로직마다 필요한 API 추가 호출
- 차량기본정보
- 갑부정보
- 기본정보 + 갑부정보
**예시**: 이첩-2의 경우, `(TB_CAR_FFNLG_TRGT.검사종료일자 + 115일)` 기준으로 차량기본정보 API 추가 호출
---
## 비교 로직 상세
### 1. 상품용 검증 [차량기본정보 필요]
#### 조건
1. `TB_CAR_FFNLG_TRGT.검사일` 기준 API 호출
2. `api.MBER_NM like '%상품용%'`
3. 갑부 상세(LedgerRecord) 리스트에서 다음 조건 확인:
```
(List.CHG_YMD between TB_CAR_FFNLG_TRGT.유효기간만료일 and TB_CAR_FFNLG_TRGT.검사종료일자)
AND
(List.CHANGE_JOB_SE_CODE = '11') -- 명의이전 코드
```
#### 처리 결과
- **TB_CAR_FFNLG_TRGT.비고**: `"[상품용] 갑부정보"`
---
### 2. 이첩-1 [차량등록원부(갑) 필요]
#### 조건
1. `TB_CAR_FFNLG_TRGT.검사일` 기준 API 호출
2. 법정동코드 불일치 검증
#### 검증 로직 (Java)
```java
/**
* 이첩 조건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;
}
```
#### 처리 결과
- **TB_CAR_FFNLG_TRGT.비고**: `"서울시 용산구 / 이경호, 검사일사용본거지, [검사대상, 사용자 조직코드 앞 4자리 및 법정동명]"`
---
### 3. 이첩-2 [차량기본정보 필요]
#### 조건
1. `TB_CAR_FFNLG_TRGT.DAYCNT` (textFile 일수) > 115
2. **검사일 기준** API 호출
3. **(검사종료일자 + 115일) 기준** API 호출 (추가)
4. 2번 API와 3번 API의 자동차기본정보 비교
- **사용본거지법정동코드 앞 4자리**가 다를 경우
#### 처리 결과
- **TB_CAR_FFNLG_TRGT.비고**: `"전라남도 순천시 / 김정대, 115일 도래지, [2개의 API 법정동코드 및 법정동명]"`
---
## 데이터 흐름도
```
TB_CAR_FFNLG_TRGT (과태료 대상)
검사일 기준 API 호출 (기본정보 + 갑부정보)
┌─────────────┬─────────────┬─────────────┐
│ 상품용 │ 이첩-1 │ 이첩-2 │
│ 검증 로직 │ 검증 로직 │ 검증 로직 │
└─────────────┴─────────────┴─────────────┘
↓ ↓ ↓
조건 만족 시 비고 업데이트 → 다음 차량번호 처리
```
---
## 핵심 필드 매핑
| 테이블 | 필드명 | 설명 |
|--------|--------|------|
| TB_CAR_FFNLG_TRGT | 검사일 | API 호출 기준일 (기본) |
| TB_CAR_FFNLG_TRGT | 검사종료일자 | 이첩-2에서 +115일 계산에 사용 |
| TB_CAR_FFNLG_TRGT | 유효기간만료일 | 상품용 검증 기간 시작일 |
| TB_CAR_FFNLG_TRGT | DAYCNT | 일수 (이첩-2 조건) |
| TB_CAR_FFNLG_TRGT | 비고 | 검증 결과 저장 |
| API 기본정보 | MBER_NM | 회원명 (상품용 검증) |
| API 기본정보 | useStrnghldLegaldongCode | 사용본거지법정동코드 (이첩 검증) |
| API 갑부 상세 | CHG_YMD | 변경일자 (상품용 기간 검증) |
| API 갑부 상세 | CHANGE_JOB_SE_CODE | 변경작업구분코드 (명의이전: 11) |
| 사용자 정보 | OrgCd | 조직코드 (이첩-1 검증) |
---
## 주의사항
1. **순차 처리**: 각 차량번호는 조건에 걸리는 즉시 다음 차량번호로 넘어갑니다.
2. **API 호출 최적화**: 기본적으로 검사일 기준 API를 호출하고, 필요시에만 추가 호출합니다.
3. **로그 기록**: 각 검증 단계마다 상세 로그를 남겨 추적 가능하도록 합니다.
4. **에러 처리**: API 호출 실패 또는 데이터 부재 시 적절한 에러 처리가 필요합니다.

@ -1,65 +0,0 @@
* 순서 중요!!
* 조건에 걸리는 순간 다음 차량번호 비교 진행
* 현재는 하나의 api 를 호출해서 비교로직 진행 ->
수정후에는 기본적으로 TB_CAR_FFNLG_TRGT.검사일 기준 api 기본정보+갑부정보 호출
그 이후 비교 로직당 필요한 api 추가 [차량기본정보 or 갑부정보 or 기본정보+갑부정보 각각 호출]하는 형태로 변경
예)이첩-2 번은 추가로 (TB_CAR_FFNLG_TRGT.검사종료일자 + 115일) 기준 차량기본정보 api 추가 호출
-- 상품용 [차량기본정보 필요]
1. TB_CAR_FFNLG_TRGT.검사일 기준 api 호출
2. api.MBER_NM like %상품용%
3. (갑부 상세(LedgerRecord) List.CHG_YMD between TB_CAR_FFNLG_TRGT.유효기간만료일 and TB_CAR_FFNLG_TRGT.검사종료일자) and (갑부 상세 List.CHANGE_JOB_SE_CODE = '11' --명의이전 코드)
4. TB_CAR_FFNLG_TRGT 비고 : 조건에 걸린 - "[상품용] 갑부정보"
-- 이첩-1 [차량등록원부(갑) 필요]
1. TB_CAR_FFNLG_TRGT.검사일 기준 api 호출
2. 아래 로직 참조
/**
* 이첩 조건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;
}
3. table 비고 : 조건에 걸린 - "서울시 용산구/ 이경호, 검사일사용본거지, [검사대상, 사용자 조직코드 앞 4자리 및 법정동명]"
-- 이첩-2 [차량기본정보 필요]
1. TB_CAR_FFNLG_TRGT.DAYCNT(textFile 일수) > 115
2. 검사일 기준 api 호출
3. (TB_CAR_FFNLG_TRGT.검사종료일자 + 115일) 기준 api 호출
4. 2 api<->3 api 자동차기본정보 : 사용본거지법정동코드 앞 4자리 다를경우
5. TB_CAR_FFNLG_TRGT 비고 : 조건에 걸린 - "전라남도 순천시 / 김정대, 115일 도래지, [2개의 api 법정동코드 및 법정동명]"

@ -0,0 +1,264 @@
# 자동차 과태료 비교 로직 명세서 (미필)
## 개요
자동차 과태료 부과 대상(미필)을 검증하기 위한 비교 로직 정의서입니다.
### 구현 위치
- **Checker 클래스**: `src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/`
- `ProductUseOmChecker.java` - 1. 상품용 : 상품용
- `OwnerTransferOmChecker.java` - 2. 명의이전 소유자 확인 : 명의이전(25.9.5.) 이전소유자 상품용, 경상남도 창원시/ 현대캐피탈 주식회사, 미수검명의이전(25.5.19.)(37하1553)
- `TransferOmChecker.java` - 3. 이첩 : 경기도 과천시/ 이정호, 115일 도래지
### 기본 설정
- 비교로직에 사용되는 API: `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출
- 날짜 유틸리티: `DateUtil.parseDate()`, `DateUtil.formatDateString()` 사용
- **미필 부과일자**: 검사유효기간 종료일 + 146일 (`levyCrtrYmd`)
### 문서 이력
| 일자 | 변경 내용 | 비고 |
|------|----------|------|
| 2025-12-08 | om_checker 소스 기준 전면 작성 | 실제 코드와 일치하도록 정리 |
| 2025-12-09 | 검사유효기간 종료일 + 31일 로직 반영 | ProductUseOmChecker, OwnerTransferOmChecker |
### 처리 규칙
> **중요**: 순서가 중요함!
> 1. 상품용 → 2. 명의이전 소유자 확인 → 3. 이첩
> - 조건에 걸리는 순간 다음 차량번호 비교로 진행
> - 각 비교 로직별로 개별 API 호출 수행
---
## 비교 로직 상세
### 1. 상품용 검증 (`ProductUseOmChecker`)
**처리상태코드**: `02` (상품용)
**클래스**: `ProductUseOmChecker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사유효기간종료일+146일` | `차대번호(vin)`, `소유자명`, `대표소유자회원번호` | 부과일자 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD` | `소유자명`, `대표소유자회원번호` | 명의이전 시점 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") ← 상품용 아니면 return null
2. Step2 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
3. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- CHG_YMD <= 검사유효기간 종료일 + 31일
- 가장 마지막 일자 선택
← 없으면 return null
4. Step4 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
→ 모든 조건 충족: 상품용(02) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("02"); // 상품용
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 2. 명의이전 소유자 확인 (`OwnerTransferOmChecker`)
**처리상태코드**: `01` (그대로 접수)
**클래스**: `OwnerTransferOmChecker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사유효기간종료일+146일` | `차대번호(vin)`, `소유자명` | 부과일자 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- CHG_YMD > 검사유효기간 종료일 + 31일
- 가장 마지막 일자 선택
← 없으면 return null
3. Step4 소유자명 확인
- "상품용" 포함 → 접수(01)
- "상품용" 미포함 → 접수(01)
→ 모든 조건 충족: 접수(01) 처리
```
#### 결과 분기
| Step4 소유자명 | 처리상태코드 | 비고 형식 |
|---------------|-----------|-----------|
| 상품용 포함 | `01` (접수) | 명의이전(날짜) 이전소유자 상품용 |
| 상품용 미포함 | `01` (접수) | 명의이전(날짜) |
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("01"); // 접수
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 3. 이첩 검증 (`TransferOmChecker`)
**처리상태코드**: `03` (이첩)
**클래스**: `TransferOmChecker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사유효기간종료일+146일` | `소유자명`, `소유자회원번호`, `사용본거지법정동코드`, `차대번호` | 부과일자 기준 정보 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=검사유효기간종료일-90일` | `소유자명`, `소유자회원번호` | 90일 전 소유자 확인 |
| 3 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `소유자명`, `소유자회원번호` | 현재 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. Step1 소유자회원번호 != Step2 소유자회원번호
← 불일치면 return null
3. Step1 소유자회원번호 != Step3 소유자회원번호
← 불일치면 return null
4. 법정동코드 앞 4자리 == 사용자 조직코드 앞 4자리
← 일치하면 return null
→ 불일치: 이첩(03) 처리
```
#### 결과 처리
| 구분 | 비고 컬럼 형식 |
|------|---------------|
| 이첩 | `"{시군구명}, 115일 도래지, [법정동코드: {legalDong4}, 법정동명: {sggNm}]"` |
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("03"); // 이첩
existingData.setCarBscMttrInqFlnm(step1RprsOwnrNm);
existingData.setCarBscMttrInqSggCd(sggCd);
existingData.setCarBscMttrInqSggNm(sggNm);
```
---
## 처리 흐름도
```
시작
[차량번호 조회]
┌──────────────────────────────────────────────┐
│ 1. 상품용 (ProductUseOmChecker) │
│ 조건: 부과일자 소유자명.contains("상품용") │
│ + 명의이전(11) 레코드 존재 │
│ + CHG_YMD <= 검사유효기간종료일+31일 │
│ + 소유자회원번호 일치 │
│ → 조건 충족: 상품용(02) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 2. 명의이전 소유자 확인 (OwnerTransferOmChecker)│
│ 조건: 부과일자 소유자명에 상품용 미포함 │
│ + CHG_YMD > 검사유효기간종료일+31일 │
│ + 명의이전(11) 레코드 존재 │
│ → 조건 충족: 접수(01) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 3. 이첩 (TransferOmChecker) │
│ 조건: 소유자명에 상품용 미포함 │
│ + 1단계/2단계 소유자 같음 │
│ + 1단계/3단계 소유자 같음 │
│ + 법정동코드 앞4자리 != 조직코드 앞4자리│
│ → 조건 충족: 이첩(03) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
[다음 차량]
```
---
## 요약 정리
### 처리상태코드 매핑
| 코드 | 상태명 | 처리 로직 | 클래스 |
|------|--------|--------------------------|--------|
| 02 | 상품용 | 부과일자 소유자가 상품용 + 명의이전(11) CHG_YMD <= 종료일+31일 | `ProductUseOmChecker` |
| 01 | 접수 | 검사유효기간종료일+31일 이후 명의이전 존재 | `OwnerTransferOmChecker` |
| 03 | 이첩 | 소유자 일치 + 법정동코드 불일치 | `TransferOmChecker` |
### 미필 vs 지연 비교
| 구분 | 미필 | 지연 |
|------|-----|------|
| 부과일자 | 검사유효기간 종료일 + 146일 | 검사일 |
| 상품용 조건 | 부과일자 기준 소유자명 | 검사일 기준 소유자명 |
| Checker 개수 | 3개 | 7개 |
| 이첩 로직 | 3단계 API + 소유자 비교 | DAYCNT 기반 |
---
## 참고 상수 및 유틸리티
### 상수
```java
// 부과일자 계산 (미필)
levyCrtrYmd = 검사유효기간 종료일 + 146일
// 처리상태코드
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE = "02"
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER = "03"
// 변경업무구분코드
"11" = 명의이전
```
---
**문서 최종 수정일**: 2025-12-09
**실제 소스 코드 기준**: om_checker 폴더 내 Checker 클래스들
**분석 대상 클래스**: 3개 (ProductUseOmChecker, OwnerTransferOmChecker, TransferOmChecker)
**주요 변경사항**: 검사유효기간 종료일 + 31일 로직 적용

@ -0,0 +1,474 @@
# 자동차 과태료 비교 로직 명세서 (지연)
## 개요
자동차 과태료 부과 대상(지연)을 검증하기 위한 비교 로직 정의서입니다.
### 구현 위치
- **Checker 클래스**: `src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/delay_checker/`
- `ProductUseChecker.java` - 1. 상품용(명의이전) : 상품용
- `ProductUseChnageChecker.java` - 1-1. 상품용(변경등록) : 상품용
- `ProductCloseWithin31Checker.java` - 2. 내사종결(명의이전 이전소유자 상품용, 31일 이내) : 명의이전(25.9.11.) 이전소유자 상품용
- `OwnerCloseWithin31Checker.java` - 3. 내사종결(순수 명의이전, 31일 이내) : 명의이전(25.8.28.)
- `ProductLevyOver31Checker.java` - 4. 날짜수정후부과(명의이전 이전소유자 상품용, 31일 초과) : 경기도 고양시/ 장준혁, 미수검명의이전(25.8.19.)
- `OwnerLevyOver31Checker.java` - 5. 날짜수정후부과(순수 명의이전, 31일 초과) : 대구광역시 달서구/ 하나캐피탈(주), 미수검명의이전(25.9.3.)
- `TransferCase115DayChecker.java` - 6. 이첩 : case 1 = 경상남도 창원시/ 현대캐피탈 주식회사, 115일 도래지 case 2 = 인천광역시 부평구/ (주)우리카드, 검사일사용본거지
### 기본 설정
- 비교로직에 사용되는 API: `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출
- 날짜 유틸리티: `DateUtil.parseDate()`, `DateUtil.formatDateString()` 사용
- 일수 계산 기준: `DAYS_THRESHOLD = 31일`
### 문서 이력
| 일자 | 변경 내용 | 비고 |
|------|----------|------|
| 2025-12-08 | delay_checker 소스 기준 전면 재작성 | 실제 코드와 일치하도록 정리 |
| 2025-12-03 | 실제 소스 코드 기준으로 전면 재작성 | Javadoc과 실제 로직 차이 명시 |
| 2025-12-02 | 내사종결 로직 추가 및 상품용 조건 변경 | 명의이전 31일 이내 조건 추가 |
### 처리 규칙
> **중요**: 순서가 중요함!
> 1. 상품용(명의이전) → 1-1. 상품용(변경등록) → 2. 내사종결(상품용) → 3. 내사종결(명의이전) → 4. 날짜수정(상품용) → 5. 날짜수정(명의이전) → 6. 이첩
> - 조건에 걸리는 순간 다음 차량번호 비교로 진행
> - 각 비교 로직별로 개별 API 호출 수행
---
## 비교 로직 상세
### 1. 상품용 검증 (`ProductUseChecker`)
**처리상태코드**: `02` (상품용)
**클래스**: `ProductUseChecker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호(vin)`, `소유자명`, `대표소유자회원번호` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD` | `소유자명`, `대표소유자회원번호` | 명의이전 시점 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") ← 상품용 아니면 return null
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- CHG_YMD <= 검사종료일자
- 가장 마지막 일자 선택
← 없으면 return null
3. Step4 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
→ 모든 조건 충족: 상품용(02) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("02"); // 상품용
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 1-1. 상품용-변경등록 검증 (`ProductUseChnageChecker`)
**처리상태코드**: `02` (상품용)
**클래스**: `ProductUseChnageChecker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호(vin)`, `소유자명`, `대표소유자회원번호` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 변경등록 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD` | `소유자명`, `대표소유자회원번호` | 변경등록 시점 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") ← 상품용 아니면 return null
2. 갑부에서 변경등록(21) 레코드 찾기
- CHG_TASK_SE_CD == "21"
- CHG_YMD <= 검사종료일자
- spcablMttr.contains("성명") ← 성명 변경 포함 필수
- 가장 마지막 일자 선택
← 없으면 return null
3. Step4 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
→ 모든 조건 충족: 상품용(02) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("02"); // 상품용
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("21");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 2. 내사종결 - 명의이전 이전소유자 상품용, 31일 이내 (`ProductCloseWithin31Checker`)
**처리상태코드**: `04` (내사종결)
**클래스**: `ProductCloseWithin31Checker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- CHG_YMD <= 검사일
- 가장 마지막 일자 선택
← 없으면 return null
3. 일수 계산: 명의이전일자 ~ 검사일
- 0~31일 이내 → 계속 진행
- 31일 초과 → return null (4번에서 처리)
4. Step4 소유자명.contains("상품용")
← 상품용 아니면 return null
→ 모든 조건 충족: 내사종결(04) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("04"); // 내사종결
existingData.setCarBscMttrInqFlnm(step4OwnerName); // 명의이전 직전 소유자명
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 3. 내사종결 - 순수 명의이전, 31일 이내 (`OwnerCloseWithin31Checker`)
**처리상태코드**: `04` (내사종결)
**클래스**: `OwnerCloseWithin31Checker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명`, `대표소유자회원번호` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD` | `소유자명`, `대표소유자회원번호` | 명의이전 시점 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자
- 가장 마지막 일자 선택
← 없으면 return null
3. 일수 계산: 명의이전일자 ~ 검사일
- 0~31일 이내 → 계속 진행
- 31일 초과 → return null (5번에서 처리)
4. Step4 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
→ 모든 조건 충족: 내사종결(04) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("04"); // 내사종결
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 4. 날짜수정후부과 - 명의이전 이전소유자 상품용, 31일 초과 (`ProductLevyOver31Checker`)
**처리상태코드**: `05` (날짜 수정 후 부과)
**클래스**: `ProductLevyOver31Checker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD-1일` | `소유자명` | 명의이전 직전 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- CHG_YMD <= 검사일
- 가장 마지막 일자 선택
← 없으면 return null
3. 일수 계산: 명의이전일자 ~ 검사일
- 31일 초과 → 계속 진행
- 0~31일 이내 → return null (2번에서 처리)
4. Step4 소유자명.contains("상품용")
← 상품용 아니면 return null
→ 모든 조건 충족: 날짜수정후부과(05) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("05"); // 날짜수정후부과
existingData.setCarBscMttrInqFlnm(step4OwnerName); // 명의이전 직전 소유자명
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 5. 날짜수정후부과 - 순수 명의이전, 31일 초과 (`OwnerLevyOver31Checker`)
**처리상태코드**: `05` (날짜 수정 후 부과)
**클래스**: `OwnerLevyOver31Checker.java`
#### API 호출 순서
| 순서 | API | 입력 파라미터 | 출력 데이터 | 용도 |
|------|-----|--------------|-------------|------|
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명`, `대표소유자회원번호` | 검사일 기준 소유자 확인 |
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘` | `차량번호`, `성명`, `주민번호`, `법정동코드` | 현재 소유자 정보 |
| 3 | 자동차등록원부(갑) | `2.차량번호`, `2.성명`, `2.주민번호`, `2.법정동코드` | 갑부 상세 List | 명의이전 이력 조회 |
| 4 | 자동차기본정보 | `1.차대번호`, `부과일자=CHG_YMD` | `소유자명`, `대표소유자회원번호` | 명의이전 시점 소유자 확인 |
#### 비교 조건
```
1. Step1 소유자명.contains("상품용") → return null (1번에서 처리됨)
!Step1 소유자명.contains("상품용") → 계속 진행
2. 갑부에서 명의이전(11) 레코드 찾기
- CHG_TASK_SE_CD == "11"
- 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자
- 가장 마지막 일자 선택
← 없으면 return null
3. 일수 계산: 명의이전일자 ~ 검사일
- 31일 초과 → 계속 진행
- 0~31일 이내 → return null (3번에서 처리)
4. Step4 대표소유자회원번호 == Step1 대표소유자회원번호
← 불일치면 return null
→ 모든 조건 충족: 날짜수정후부과(05) 처리
```
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("05"); // 날짜수정후부과
existingData.setCarBscMttrInqFlnm(step4OwnerName);
existingData.setCarRegFrmbkChgTaskSeCd("11");
existingData.setCarRegFrmbkChgYmd(targetRecord.getChgYmd());
```
---
### 6. 이첩 검증 (`TransferCase115DayChecker`)
**처리상태코드**: `03` (이첩)
**클래스**: `TransferCase115DayChecker.java`
#### API 호출
| API | 입력 파라미터 | 출력 데이터 | 용도 |
|-----|--------------|-------------|------|
| 자동차기본정보 | `차량번호`, `부과일자=부과기준일` | `사용본거지법정동코드` | 부과기준일 기준 사용본거지 확인 |
#### 비교 조건
```
1. DAYCNT 값으로 부과기준일 계산
- DAYCNT > 115: 이첩-2 (부과기준일 = 검사종료일자 + 115일)
- DAYCNT <= 115: 이첩-1 (부과기준일 = 검사일자)
← DAYCNT 없으면 return null
2. 자동차기본정보(차량번호, 부과기준일) 호출
→ 사용본거지법정동코드 조회
3. 법정동코드 앞 4자리 != 사용자 조직코드 앞 4자리
← 일치하면 return null
→ 불일치: 이첩(03) 처리
```
#### 결과 처리
| 구분 | 조건 | 비고 컬럼 형식 |
|------|------|---------------|
| 이첩-1 | `DAYCNT <= 115` | `"{시군구명}, 검사일사용본거지, [사용자 조직코드 앞 4자리: {userOrg4}, 법정동명: {sggNm}]"` |
| 이첩-2 | `DAYCNT > 115` | `"{시군구명}, 115일 도래지, [법정동코드: {legalDong4}, 법정동명: {sggNm}]"` |
#### DB 업데이트 필드
```java
existingData.setTaskPrcsSttsCd("03"); // 이첩
existingData.setCarBscMttrInqFlnm(existingData.getOwnrNm()); // 기존 소유자명 유지
existingData.setCarBscMttrInqSggCd(sggCd);
existingData.setCarBscMttrInqSggNm(sggNm);
```
---
## 처리 흐름도
```
시작
[차량번호 조회]
┌──────────────────────────────────────────────┐
│ 1. 상품용-명의이전 (ProductUseChecker) │
│ 조건: 검사일 소유자명.contains("상품용") │
│ + 명의이전(11) 레코드 존재 │
│ → 조건 충족: 상품용(02) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 1-1. 상품용-변경등록 (ProductUseChnageChecker)│
│ 조건: 검사일 소유자명.contains("상품용") │
│ + 변경등록(21) + 성명변경 포함 │
│ → 조건 충족: 상품용(02) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 2. 내사종결-상품용 (ProductCloseWithin31Checker) │
│ 조건: 31일 이내 + 명의이전 전 상품용 │
│ → 조건 충족: 내사종결(04) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 3. 내사종결-명의이전 (OwnerCloseWithin31Checker) │
│ 조건: 31일 이내 + 동일 소유자 │
│ → 조건 충족: 내사종결(04) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 4. 날짜수정-상품용 (ProductLevyOver31Checker) │
│ 조건: 31일 초과 + 명의이전 전 상품용 │
│ → 조건 충족: 날짜수정후부과(05) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 5. 날짜수정-명의이전 (OwnerLevyOver31Checker) │
│ 조건: 31일 초과 + 동일 소유자 │
│ → 조건 충족: 날짜수정후부과(05) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
┌──────────────────────────────────────────────┐
│ 6. 이첩 (TransferCase115DayChecker) │
│ 조건: 법정동코드 앞4자리 != 조직코드 앞4자리│
│ → 조건 충족: 이첩(03) │
└──────────────────────────────────────────────┘
│ (조건 미충족)
[다음 차량]
```
---
## 요약 정리
### 처리상태코드 매핑
| 코드 | 상태명 | 처리 로직 | 클래스 |
|------|--------|----------|--------|
| 02 | 상품용 | 검사일 소유자가 상품용 + 명의이전(11) | `ProductUseChecker` |
| 02 | 상품용 | 검사일 소유자가 상품용 + 변경등록(21) + 성명변경 | `ProductUseChnageChecker` |
| 04 | 내사종결 | 명의이전 전 상품용 (31일 이내) | `ProductCloseWithin31Checker` |
| 04 | 내사종결 | 순수 명의이전 (31일 이내) | `OwnerCloseWithin31Checker` |
| 05 | 날짜수정후부과 | 명의이전 전 상품용 (31일 초과) | `ProductLevyOver31Checker` |
| 05 | 날짜수정후부과 | 순수 명의이전 (31일 초과) | `OwnerLevyOver31Checker` |
| 03 | 이첩 | 법정동코드 불일치 | `TransferCase115DayChecker` |
### 상품용 vs 순수 명의이전 비교
| 구분 | 상품용 케이스 (2, 4번) | 순수 명의이전 케이스 (3, 5번) |
|------|----------------------|----------------------------|
| 검사일 소유자 | 상품용 아님 | 상품용 아님 |
| 명의이전 직전 소유자 | 상품용 **포함** | 소유자 회원번호 **일치** |
| Step 4 확인사항 | 소유자명.contains("상품용") | 대표소유자회원번호 == Step1 |
| 갑부 검색 조건 | CHG_YMD <= 검사일 | 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자 |
| 일수 기준 | 31일 이내/초과 | 31일 이내/초과 |
| 처리상태코드 | 31일 이내: 04, 초과: 05 | 31일 이내: 04, 초과: 05 |
---
## 참고 상수 및 유틸리티
### 상수
```java
// 일수 기준값
private static final int DAYS_THRESHOLD = 31;
// 처리상태코드
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE = "02"
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER = "03"
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED = "04"
TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY = "05"
// 변경업무구분코드
"11" = 명의이전
"21" = 변경등록
```
---
**문서 작성 완료일**: 2025-12-08
**실제 소스 코드 기준**: delay_checker 폴더 내 Checker 클래스들
**분석 대상 클래스**: 7개 (ProductUseChecker, ProductUseChnageChecker, ProductCloseWithin31Checker, OwnerCloseWithin31Checker, ProductLevyOver31Checker, OwnerLevyOver31Checker, TransferCase115DayChecker)

@ -0,0 +1,19 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-09-01 *162고6489 1244110241315 엠뱅크언더리프 특수차구난형소영업용 2025-08-25 *7 4만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H494 2025-09-01 271구5475 (주)케이비캐피탈 1301110013499 엠뱅크언더리프 특수차구난형소영업용 2024-09-24 303 60만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H494 2025-09-11 180너6976 (주)기아주식회사 1101110037998 엠뱅크언더리프 특수차구난형소영업용 2025-05-26 109 56만원
2025-07-14 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05

@ -0,0 +1,16 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-09-14 142러3779 서찬규 1244110241315 엠뱅크언더리프 특수차구난형소영업용 2025-06-19 90 42만원
2025-05-17 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05
H494 2025-09-07 22루2283 송동건 1301110013499 엠뱅크언더리프 특수차구난형소영업용 2025-03-10 182 60만원
2025-02-05 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2020-12-05

@ -0,0 +1,16 @@
유효기간경과 과태료부과대상 리스트
------------------------------------
* 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다. (재검여부 = *일수)
* 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.
-------------------------------------------------------------------------------------------------------------------------------------------------
검사소 검사일자 자동차번호 소유자명 주민등록번호 차 명 차 종 용 도 종료일 일수 과태료
최종등록일 주 소 유효기간만료일 매매상품용
-------------------------------------------------------------------------------------------------------------------------------------------------
H494 2025-09-12 254고9785 서찬규 1244110241315 엠뱅크언더리프 특수차구난형소영업용 2025-09-01 90 42만원
2025-08-01 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2025-08-01
H494 2025-09-15 254고9785 서찬규 1244110241315 엠뱅크언더리프 특수차구난형소영업용 2025-09-01 90 42만원
2025-08-01 경기도 용인시 기흥구 강남로 9, 111-111호(신행동, 진주만프라자) 2025-08-01

Binary file not shown.

@ -38,4 +38,10 @@ public class TaskPrcsSttsConstants {
*/
public static final String TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED = "04";
/**
* - 05:
* API
*/
public static final String TASK_PRCS_STTS_CD_05_DATE_MODIFIED_LEVY = "05";
}

@ -153,6 +153,34 @@ public class DateUtil {
}
}
/**
* LocalDate
* 8 yyyyMMdd, 10 yyyy-MM-dd
*
* @param dateStr
* @return LocalDate , null
*/
public static LocalDate parseDate(String dateStr) {
if (dateStr == null) {
return null;
}
try {
String trimmed = dateStr.trim();
if (trimmed.length() == 8) {
// yyyyMMdd 형식
return LocalDate.parse(trimmed, DateTimeFormatter.ofPattern("yyyyMMdd"));
} else if (trimmed.length() == 10) {
// yyyy-MM-dd 형식
return LocalDate.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} else {
return null;
}
} catch (Exception e) {
return null;
}
}
/**
* LocalDateTime
* @param dateTimeStr
@ -171,20 +199,73 @@ public class DateUtil {
}
/**
* yyyyMMdd yyyy-MM-dd
* @param dateStr (yyyyMMdd , : "20250825")
* @return (yyyy-MM-dd , : "2025-08-25"), null
* yyyy-MM-dd
* 8 yyyyMMdd , 10 yyyy-MM-dd
*
* @param dateStr (yyyyMMdd yyyy-MM-dd )
* @return yyyy-MM-dd ,
*/
public static String formatDateString(String dateStr) {
if (dateStr == null || dateStr.length() != 8) {
return null;
if (dateStr == null || dateStr.isEmpty()) {
return dateStr;
}
try {
// yyyyMMdd 형식으로 파싱한 후 yyyy-MM-dd 형식으로 포맷
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String trimmed = dateStr.trim();
if (trimmed.length() == 8) {
// yyyyMMdd → yyyy-MM-dd
LocalDate date = LocalDate.parse(trimmed, DateTimeFormatter.ofPattern("yyyyMMdd"));
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} else if (trimmed.length() == 10) {
// 이미 yyyy-MM-dd 형식
return trimmed;
} else {
// 그 외 형식은 그대로 반환
return dateStr;
}
} catch (Exception e) {
return null;
return dateStr;
}
}
/**
* (YYYYMMDD YYYY-MM-DD -> YY.M.D)
* : 20250903 -> 25.9.3
* : 2025-09-03 -> 25.9.3
*
* @param dateStr (yyyyMMdd yyyy-MM-dd )
* @return YY.M.D ,
*/
public static String formatToShortDate(String dateStr) {
if (dateStr == null) {
return dateStr;
}
try {
String trimmed = dateStr.trim();
String year, monthStr, dayStr;
if (trimmed.length() == 8) {
// YYYYMMDD 형식
year = trimmed.substring(2, 4); // YY
monthStr = trimmed.substring(4, 6); // MM
dayStr = trimmed.substring(6, 8); // DD
} else if (trimmed.length() == 10) {
// YYYY-MM-DD 형식
year = trimmed.substring(2, 4); // YY
monthStr = trimmed.substring(5, 7); // MM
dayStr = trimmed.substring(8, 10); // DD
} else {
// 그 외 형식은 원본 반환
return dateStr;
}
int month = Integer.parseInt(monthStr); // M (앞의 0 제거)
int day = Integer.parseInt(dayStr); // D (앞의 0 제거)
return year + "." + month + "." + day;
} catch (Exception e) {
return dateStr;
}
}
@ -326,6 +407,35 @@ public class DateUtil {
return result.format(DateTimeFormatter.ofPattern(pattern));
}
/**
*
* yyyyMMdd yyyy-MM-dd
*
* @param targetDate
* @param startDate
* @param endDate
* @return (startDate <= targetDate <= endDate)
*/
public static boolean isDateBetween(String targetDate, String startDate, String endDate) {
if (targetDate == null || startDate == null || endDate == null) {
return false;
}
try {
LocalDate target = parseDate(targetDate);
LocalDate start = parseDate(startDate);
LocalDate end = parseDate(endDate);
if (target == null || start == null || end == null) {
return false;
}
return !target.isBefore(start) && !target.isAfter(end);
} catch (Exception e) {
return false;
}
}
/**
*
* @param startDate

@ -219,7 +219,7 @@ public class StringUtil {
/**
* UTF-8 .
* 3, / 1 .
*
*
* @param str
* @return , null 0
*/
@ -227,11 +227,11 @@ public class StringUtil {
if (str == null || str.isEmpty()) {
return 0;
}
int byteLength = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 한글 유니코드 범위 확인
if ((c >= 0xAC00 && c <= 0xD7A3) || (c >= 0x3131 && c <= 0x318E)) {
byteLength += 3; // 한글은 3바이트
@ -239,7 +239,44 @@ public class StringUtil {
byteLength += 1; // 영문/숫자는 1바이트
}
}
return byteLength;
}
/**
* null (null )
* @param value
* @return null ,
*/
public static String nvl(String value) {
return value != null ? value : "";
}
/**
* ( 7 )
* @param idecno
* @return ( 6 *******)
*/
public static String maskIdecno(String idecno) {
if (idecno == null || idecno.length() < 7) {
return idecno != null ? idecno : "";
}
String front = idecno.substring(0, Math.min(6, idecno.length()));
return front + "*******";
}
/**
* ( StringBuilder )
* @param sb StringBuilder
* @param label
* @param value
*/
public static void appendIfNotEmpty(StringBuilder sb, String label, String value) {
if (value != null && !value.isEmpty()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(label).append(": ").append(value);
}
}
}

@ -1,23 +0,0 @@
package go.kr.project.api.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
/**
* API MyBatis Mapper
*
* <p>VMIS API Mapper .</p>
*
* <ul>
* <li>DataSource: egovframework (DataSourceProxyConfig)</li>
* <li>TransactionManager: egovframework (EgovConfigTransaction.txManager)</li>
* <li>SqlSessionFactory: MyBatis Spring Boot Starter </li>
* <li>MapperScan: go.kr.project.api.internal.mapper (API )</li>
* </ul>
*
* <p> Mapper egovframework .</p>
*/
@Configuration
@MapperScan(basePackages = "go.kr.project.api.internal.mapper")
public class ApiMapperConfig {
}

@ -22,9 +22,7 @@ import java.io.IOException;
/**
* RestTemplate
* VMIS Integration Mode
* - Internal Mode: API ( )
* - External Mode: VMIS-interface API ( )
* VMIS-interface API
* Apache HttpClient 4
*/
@Slf4j
@ -36,17 +34,9 @@ public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// VMIS Integration Mode에 따라 적절한 설정 선택
String mode = vmisProperties.getIntegration().getMode();
VmisProperties.RestTemplateProps.ModeConfig config;
VmisProperties.RestTemplateProps config = vmisProperties.getRestTemplate();
if ("internal".equalsIgnoreCase(mode)) {
config = vmisProperties.getRestTemplate().getInternal();
log.info("RestTemplate 설정 - Internal Mode (정부 API 호출용)");
} else {
config = vmisProperties.getRestTemplate().getExternal();
log.info("RestTemplate 설정 - External Mode (외부 VMIS-interface API 호출용)");
}
log.info("RestTemplate 설정 - 외부 VMIS-interface API 호출용");
// 타임아웃 설정
int connectTimeout = config.getTimeout().getConnectTimeoutMillis();
@ -98,6 +88,7 @@ public class RestTemplateConfig {
* Rate Limiting
* Guava RateLimiter
*/
@SuppressWarnings("UnstableApiUsage")
private static class RateLimitInterceptor implements ClientHttpRequestInterceptor {
private final RateLimiter rateLimiter;
private final double permitsPerSecond;

@ -1,117 +0,0 @@
package go.kr.project.api.config;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.service.VehicleInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* VMIS
*
* <p> VMIS ,
* .</p>
*
* <h3> :</h3>
* <ul>
* <li> VMIS </li>
* <li> VehicleInfoService </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>internal: VMIS (InternalVehicleInfoServiceImpl)</li>
* <li>external: REST API (ExternalVehicleInfoServiceImpl)</li>
* </ul>
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class VmisIntegrationConfig {
private final VmisProperties vmisProperties;
/**
* VMIS
*
* <p> VMIS
* .</p>
*
* @param vehicleInfoService VehicleInfoService
* @return CommandLineRunner
*/
@Bean
public CommandLineRunner vmisIntegrationModeLogger(VehicleInfoService vehicleInfoService) {
return args -> {
String mode = vmisProperties.getIntegration().getMode();
String implClass = vehicleInfoService.getClass().getSimpleName();
log.info("========================================");
log.info("VMIS Integration Mode: {}", mode);
log.info("Active Implementation: {}", implClass);
log.info("========================================");
if ("internal".equalsIgnoreCase(mode)) {
logInternalModeInfo();
} else if ("external".equalsIgnoreCase(mode)) {
logExternalModeInfo();
} else {
log.warn("알 수 없는 VMIS 통합 모드: {}. 'internal' 또는 'external'을 사용하세요.", mode);
}
};
}
/**
* Internal
*/
private void logInternalModeInfo() {
log.info("[Internal Mode] 내부 VMIS 모듈을 직접 사용합니다");
log.info(" - 정부 API 호스트: {}://{}",
vmisProperties.getGov().getScheme(),
vmisProperties.getGov().getHost());
log.info(" - 기본사항 조회 경로: {}",
vmisProperties.getGov().getServices().getBasic().getPath());
log.info(" - 등록원부 조회 경로: {}",
vmisProperties.getGov().getServices().getLedger().getPath());
log.info(" - GPKI 암호화: {}",
vmisProperties.getGpki().getEnabled());
log.info(" - 연결 타임아웃: {}ms",
vmisProperties.getRestTemplate().getInternal().getTimeout().getConnectTimeoutMillis());
log.info(" - 읽기 타임아웃: {}ms",
vmisProperties.getRestTemplate().getInternal().getTimeout().getReadTimeoutMillis());
log.info(" - Rate Limit: 초당 {} 건",
vmisProperties.getRestTemplate().getInternal().getRateLimit().getPermitsPerSecond());
if ("Y".equalsIgnoreCase(vmisProperties.getGpki().getEnabled())) {
log.info(" - GPKI 인증서 서버 ID: {}",
vmisProperties.getGpki().getCertServerId());
log.info(" - GPKI 대상 서버 ID: {}",
vmisProperties.getGpki().getTargetServerId());
} else {
log.warn(" - GPKI 암호화가 비활성화되어 있습니다. 개발 환경에서만 사용하세요.");
}
}
/**
* External
*/
private void logExternalModeInfo() {
log.info("[External Mode] 외부 REST API를 사용합니다");
log.info(" - 외부 API Base URL: {}",
vmisProperties.getExternal().getApi().getUrl().getBase());
log.info(" - 연결 타임아웃: {}ms",
vmisProperties.getRestTemplate().getExternal().getTimeout().getConnectTimeoutMillis());
log.info(" - 읽기 타임아웃: {}ms",
vmisProperties.getRestTemplate().getExternal().getTimeout().getReadTimeoutMillis());
log.info(" - Rate Limit: 초당 {} 건",
vmisProperties.getRestTemplate().getExternal().getRateLimit().getPermitsPerSecond());
log.warn(" - 외부 VMIS-interface 서버가 실행 중이어야 합니다.");
log.info(" - 기본사항 조회: POST {}",
vmisProperties.getExternal().getApi().getUrl().buildBasicUrl());
log.info(" - 등록원부 조회: POST {}",
vmisProperties.getExternal().getApi().getUrl().buildLedgerUrl());
}
}

@ -2,35 +2,23 @@ package go.kr.project.api.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
@Component
@ConfigurationProperties(prefix = "vmis")
@Validated
public class VmisProperties {
@NotNull
private IntegrationProps integration = new IntegrationProps();
@NotNull
private RestTemplateProps restTemplate = new RestTemplateProps();
@NotNull
private SystemProps system = new SystemProps();
@NotNull
private GpkiProps gpki = new GpkiProps();
@NotNull
private GovProps gov = new GovProps();
@NotNull
private ExternalProps external = new ExternalProps();
@Data
public static class IntegrationProps {
@NotBlank
private String mode = "external";
}
@Data
public static class ExternalProps {
@NotNull
@ -45,14 +33,56 @@ public class VmisProperties {
public static class UrlProps {
@NotBlank
private String base = "http://localhost:8081/api/v1/vehicles"; // 기본 Base URL
@NotBlank
private String basic = "/basic"; // 자동차기본정보 엔드포인트 경로
@NotBlank
private String ledger = "/ledger"; // 자동차등록원부 엔드포인트 경로
// 중요: 외부 VMIS-interface 호출용 전체 URL 조합 헬퍼 (공통 RestTemplate 타임아웃 설정 사용)
public String buildBasicUrl() { return join(base, basic); }
public String buildLedgerUrl() { return join(base, ledger); }
@NotNull
private ApiTypeProps basic = new ApiTypeProps(); // 자동차기본정보 API 설정
@NotNull
private ApiTypeProps ledger = new ApiTypeProps(); // 자동차등록원부(갑) API 설정
@Data
public static class ApiTypeProps {
@NotBlank
private String oldOrNew = "new"; // API 버전 선택 ("old" 또는 "new")
@NotBlank
private String oldUrl = "/old-basic"; // 구 API 엔드포인트
@NotBlank
private String newUrl = "/new-basic"; // 신 API 엔드포인트
// 선택된 API URL 반환
public String getSelectedUrl() {
return "old".equalsIgnoreCase(oldOrNew) ? oldUrl : newUrl;
}
}
// 외부 VMIS-interface 호출용 전체 URL 조합 헬퍼
// YAML 설정(old-or-new)에 따라 자동으로 선택
public String buildBasicUrl() {
return join(base, basic.getSelectedUrl());
}
public String buildLedgerUrl() {
return join(base, ledger.getSelectedUrl());
}
// 명시적으로 old/new 버전 선택 (하위 호환성 및 명시적 호출용)
public String buildOldBasicUrl() {
return join(base, basic.getOldUrl());
}
public String buildNewBasicUrl() {
return join(base, basic.getNewUrl());
}
public String buildOldLedgerUrl() {
return join(base, ledger.getOldUrl());
}
public String buildNewLedgerUrl() {
return join(base, ledger.getNewUrl());
}
private String join(String base, String path) {
String b = base;
@ -69,125 +99,30 @@ public class VmisProperties {
}
}
@Data
public static class SystemProps {
@NotBlank
private String infoSysId;
/** INFO_SYS_IP */
private String infoSysIp;
/** 시군구코드 (SIGUNGU_CODE) */
private String sigunguCode;
private String departmentCode;
// 담당자 정보
private String chargerId;
private String chargerIp;
private String chargerNm;
}
@Data
public static class GpkiProps {
/** "Y" 또는 "N" */
@NotBlank
private String enabled = "N";
private boolean useSign = true;
@NotBlank
private String charset = "UTF-8";
@NotBlank
private String certServerId;
@NotBlank
private String targetServerId;
// Optional advanced config for native GPKI util
private Boolean ldap; // null -> util default
private String gpkiLicPath; // e.g., C:/gpki2/gpkisecureweb/conf
private String certFilePath; // directory for target cert files when LDAP=false
private String envCertFilePathName; // ..._env.cer
private String envPrivateKeyFilePathName; // ..._env.key
private String envPrivateKeyPasswd;
private String sigCertFilePathName; // ..._sig.cer
private String sigPrivateKeyFilePathName; // ..._sig.key
private String sigPrivateKeyPasswd;
public boolean isEnabledFlag() { return "Y".equalsIgnoreCase(enabled); }
}
@Data
public static class RestTemplateProps {
@NotNull
private ModeConfig internal = new ModeConfig();
private TimeoutConfig timeout = new TimeoutConfig();
@NotNull
private ConnectionPoolConfig connectionPool = new ConnectionPoolConfig();
@NotNull
private ModeConfig external = new ModeConfig();
private RateLimitConfig rateLimit = new RateLimitConfig();
@Data
public static class ModeConfig {
@NotNull
private TimeoutConfig timeout = new TimeoutConfig();
@NotNull
private ConnectionPoolConfig connectionPool = new ConnectionPoolConfig();
@NotNull
private RateLimitConfig rateLimit = new RateLimitConfig();
@Data
public static class TimeoutConfig {
private int connectTimeoutMillis = 10000;
private int readTimeoutMillis = 12000;
}
@Data
public static class ConnectionPoolConfig {
private int maxTotal = 100;
private int maxPerRoute = 20;
}
@Data
public static class RateLimitConfig {
private double permitsPerSecond = 5.0;
}
}
}
@Data
public static class GovProps {
@NotBlank
private String scheme = "http";
@NotBlank
private String host;
@NotBlank
private String basePath;
@NotNull
private Services services = new Services();
public String buildServiceUrl(String path) {
StringBuilder sb = new StringBuilder();
sb.append(scheme).append("://").append(host);
if (basePath != null && !basePath.isEmpty()) {
if (!basePath.startsWith("/")) sb.append('/');
sb.append(basePath);
}
if (path != null && !path.isEmpty()) {
if (!path.startsWith("/")) sb.append('/');
sb.append(path);
}
return sb.toString();
public static class TimeoutConfig {
private int connectTimeoutMillis = 10000;
private int readTimeoutMillis = 12000;
}
@Data
public static class Services {
@NotNull
private Service basic = new Service();
@NotNull
private Service ledger = new Service();
public static class ConnectionPoolConfig {
private int maxTotal = 100;
private int maxPerRoute = 20;
}
@Data
public static class Service {
@NotBlank
private String path;
@NotBlank
private String cntcInfoCode;
@NotBlank
private String apiKey;
@NotBlank
private String cvmisApikey;
public static class RateLimitConfig {
private double permitsPerSecond = 5.0;
}
}
}

@ -1,12 +1,11 @@
package go.kr.project.api.controller;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.service.VehicleInfoService;
import go.kr.project.api.model.request.NewBasicRequest;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.NewBasicResponse;
import go.kr.project.api.model.response.NewLedgerResponse;
import go.kr.project.api.service.ExternalVehicleApiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
@ -47,45 +46,10 @@ import java.util.Collections;
@RequestMapping("/api/v1/vehicles")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "VMIS 차량정보 (Swagger)", description = "vmis.integration.mode에 따라 내부/외부 분기 호출")
@Tag(name = "VMIS 차량정보 (Swagger)", description = "외부 호출")
public class VehicleInterfaceController {
private final VehicleInfoService vehicleInfoService; // 모드별 구현체 자동 주입
/**
* + ()
* - Internal/External (VehicleInfoService )
* - Envelope<BasicRequest> VHRNO()
*/
@PostMapping(value = "/info.ajax", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "자동차 통합 조회 (기본+등록원부)",
description = "vmis.integration.mode 값에 따라 내부 모듈 또는 외부 REST API를 통해 통합 조회 수행",
requestBody = @RequestBody(
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @ExampleObject(
name = "통합 조회 예제",
value = "{\"data\": [{\"VHRNO\": \"12가3456\",\"LEVY_STDDE\": \"20250101\"}]}"
)
)
)
)
public ResponseEntity<Envelope<VehicleApiResponseVO>> info(
@org.springframework.web.bind.annotation.RequestBody Envelope<BasicRequest> envelope
) {
// 중요 로직: Swagger 요청 Envelope에서 BasicRequest 추출 (차량번호 및 필수 파라미터 포함)
BasicRequest request = (envelope != null && !envelope.getData().isEmpty()) ? envelope.getData().get(0) : null;
if (request == null || request.getVhrno() == null || request.getVhrno().trim().isEmpty()) {
// 간단한 검증 실패 시 빈 데이터로 반환
return ResponseEntity.ok(new Envelope<>(Collections.emptyList()));
}
// VehicleInfoService는 모드에 따라 구현체가 자동 주입됨
VehicleApiResponseVO resp = vehicleInfoService.getVehicleInfo(request);
Envelope<VehicleApiResponseVO> out = new Envelope<>(resp);
return ResponseEntity.ok(out);
}
private final ExternalVehicleApiService service;
/**
*
@ -94,29 +58,38 @@ public class VehicleInterfaceController {
@PostMapping(value = "/basic.ajax", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "자동차기본사항조회 (단독)",
description = "vmis.integration.mode에 따라 내부 모듈 또는 외부 REST API를 통해 기본정보만 조회",
description = "YAML 설정(vmis.external.api.url.basic.old-or-new)에 따라 old/new API를 자동 호출하여 기본정보만 조회",
requestBody = @RequestBody(
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @ExampleObject(
name = "기본사항조회 예제 (자동차번호)",
value = "{\"data\": [{\"VHRNO\": \"12가3456\",\"LEVY_STDDE\": \"20250101\"}]}"
value = "{\n" +
" \"data\": [{\n" +
" \"record\": [{\n" +
" \"INQ_SE_CD\": \"자동설정: VHRNO not null → 3:자동차번호, VIN not null → 2:차대번호\",\n" +
" \"LEVY_CRTR_YMD\": \"20250101\",\n" +
" \"VHRNO\": \"차량번호\",\n" +
" \"VIN\": null\n" +
" }]\n" +
" }]\n" +
"}"
)
)
)
)
public ResponseEntity<Envelope<BasicResponse>> basic(
@org.springframework.web.bind.annotation.RequestBody Envelope<BasicRequest> envelope
public ResponseEntity<Envelope<NewBasicResponse>> basic(
@org.springframework.web.bind.annotation.RequestBody Envelope<NewBasicRequest> envelope
) {
// 중요 로직: Swagger 요청 Envelope에서 BasicRequest 추출 (차량번호 및 필수 파라미터 포함)
BasicRequest request = (envelope != null && !envelope.getData().isEmpty()) ? envelope.getData().get(0) : null;
if (request == null || request.getVhrno() == null || request.getVhrno().trim().isEmpty()) {
// Envelope에서 NewBasicRequest 추출
NewBasicRequest request = (envelope != null && !envelope.getData().isEmpty()) ? envelope.getData().get(0) : null;
if (request == null || request.getRecord() == null || request.getRecord().isEmpty()) {
return ResponseEntity.ok(new Envelope<>(Collections.emptyList()));
}
// VehicleInfoService는 모드에 따라 구현체가 자동 주입되어 분기 처리
BasicResponse basic = vehicleInfoService.getBasicInfo(request);
Envelope<BasicResponse> out = (basic != null) ? new Envelope<>(basic) : new Envelope<>(Collections.emptyList());
// YAML 설정에 따라 old/new API 자동 선택
NewBasicResponse basic = service.getBasicInfo(request);
Envelope<NewBasicResponse> out = (basic != null) ? new Envelope<>(basic) : new Envelope<>(Collections.emptyList());
return ResponseEntity.ok(out);
}
@ -127,29 +100,41 @@ public class VehicleInterfaceController {
@PostMapping(value = "/ledger.ajax", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "자동차등록원부(갑) 조회 (단독)",
description = "vmis.integration.mode에 따라 내부 모듈 또는 외부 REST API를 통해 등록원부만 조회",
description = "YAML 설정(vmis.external.api.url.ledger.old-or-new)에 따라 old/new API를 자동 호출하여 등록원부만 조회",
requestBody = @RequestBody(
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @ExampleObject(
name = "등록원부 조회 예제",
value = "{\"data\": [{\"VHRNO\": \"12가3456\",\"ONES_INFORMATION_OPEN\": \"1\",\"CPTTR_NM\": \"홍길동\",\"CPTTR_IHIDNUM\": \"8801011234567\",\"CPTTR_LEGALDONG_CODE\": \"1111011700\",\"DETAIL_EXPRESSION\": \"1\",\"INQIRE_SE_CODE\": \"1\"}]}"
value = "{\n" +
" \"data\": [\n" +
" {\n" +
" \"VHRNO\": \"차량번호\",\n" +
" \"PRVC_RLS\": \"1\",\n" +
" \"CVLPR_NM\": \"홍길동\",\n" +
" \"CVLPR_IDECNO\": \"8801011234567\",\n" +
" \"CVLPR_STDG_CD\": \"1111011700\",\n" +
" \"DSCTN_INDCT\": \"1\",\n" +
" \"INQ_SE_CD\": \"1\"\n" +
" }\n" +
" ]\n" +
"}"
)
)
)
)
public ResponseEntity<Envelope<LedgerResponse>> ledger(
@org.springframework.web.bind.annotation.RequestBody Envelope<LedgerRequest> envelope
public ResponseEntity<Envelope<NewLedgerResponse>> ledger(
@org.springframework.web.bind.annotation.RequestBody Envelope<NewLedgerRequest> envelope
) {
// 중요 로직: Swagger 요청 Envelope에서 LedgerRequest 추출 (차량번호 및 필수 파라미터 포함)
LedgerRequest request = (envelope != null && !envelope.getData().isEmpty()) ? envelope.getData().get(0) : null;
// Envelope에서 NewLedgerRequest 추출
NewLedgerRequest request = (envelope != null && !envelope.getData().isEmpty()) ? envelope.getData().get(0) : null;
if (request == null || request.getVhrno() == null || request.getVhrno().trim().isEmpty()) {
return ResponseEntity.ok(new Envelope<>(Collections.emptyList()));
}
// VehicleInfoService는 모드에 따라 구현체가 자동 주입되어 분기 처리
LedgerResponse ledger = vehicleInfoService.getLedgerInfo(request);
Envelope<LedgerResponse> out = (ledger != null) ? new Envelope<>(ledger) : new Envelope<>(Collections.emptyList());
// YAML 설정에 따라 old/new API 자동 선택
NewLedgerResponse ledger = service.getLedgerInfo(request);
Envelope<NewLedgerResponse> out = (ledger != null) ? new Envelope<>(ledger) : new Envelope<>(Collections.emptyList());
return ResponseEntity.ok(out);
}
}

@ -1,34 +0,0 @@
package go.kr.project.api.external.service;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
/**
* VMIS-interface API
* VMIS-interface API
*/
public interface ExternalVehicleApiService {
/**
* ( )
*
* @param basicRequest (, , )
* @return
*/
VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest);
/**
* ( REST )
* : ,
*/
BasicResponse getBasicInfo(BasicRequest request);
/**
* () ( REST )
* :
*/
LedgerResponse getLedgerInfo(LedgerRequest request);
}

@ -1,237 +0,0 @@
package go.kr.project.api.external.service.impl;
import egovframework.exception.MessageException;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.external.service.ExternalVehicleApiService;
import go.kr.project.api.internal.util.ExceptionDetailUtil;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.*;
import go.kr.project.api.service.VmisCarBassMatterInqireLogService;
import go.kr.project.api.service.VmisCarLedgerFrmbkLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* VMIS-interface API
* VMIS-interface API
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ExternalVehicleApiServiceImpl extends EgovAbstractServiceImpl implements ExternalVehicleApiService {
private final RestTemplate restTemplate;
private final VmisProperties vmisProperties; // 프로퍼티 주입 (외부 API URL 구성에 사용)
private final VmisCarBassMatterInqireLogService bassMatterLogService; // 기본사항 조회 로그 서비스
private final VmisCarLedgerFrmbkLogService ledgerLogService; // 등록원부 로그 서비스
@Override
public VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest) {
String vehicleNumber = basicRequest.getVhrno();
log.info("차량 정보 조회 시작 - 차량번호: {}, 부과기준일: {}, 조회구분: {}",
vehicleNumber, basicRequest.getLevyStdde(), basicRequest.getInqireSeCode());
VehicleApiResponseVO response = new VehicleApiResponseVO();
response.setVhrno(vehicleNumber);
try {
// 1. 차량 기본정보 조회
// 중요 로직: BasicRequest 전체를 사용하여 조회
BasicResponse basicInfo = getBasicInfo(basicRequest);
response.setBasicInfo(basicInfo);
// 2. 자동차 등록원부 조회
// 중요 로직: 통합 조회 시에는 차량번호와 기본정보를 바탕으로 LedgerRequest 생성
LedgerRequest ledgerRequest = new LedgerRequest();
ledgerRequest.setVhrno(vehicleNumber);
// basicInfo에서 민원인 정보 가져오기
if (basicInfo != null && basicInfo.getRecord() != null && !basicInfo.getRecord().isEmpty()) {
BasicResponse.Record record = basicInfo.getRecord().get(0);
ledgerRequest.setCpttrNm(record.getMberNm()); // 민원인성명
ledgerRequest.setCpttrIhidnum(record.getMberSeNo()); // 민원인주민번호
}
// 고정값 설정
ledgerRequest.setCpttrLegaldongCode(null); // 민원인법정동코드
ledgerRequest.setRouteSeCode("3"); // 경로구분코드
ledgerRequest.setDetailExpression("1"); // 내역표시 (전체내역)
LedgerResponse ledgerInfo = getLedgerInfo(ledgerRequest);
response.setLedgerInfo(ledgerInfo);
// 3. 결과 검증
if (basicInfo != null && ApiConstant.CNTC_RESULT_CODE_SUCCESS.equals(basicInfo.getCntcResultCode())) {
response.setSuccess(true);
response.setMessage("조회 성공");
log.info("차량번호 {} 조회 성공", vehicleNumber);
} else {
response.setSuccess(false);
response.setMessage(basicInfo != null ? basicInfo.getCntcResultDtls() : "조회 실패");
log.warn("차량번호 {} 조회 실패 - {}", vehicleNumber, response.getMessage());
}
} catch (Exception e) {
response.setSuccess(false);
response.setMessage("API 호출 오류: " + e.getMessage());
log.error("차량번호 {} API 호출 중 오류 발생", vehicleNumber, e);
}
return response;
}
/**
* API
* : BasicRequest API
* (, , , )
*
* @param request (, , )
* @return
*/
@Override
public BasicResponse getBasicInfo(BasicRequest request) {
log.debug("차량 기본정보 조회 API 호출 - 차량번호: {}", request.getVhrno());
String generatedId = null;
try {
// 1) 최초 요청 로그 저장 (별도 트랜잭션)
VmisCarBassMatterInqireVO logEntity = VmisCarBassMatterInqireVO.fromRequest(request);
generatedId = bassMatterLogService.createInitialRequestNewTx(logEntity);
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
Envelope<BasicRequest> requestEnvelope = new Envelope<>(request);
// HTTP 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Envelope<BasicRequest>> requestEntity = new HttpEntity<>(requestEnvelope, headers);
// 2) API 호출
ResponseEntity<Envelope<BasicResponse>> responseEntity = restTemplate.exchange(
vmisProperties.getExternal().getApi().getUrl().buildBasicUrl(),
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<Envelope<BasicResponse>>() {}
);
// 3) 응답 로그 업데이트 (별도 트랜잭션)
if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) {
List<BasicResponse> data = responseEntity.getBody().getData();
if (data != null && !data.isEmpty()) {
VmisCarBassMatterInqireVO update = VmisCarBassMatterInqireVO.fromResponse(generatedId, responseEntity.getBody());
if (update != null) {
bassMatterLogService.updateResponseNewTx(update);
}
return data.get(0);
}
}
log.warn("차량 기본정보 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
return null;
} catch (Exception e) {
// 4) 오류 로그 업데이트 (별도 트랜잭션)
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
VmisCarBassMatterInqireVO errorLog = VmisCarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId)
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR)
.cntcResultDtls(detail)
.build();
bassMatterLogService.updateResponseNewTx(errorLog);
log.error("[EXTERNAL-BASIC-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[EXTERNAL-BASIC-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
log.error("차량 기본정보 조회 API 호출 실패 - 차량번호: {}", request.getVhrno(), e);
throw new MessageException("차량 기본정보 조회 실패: " + e.getMessage(), e);
}
}
/**
* () API
* : ,
*
* @param request (, , )
* @return
*/
@Override
public LedgerResponse getLedgerInfo(LedgerRequest request) {
log.debug("자동차 등록원부 조회 API 호출 - 차량번호: {}", request.getVhrno());
String generatedId = null;
try {
// 1) 최초 요청 로그 저장 (별도 트랜잭션)
VmisCarLedgerFrmbkVO init = VmisCarLedgerFrmbkVO.fromRequest(request);
generatedId = ledgerLogService.createInitialRequestNewTx(init);
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
Envelope<LedgerRequest> requestEnvelope = new Envelope<>(request);
// HTTP 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Envelope<LedgerRequest>> requestEntity = new HttpEntity<>(requestEnvelope, headers);
// 2) API 호출
ResponseEntity<Envelope<LedgerResponse>> responseEntity = restTemplate.exchange(
vmisProperties.getExternal().getApi().getUrl().buildLedgerUrl(),
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<Envelope<LedgerResponse>>() {}
);
// 3) 응답 로그 업데이트 (마스터 + 상세, 별도 트랜잭션)
if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) {
List<LedgerResponse> data = responseEntity.getBody().getData();
if (data != null && !data.isEmpty()) {
LedgerResponse body = data.get(0);
VmisCarLedgerFrmbkVO masterUpdate = VmisCarLedgerFrmbkVO.fromResponseMaster(generatedId, body);
ledgerLogService.updateResponseNewTx(masterUpdate);
List<VmisCarLedgerFrmbkDtlVO> details = VmisCarLedgerFrmbkDtlVO.listFromResponse(body, generatedId);
if (details != null && !details.isEmpty()) {
ledgerLogService.saveDetailsNewTx(generatedId, details);
}
return body;
}
}
log.warn("자동차 등록원부 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
return null;
} catch (Exception e) {
// 4) 오류 로그 업데이트 (별도 트랜잭션)
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
VmisCarLedgerFrmbkVO errorLog = VmisCarLedgerFrmbkVO.builder()
.carLedgerFrmbkId(generatedId)
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR)
.cntcResultDtls(detail)
.build();
ledgerLogService.updateResponseNewTx(errorLog);
log.error("[EXTERNAL-LEDGER-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[EXTERNAL-LEDGER-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
log.error("자동차 등록원부 조회 API 호출 실패 - 차량번호: {}", request.getVhrno(), e);
throw new MessageException("자동차 등록원부 조회 실패: " + e.getMessage(), e);
}
}
}

@ -1,91 +0,0 @@
package go.kr.project.api.external.service.impl;
import go.kr.project.api.external.service.ExternalVehicleApiService;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.service.VehicleInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
* REST API
*
* <p> VMIS-interface REST API
* . ExternalVehicleApiService .</p>
*
* <h3> :</h3>
* <pre>
* # application.yml
* vmis:
* integration:
* mode: external
* </pre>
*
* <h3> :</h3>
* <ol>
* <li> ExternalVehicleApiService </li>
* <li>ExternalVehicleApiService REST API </li>
* <li> </li>
* </ol>
*
* <h3> API :</h3>
* <ul>
* <li> URL: vmis.external.api.url </li>
* <li>: http://localhost:8081/api/v1/vehicles</li>
* <li> VMIS-interface </li>
* </ul>
*
* @see VehicleInfoService
* @see ExternalVehicleApiService
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true)
public class ExternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl implements VehicleInfoService {
private final ExternalVehicleApiService externalVehicleApiService;
@Override
public VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest) {
String vehicleNumber = basicRequest.getVhrno();
log.info("[External Mode] 차량 정보 조회 시작 - 차량번호: {}, 부과기준일: {}, 조회구분: {}",
vehicleNumber, basicRequest.getLevyStdde(), basicRequest.getInqireSeCode());
VehicleApiResponseVO response = externalVehicleApiService.getVehicleInfo(basicRequest);
if (response.isSuccess()) {
log.info("[External Mode] 차량번호 {} 조회 성공", vehicleNumber);
} else {
log.warn("[External Mode] 차량번호 {} 조회 실패 - {}", vehicleNumber, response.getMessage());
}
return response;
}
/**
* REST -
* : API ExternalVehicleApiService (BasicRequest )
*/
@Override
public BasicResponse getBasicInfo(BasicRequest request) {
// 중요 로직: 외부 API 호출은 ExternalVehicleApiService에 위임
return externalVehicleApiService.getBasicInfo(request);
}
/**
* REST -
* : LedgerRequest API
*/
@Override
public LedgerResponse getLedgerInfo(LedgerRequest request) {
// 중요 로직: 외부 API 호출은 ExternalVehicleApiService에 위임
return externalVehicleApiService.getLedgerInfo(request);
}
}

@ -1,21 +0,0 @@
package go.kr.project.api.internal.client;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import org.springframework.http.ResponseEntity;
/**
* API .
*
* <p>
* .</p>
*/
public interface GovernmentApi {
ResponseEntity<Envelope<BasicResponse>> callBasic(Envelope<BasicRequest> envelope);
ResponseEntity<Envelope<LedgerResponse>> callLedger(Envelope<LedgerRequest> envelope);
}

@ -1,627 +0,0 @@
package go.kr.project.api.internal.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.internal.gpki.GpkiService;
import go.kr.project.api.internal.util.TxIdUtil;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
/**
* API
*
* <p> API
* . HTTP , , API
* .</p>
*
* <h3> :</h3>
* <ul>
* <li> API HTTP </li>
* <li>GPKI() / </li>
* <li> HTTP </li>
* <li>/ JSON /</li>
* <li> ID(tx_id) </li>
* <li> HTTP </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>Adapter : API </li>
* <li>Template Method : callModel </li>
* <li>Dependency Injection: </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>GPKI ( )</li>
* <li>API </li>
* <li> (INFO_SYS_ID, REGION_CODE ) </li>
* </ul>
*
* @see RestTemplate
* @see GpkiService
* @see VmisProperties
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class GovernmentApiClient implements GovernmentApi {
/**
* Spring RestTemplate ( RestTemplate )
*
* <p>HTTP .
* Spring Bean , :</p>
* <ul>
* <li>Connection Timeout </li>
* <li>Read Timeout </li>
* <li>Connection Pool </li>
* <li>Rate Limiting ( )</li>
* <li> (Jackson for JSON)</li>
* <li> (, )</li>
* </ul>
*/
private final RestTemplate restTemplate;
/**
* VMIS
*
* <p>application.yml application.properties .
* :</p>
* <ul>
* <li> API URL (, , )</li>
* <li>API </li>
* <li> (INFO_SYS_ID, REGION_CODE )</li>
* <li>GPKI ( ID )</li>
* </ul>
*/
private final VmisProperties props;
/**
* GPKI()
*
* <p>24 .
* :</p>
* <ul>
* <li> ( )</li>
* <li> ( )</li>
* <li> </li>
* <li> </li>
* </ul>
*
* <p> (Plain Text) .</p>
*/
private final GpkiService gpkiService;
/**
* Jackson ObjectMapper
*
* <p>Java JSON .
* :</p>
* <ul>
* <li> JSON (Serialization)</li>
* <li> JSON Java (Deserialization)</li>
* <li> (TypeReference )</li>
* <li>/ </li>
* <li>null </li>
* </ul>
*
* <p>Spring Boot ObjectMapper
* ( , ) .</p>
*/
private final ObjectMapper objectMapper;
/**
*
*
* <p> API .
* API .</p>
*
* <h4> :</h4>
* <ul>
* <li>BASIC:
* <ul>
* <li> (, , ) </li>
* <li> </li>
* <li> </li>
* </ul>
* </li>
* <li>LEDGER: ()
* <ul>
* <li> </li>
* <li>, , </li>
* <li> </li>
* </ul>
* </li>
* </ul>
*/
public enum ServiceType {
/**
* Basic service type.
*/
BASIC,
/**
* Ledger service type.
*/
LEDGER }
/**
* HTTP
*
* <p> API HTTP private .
* .</p>
*
* <h3> :</h3>
* <table border="1">
* <tr>
* <th></th>
* <th></th>
* <th></th>
* <th></th>
* </tr>
* <tr>
* <td>Content-Type</td>
* <td> </td>
* <td>application/json; charset=UTF-8</td>
* <td></td>
* </tr>
* <tr>
* <td>Accept</td>
* <td> </td>
* <td>application/json</td>
* <td></td>
* </tr>
* <tr>
* <td>gpki_yn</td>
* <td>GPKI (Y/N)</td>
* <td>Y</td>
* <td></td>
* </tr>
* <tr>
* <td>tx_id</td>
* <td> ID ( )</td>
* <td>20250104123045_abc123</td>
* <td></td>
* </tr>
* <tr>
* <td>cert_server_id</td>
* <td> </td>
* <td>VMIS_SERVER_01</td>
* <td></td>
* </tr>
* <tr>
* <td>api_key</td>
* <td> API </td>
* <td>abc123def456...</td>
* <td></td>
* </tr>
* <tr>
* <td>cvmis_apikey</td>
* <td>CVMIS API </td>
* <td>xyz789uvw012...</td>
* <td></td>
* </tr>
* <tr>
* <td>INFO_SYS_ID</td>
* <td> </td>
* <td>VMIS_SEOUL</td>
* <td></td>
* </tr>
* </table>
*
* <h3> :</h3>
* <ul>
* <li>Content-Type UTF-8 </li>
* <li> </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>API </li>
* <li> API </li>
* <li> (BASIC, LEDGER) API </li>
* </ul>
*
* @param svc (API , )
* @param txId ID
* @return HttpHeaders HTTP
*/
private HttpHeaders buildHeaders(VmisProperties.GovProps.Service svc, String txId) {
// 1. 빈 HttpHeaders 객체 생성
HttpHeaders headers = new HttpHeaders();
// 2. Content-Type 설정
// UTF-8 인코딩을 명시하여 한글 데이터가 올바르게 전송되도록 함
headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
// 3. Accept 헤더 설정
// 서버에게 JSON 형식의 응답을 요청함
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
// 4. GPKI 암호화 사용 여부
// 정부 서버가 요청 바디 복호화 여부를 결정하는 데 사용
headers.add("gpki_yn", gpkiService.isEnabled() ? "Y" : "N");
// 5. 트랜잭션 ID
// 요청 추적, 로그 연관, 문제 해결 시 사용
headers.add("tx_id", txId);
// 6. 인증서 서버 ID
// GPKI 인증서를 발급받은 서버의 식별자
headers.add("cert_server_id", props.getGpki().getCertServerId());
// 7. API 인증 키
// 서비스별로 다른 API 키 사용 가능 (BASIC과 LEDGER 각각)
headers.add("api_key", svc.getApiKey());
// 8. CVMIS API 키
// CVMIS(Car Vehicle Management Information System) 전용 API 키
headers.add("cvmis_apikey", svc.getCvmisApikey());
// 구성 완료된 헤더 반환
return headers;
}
/**
* API
*
* <p> .
* {@link #callModel} .</p>
*
* <h3>:</h3>
* <ul>
* <li> </li>
* <li>/ Envelope </li>
* <li>Jackson TypeReference </li>
* <li>API DB </li>
* </ul>
*
* <h3> :</h3>
* <ol>
* <li> DB INSERT ( )</li>
* <li> API </li>
* <li> DB UPDATE</li>
* <li> DB UPDATE</li>
* </ol>
*
* <h3> :</h3>
* <pre>
* BasicRequest request = new BasicRequest();
* request.setVehicleNo("12가3456");
*
* Envelope&lt;BasicRequest&gt; envelope = new Envelope&lt;&gt;();
* envelope.setData(request);
*
* ResponseEntity&lt;Envelope&lt;BasicResponse&gt;&gt; response = govClient.callBasic(envelope);
* BasicResponse data = response.getBody().getData();
* </pre>
*
* @param envelope Envelope
* @return ResponseEntity&lt;Envelope&lt;BasicResponse&gt;&gt;
*/
public ResponseEntity<Envelope<BasicResponse>> callBasic(Envelope<BasicRequest> envelope) {
// 순수한 전송 책임만 수행: DB 로깅은 서비스 레이어에서 처리
return callModel(ServiceType.BASIC, envelope, new TypeReference<Envelope<BasicResponse>>(){});
}
/**
* () API
*
* <p> .
* {@link #callModel} .</p>
*
* <h3>:</h3>
* <ul>
* <li> </li>
* <li>/ Envelope </li>
* <li>Jackson TypeReference </li>
* </ul>
*
* <h3> :</h3>
* <pre>
* LedgerRequest request = new LedgerRequest();
* request.setVehicleNo("12가3456");
* request.setOwnerName("홍길동");
*
* Envelope&lt;LedgerRequest&gt; envelope = new Envelope&lt;&gt;();
* envelope.setData(request);
*
* ResponseEntity&lt;Envelope&lt;LedgerResponse&gt;&gt; response = govClient.callLedger(envelope);
* LedgerResponse data = response.getBody().getData();
* </pre>
*
* @param envelope Envelope
* @return ResponseEntity&lt;Envelope&lt;LedgerResponse&gt;&gt;
*/
public ResponseEntity<Envelope<LedgerResponse>> callLedger(Envelope<LedgerRequest> envelope) {
// TypeReference를 사용하여 제네릭 타입 정보 전달
// 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결
return callModel(ServiceType.LEDGER, envelope, new TypeReference<Envelope<LedgerResponse>>(){});
}
/**
* API ( )
*
* <p> API .
* Java JSON , ,
* Java .</p>
*
* <h3>Template Method :</h3>
* <ul>
* <li> Template Method 릿 </li>
* <li> </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>&lt;TReq&gt;: (BasicRequest LedgerRequest)</li>
* <li>&lt;TResp&gt;: (BasicResponse LedgerResponse)</li>
* <li> </li>
* </ul>
*
* <h3> ():</h3>
* <ol>
* <li><b> :</b>
* <ul><li> BASIC LEDGER </li></ul>
* </li>
* <li><b>URL ID :</b>
* <ul><li> API URL </li>
* <li> ID </li></ul>
* </li>
* <li><b> (Serialization):</b>
* <ul><li>Java (Envelope&lt;TReq&gt;) JSON </li>
* <li>ObjectMapper.writeValueAsString() </li></ul>
* </li>
* <li><b> :</b>
* <ul><li>buildHeaders() </li>
* <li> </li></ul>
* </li>
* <li><b>GPKI ():</b>
* <ul><li>GPKI JSON </li>
* <li>gpkiEncrypt() </li></ul>
* </li>
* <li><b>HTTP :</b>
* <ul><li>RestTemplate.exchange() POST </li>
* <li> </li></ul>
* </li>
* <li><b>GPKI ():</b>
* <ul><li> (2xx) GPKI </li>
* <li>gpkiDecrypt() </li></ul>
* </li>
* <li><b> (Deserialization):</b>
* <ul><li>JSON Java (Envelope&lt;TResp&gt;) </li>
* <li>TypeReference </li></ul>
* </li>
* <li><b> :</b>
* <ul><li>ResponseEntity HTTP </li></ul>
* </li>
* </ol>
*
* <h3> (3):</h3>
* <ol>
* <li><b>HttpStatusCodeException (HTTP ):</b>
* <ul>
* <li> API 4xx 5xx </li>
* <li> Envelope </li>
* <li> Envelope </li>
* <li> (WARN )</li>
* </ul>
* </li>
* <li><b>JSON :</b>
* <ul>
* <li> JSON </li>
* <li>RuntimeException </li>
* </ul>
* </li>
* <li><b> :</b>
* <ul>
* <li> , </li>
* <li>RuntimeException </li>
* </ul>
* </li>
* </ol>
*
* <h3>TypeReference :</h3>
* <p>Java (Type Erasure)
* {@code objectMapper.readValue(json, Envelope<TResp>.class)}
* . TypeReference
* Jackson .</p>
*
* <h3> :</h3>
* <ul>
* <li> : [GOV-REQ] url, tx_id, gpki, length</li>
* <li> : [GOV-ERR] status, body</li>
* </ul>
*
* @param <TReq>
* @param <TResp>
* @param type (BASIC LEDGER)
* @param envelope Envelope
* @param respType TypeReference ( )
* @return ResponseEntity&lt;Envelope&lt;TResp&gt;&gt; ResponseEntity
* @throws RuntimeException JSON / , , GPKI
*/
private <TReq, TResp> ResponseEntity<Envelope<TResp>> callModel(ServiceType type,
Envelope<TReq> envelope,
TypeReference<Envelope<TResp>> respType) {
// 1. 서비스 타입에 따른 설정 로드
VmisProperties.GovProps gov = props.getGov();
VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC)
? gov.getServices().getBasic()
: gov.getServices().getLedger();
// 2. URL 및 트랜잭션 ID 생성
String url = gov.buildServiceUrl(svc.getPath());
String txId = TxIdUtil.generate();
try {
// 3. 직렬화: Java 객체 → JSON 문자열
// ObjectMapper가 Envelope 객체를 JSON으로 변환
// 날짜, null 값 등의 처리는 ObjectMapper 설정에 따름
String jsonBody = objectMapper.writeValueAsString(envelope);
// 4. HTTP 헤더 구성
HttpHeaders headers = buildHeaders(svc, txId);
// 5. GPKI 암호화 처리
String bodyToSend = jsonBody;
if (gpkiService.isEnabled()) {
// JSON 평문을 암호화된 문자열로 변환
bodyToSend = gpkiEncrypt(jsonBody);
}
// 6. HTTP 엔티티 생성 (헤더 + 바디)
HttpEntity<String> request = new HttpEntity<>(bodyToSend, headers);
// 7. 요청 로그 기록
log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0);
// 8. 실제 HTTP POST 요청 전송
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
String respBody = response.getBody();
// 9. GPKI 복호화 처리 (성공 응답인 경우만)
if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) {
// 암호화된 응답을 평문 JSON으로 복호화
respBody = gpkiDecrypt(respBody);
}
// 10. 역직렬화: JSON 문자열 → Java 객체
// TypeReference를 사용하여 제네릭 타입 정보 전달
Envelope<TResp> mapped = objectMapper.readValue(respBody, respType);
// 11. 응답 반환 (상태 코드, 헤더, 바디 모두 포함)
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(mapped);
} catch (HttpStatusCodeException ex) {
// HTTP 에러 처리 (4xx, 5xx)
log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString());
// 에러 응답 바디 파싱 시도
// 정부 API는 에러 응답도 Envelope 형식으로 반환할 수 있음
Envelope<TResp> empty = new Envelope<>();
try {
// 에러 응답을 Envelope 객체로 파싱
Envelope<TResp> parsed = objectMapper.readValue(ex.getResponseBodyAsString(), respType);
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(parsed);
} catch (Exception parseEx) {
// 파싱 실패 시 빈 Envelope 반환
// 호출자는 HTTP 상태 코드로 에러 판단 가능
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(empty);
}
} catch (Exception e) {
// 기타 모든 예외 (네트워크 오류, JSON 파싱 오류, GPKI 오류 등)
// RuntimeException으로 래핑하여 상위로 전파
// Spring의 @ExceptionHandler에서 처리 가능
throw new RuntimeException("정부 API 호출 중 오류", e);
}
}
/**
* GPKI
*
* <p> JSON GPKI() .
* RuntimeException .</p>
*
* <h3> :</h3>
* <ol>
* <li> JSON </li>
* <li> </li>
* <li> Base64 </li>
* <li>Base64 </li>
* </ol>
*
* <h3> :</h3>
* <ul>
* <li> : GPKI </li>
* <li> : </li>
* <li> : Base64 </li>
* <li> RuntimeException </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> ()</li>
* <li> </li>
* <li> </li>
* </ul>
*
* @param jsonBody JSON
* @return String Base64
* @throws RuntimeException GPKI
*/
private String gpkiEncrypt(String jsonBody) {
try {
// GpkiService에 암호화 위임
// 실제 암호화 로직은 GpkiService가 캡슐화
return gpkiService.encrypt(jsonBody);
} catch (Exception e) {
// 암호화 실패는 치명적 오류
// 평문 데이터를 전송할 수 없으므로 즉시 중단
throw new RuntimeException("GPKI 암호화 실패", e);
}
}
/**
* GPKI
*
* <p> GPKI() .
* RuntimeException .</p>
*
* <h3> :</h3>
* <ol>
* <li>Base64 </li>
* <li> </li>
* <li> UTF-8 </li>
* <li> JSON </li>
* </ol>
*
* <h3> :</h3>
* <ul>
* <li> : GPKI </li>
* <li> : </li>
* <li> : Base64 </li>
* <li> : UTF-8 </li>
* <li> RuntimeException </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> ()</li>
* <li> </li>
* <li> ( )</li>
* </ul>
*
* @param cipher Base64
* @return String JSON
* @throws RuntimeException GPKI
*/
private String gpkiDecrypt(String cipher) {
try {
// GpkiService에 복호화 위임
// 실제 복호화 로직은 GpkiService가 캡슐화
return gpkiService.decrypt(cipher);
} catch (Exception e) {
// 복호화 실패는 치명적 오류
// 암호문을 해석할 수 없으므로 즉시 중단
throw new RuntimeException("GPKI 복호화 실패", e);
}
}
}

@ -1,20 +0,0 @@
package go.kr.project.api.internal.config;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.internal.gpki.GpkiService;
import go.kr.project.api.internal.gpki.NoopGpkiService;
import go.kr.project.api.internal.gpki.RealGpkiService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GpkiConfig {
@Bean
public GpkiService gpkiService(VmisProperties properties) {
if (properties.getGpki().isEnabledFlag()) {
return new RealGpkiService(properties);
}
return new NoopGpkiService();
}
}

@ -1,27 +0,0 @@
package go.kr.project.api.internal.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI vmisOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("VMIS Interface API")
.description("시군구연계 자동차 정보 인터페이스 API (자망연계)")
.version("v0.1.0")
.contact(new Contact().name("VMIS").email("support@example.com"))
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0.html")))
.externalDocs(new ExternalDocumentation()
.description("Reference")
.url(""));
}
}

@ -1,10 +0,0 @@
package go.kr.project.api.internal.config;
import go.kr.project.api.config.properties.VmisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(VmisProperties.class)
public class PropertiesConfig {
}

@ -1,7 +0,0 @@
package go.kr.project.api.internal.gpki;
public interface GpkiService {
String encrypt(String plain) throws Exception;
String decrypt(String cipher) throws Exception;
boolean isEnabled();
}

@ -1,18 +0,0 @@
package go.kr.project.api.internal.gpki;
public class NoopGpkiService implements GpkiService {
@Override
public String encrypt(String plain) {
return plain;
}
@Override
public String decrypt(String cipher) {
return cipher;
}
@Override
public boolean isEnabled() {
return false;
}
}

@ -1,41 +0,0 @@
package go.kr.project.api.internal.gpki;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.internal.util.GpkiCryptoUtil;
/**
* Real GPKI service backed by native GPKI JNI via legacy NewGpkiUtil wrapper.
* Uses YAML-configured paths and options in {@link VmisProperties.GpkiProps}.
*/
public class RealGpkiService implements GpkiService {
private final VmisProperties props;
private final GpkiCryptoUtil crypto;
public RealGpkiService(VmisProperties props) {
this.props = props;
try {
this.crypto = GpkiCryptoUtil.from(props.getGpki());
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize GPKI (JNI) util. Check YAML paths/passwords and license.", e);
}
}
@Override
public String encrypt(String plain) throws Exception {
String charset = props.getGpki().getCharset();
String targetId = props.getGpki().getTargetServerId();
return crypto.encryptToBase64(plain, targetId, charset);
}
@Override
public String decrypt(String cipher) throws Exception {
String charset = props.getGpki().getCharset();
return crypto.decryptFromBase64(cipher, charset);
}
@Override
public boolean isEnabled() {
return true;
}
}

@ -1,26 +0,0 @@
package go.kr.project.api.internal.mapper;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
import org.apache.ibatis.annotations.Mapper;
/**
* () Mapper
*/
@Mapper
public interface VmisCarLedgerFrmbkMapper {
// ID 시퀀스
String selectNextCarLedgerFrmbkId();
String selectNextCarLedgerFrmbkDtlId();
// 마스터 INSERT/UPDATE/SELECT
int insertCarLedgerFrmbk(VmisCarLedgerFrmbkVO vo);
int updateCarLedgerFrmbk(VmisCarLedgerFrmbkVO vo);
VmisCarLedgerFrmbkVO selectCarLedgerFrmbkById(String carLedgerFrmbkId);
// 상세 INSERT (단건)
int insertCarLedgerFrmbkDtl(VmisCarLedgerFrmbkDtlVO vo);
// 편의: 상세 일괄 (MyBatis foreach를 XML에서 사용할 수도 있으나, 여기서는 단건 호출을 반복)
}

@ -1,28 +0,0 @@
package go.kr.project.api.internal.service;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.response.BasicResponse;
import org.springframework.http.ResponseEntity;
/**
*
*
* <p>API .</p>
* <ul>
* <li> </li>
* <li> </li>
* <li> API </li>
* <li> </li>
* </ul>
*/
public interface VmisCarBassMatterInqireService {
/**
*
*
* @param envelope Envelope
* @return Envelope
*/
ResponseEntity<Envelope<BasicResponse>> basic(Envelope<BasicRequest> envelope);
}

@ -1,21 +0,0 @@
package go.kr.project.api.internal.service;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.LedgerResponse;
import org.springframework.http.ResponseEntity;
/**
* ()
* - , API ,
*/
public interface VmisCarLedgerFrmbkService {
/**
* ()
*
* @param envelope Envelope
* @return Envelope
*/
ResponseEntity<Envelope<LedgerResponse>> ledger(Envelope<LedgerRequest> envelope);
}

@ -1,85 +0,0 @@
package go.kr.project.api.internal.service;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Populates incoming request models with values from YAML configuration.
* Unconditionally overwrites the listed fields per requirement:
* - INFO_SYS_ID, INFO_SYS_IP, SIGUNGU_CODE
* - CNTC_INFO_CODE (service specific)
* - CHARGER_ID, CHARGER_IP, CHARGER_NM
*/
@Slf4j
@Component
public class VmisRequestEnricher {
private final VmisProperties props;
public VmisRequestEnricher(VmisProperties props) {
this.props = props;
}
public void enrichBasic(Envelope<BasicRequest> envelope) {
if (envelope == null || envelope.getData() == null) return;
VmisProperties.SystemProps sys = props.getSystem();
String cntc = props.getGov().getServices().getBasic().getCntcInfoCode();
for (BasicRequest req : envelope.getData()) {
if (req == null) continue;
req.setInfoSysId(sys.getInfoSysId());
req.setInfoSysIp(sys.getInfoSysIp());
req.setSigunguCode(sys.getSigunguCode());
req.setCntcInfoCode(cntc);
req.setChargerId(sys.getChargerId());
req.setChargerIp(sys.getChargerIp());
req.setChargerNm(sys.getChargerNm());
// 조회구분코드 자동 설정: VHRNO가 있으면 "3" (자동차번호), VIN이 있으면 "2" (차대번호)
if (req.getInqireSeCode() == null) {
if (req.getVhrno() != null && !req.getVhrno().trim().isEmpty()) {
req.setInqireSeCode("3"); // 자동차번호로 조회
} else if (req.getVin() != null && !req.getVin().trim().isEmpty()) {
req.setInqireSeCode("2"); // 차대번호로 조회
}
}
}
log.debug("[ENRICH] basic: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
sys.getInfoSysId(), sys.getInfoSysIp(), sys.getSigunguCode(), cntc);
}
public void enrichLedger(Envelope<LedgerRequest> envelope) {
if (envelope == null || envelope.getData() == null) return;
VmisProperties.SystemProps sys = props.getSystem();
String cntc = props.getGov().getServices().getLedger().getCntcInfoCode();
for (LedgerRequest req : envelope.getData()) {
if (req == null) continue;
req.setInfoSysId(sys.getInfoSysId());
req.setInfoSysIp(sys.getInfoSysIp());
req.setSigunguCode(sys.getSigunguCode());
req.setCntcInfoCode(cntc);
req.setChargerId(sys.getChargerId());
req.setChargerIp(sys.getChargerIp());
req.setChargerNm(sys.getChargerNm());
// 고정값 설정 (값이 없는 경우에만 설정)
if (req.getOnesInformationOpen() == null || req.getOnesInformationOpen().isEmpty()) {
req.setOnesInformationOpen("1"); // 개인정보공개 (소유자공개)
}
if (req.getRouteSeCode() == null || req.getRouteSeCode().isEmpty()) {
req.setRouteSeCode("3"); // 경로구분코드
}
if (req.getDetailExpression() == null || req.getDetailExpression().isEmpty()) {
req.setDetailExpression("1"); // 내역표시 (전체내역)
}
if (req.getInqireSeCode() == null || req.getInqireSeCode().isEmpty()) {
req.setInqireSeCode("1"); // 조회구분코드 (열람)
}
}
log.debug("[ENRICH] ledger: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
sys.getInfoSysId(), sys.getInfoSysIp(), sys.getSigunguCode(), cntc);
}
}

@ -1,213 +0,0 @@
package go.kr.project.api.internal.service.impl;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.internal.service.VmisCarBassMatterInqireService;
import go.kr.project.api.internal.service.VmisCarLedgerFrmbkService;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.service.VehicleInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* VMIS
*
* <p> REST API VMIS
* . .</p>
*
* <h3> :</h3>
* <pre>
* # application.yml
* vmis:
* integration:
* mode: internal
* </pre>
*
* <h3> :</h3>
* <ol>
* <li> BasicRequest, LedgerRequest </li>
* <li>VmisCarBassMatterInqireService.basic() ()</li>
* <li>VmisCarLedgerFrmbkService.ledger() ()</li>
* <li>BasicResponse, LedgerResponse VehicleApiResponseVO </li>
* <li>VehicleApiResponseVO </li>
* </ol>
*
* @see VehicleInfoService
* @see VmisCarBassMatterInqireService
* @see VmisCarLedgerFrmbkService
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal")
public class InternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl implements VehicleInfoService {
private final VmisCarBassMatterInqireService carBassMatterInqireService;
private final VmisCarLedgerFrmbkService carLedgerFrmbkService;
private final go.kr.project.carInspectionPenalty.history.service.VehicleApiHistoryService vehicleApiHistoryService;
@Override
public VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest) {
String vehicleNumber = basicRequest.getVhrno();
log.info("[Internal Mode] 차량 정보 조회 시작 - 차량번호: {}, 부과기준일: {}, 조회구분: {}",
vehicleNumber, basicRequest.getLevyStdde(), basicRequest.getInqireSeCode());
VehicleApiResponseVO response = new VehicleApiResponseVO();
response.setVhrno(vehicleNumber);
try {
// 1. 차량 기본정보 조회
// 중요 로직: BasicRequest 전체를 사용하여 조회 (RequestEnricher가 나머지 채움)
BasicResponse basicInfo = getBasicInfo(basicRequest);
response.setBasicInfo(basicInfo);
// 2. 자동차 등록원부 조회
// 중요 로직: 통합 조회 시에는 차량번호와 기본정보를 바탕으로 LedgerRequest 생성 (RequestEnricher가 나머지 채움)
LedgerRequest ledgerRequest = new LedgerRequest();
ledgerRequest.setVhrno(vehicleNumber);
ledgerRequest.setOnesInformationOpen("1"); //개인정보공개 {1:소유자공개, 2:비공개, 3:비공개(주민등록번호), 4:비공개(사용본거지)}
// basicInfo에서 민원인 정보 가져오기
if (basicInfo != null && basicInfo.getRecord() != null && !basicInfo.getRecord().isEmpty()) {
BasicResponse.Record record = basicInfo.getRecord().get(0);
ledgerRequest.setCpttrNm(record.getMberNm()); // 민원인성명
ledgerRequest.setCpttrIhidnum(record.getMberSeNo()); // 민원인주민번호
}
// 고정값 설정
ledgerRequest.setCpttrLegaldongCode(null); // 민원인법정동코드
LedgerResponse ledgerInfo = getLedgerInfo(ledgerRequest);
response.setLedgerInfo(ledgerInfo);
// 3. 결과 검증
if (basicInfo != null && ApiConstant.CNTC_RESULT_CODE_SUCCESS.equals(basicInfo.getCntcResultCode())) {
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() : "조회 실패");
log.warn("[Internal Mode] 차량번호 {} 조회 실패 - {}", vehicleNumber, response.getMessage());
}
} catch (Exception e) {
response.setSuccess(false);
response.setMessage("내부 API 호출 오류: " + e.getMessage());
log.error("[Internal Mode] 차량번호 {} 내부 API 호출 중 오류 발생", vehicleNumber, e);
}
return response;
}
/**
* ( )
* : BasicRequest
*
* @param request (, , )
* @return
*/
@Override
public BasicResponse getBasicInfo(BasicRequest request) {
log.debug("[Internal Mode] 차량 기본정보 조회 - 차량번호: {}", request.getVhrno());
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
Envelope<BasicRequest> requestEnvelope = new Envelope<BasicRequest>();
requestEnvelope.setData(Collections.singletonList(request));
try {
// 내부 서비스 호출
ResponseEntity<Envelope<BasicResponse>> responseEntity =
carBassMatterInqireService.basic(requestEnvelope);
if (responseEntity.getBody() != null &&
responseEntity.getBody().getData() != null &&
!responseEntity.getBody().getData().isEmpty()) {
return responseEntity.getBody().getData().get(0);
}
log.warn("[Internal Mode] 차량 기본정보 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
return null;
} catch (Exception e) {
log.error("[Internal Mode] 차량 기본정보 조회 실패 - 차량번호: {}", request.getVhrno(), e);
throw new RuntimeException("차량 기본정보 조회 실패: " + e.getMessage(), e);
}
}
/**
* () ( )
* : LedgerRequest
*
* @param request (, , )
* @return
*/
@Override
public LedgerResponse getLedgerInfo(LedgerRequest request) {
log.debug("[Internal Mode] 자동차 등록원부 조회 - 차량번호: {}", request.getVhrno());
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
Envelope<LedgerRequest> requestEnvelope = new Envelope<LedgerRequest>();
requestEnvelope.setData(Collections.singletonList(request));
try {
// 내부 서비스 호출
ResponseEntity<Envelope<LedgerResponse>> responseEntity =
carLedgerFrmbkService.ledger(requestEnvelope);
if (responseEntity.getBody() != null &&
responseEntity.getBody().getData() != null &&
!responseEntity.getBody().getData().isEmpty()) {
return responseEntity.getBody().getData().get(0);
}
log.warn("[Internal Mode] 자동차 등록원부 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
return null;
} catch (Exception e) {
log.error("[Internal Mode] 자동차 등록원부 조회 실패 - 차량번호: {}", request.getVhrno(), e);
throw new RuntimeException("자동차 등록원부 조회 실패: " + e.getMessage(), e);
}
}
/**
*
*/
private int countSuccessful(List<VehicleApiResponseVO> responses) {
int count = 0;
for (VehicleApiResponseVO response : responses) {
if (response.isSuccess()) {
count++;
}
}
return count;
}
}

@ -1,88 +0,0 @@
package go.kr.project.api.internal.service.impl;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.internal.client.GovernmentApi;
import go.kr.project.api.internal.service.VmisCarBassMatterInqireService;
import go.kr.project.api.internal.service.VmisRequestEnricher;
import go.kr.project.api.internal.util.ExceptionDetailUtil;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
import go.kr.project.api.service.VmisCarBassMatterInqireLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
*
*
* <p>API .</p>
* <ul>
* <li> : createInitialRequest() - 퀀 ID INSERT</li>
* <li> : updateResponse() - UPDATE</li>
* </ul>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VmisCarBassMatterInqireServiceImpl extends EgovAbstractServiceImpl implements VmisCarBassMatterInqireService {
private final GovernmentApi governmentApi;
private final VmisRequestEnricher enricher;
private final VmisCarBassMatterInqireLogService logService;
/**
* : -> -> -> .
*/
@Override
@Transactional
public ResponseEntity<Envelope<BasicResponse>> basic(Envelope<BasicRequest> envelope) {
// 1) 요청 보강
enricher.enrichBasic(envelope);
String generatedId = null;
try {
// 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
if (envelope.getData() != null && !envelope.getData().isEmpty()) {
BasicRequest req = envelope.getData().get(0);
VmisCarBassMatterInqireVO logEntity = VmisCarBassMatterInqireVO.fromRequest(req);
generatedId = logService.createInitialRequestNewTx(logEntity);
}
// 3) 외부 API 호출
ResponseEntity<Envelope<BasicResponse>> response = governmentApi.callBasic(envelope);
// 4) 응답 로그 업데이트
// 원본 소스, 정상적인 호출, 리턴(에러 리턴포함) 일 경우에만 에러 로그 남김
if (generatedId != null && response.getBody() != null) {
VmisCarBassMatterInqireVO update = VmisCarBassMatterInqireVO.fromResponse(generatedId, response.getBody());
if (update != null) {
logService.updateResponseNewTx(update);
}
}
return response;
} catch (Exception e) {
// 5) 오류 로그 업데이트
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
VmisCarBassMatterInqireVO errorLog = VmisCarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId) // 자동차기본사항조회 ID (PK)
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR) // 연계결과코드 (에러)
.cntcResultDtls(detail) // 연계결과상세 (에러 메시지)
.build();
logService.updateResponseNewTx(errorLog);
log.error("[BASIC-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[BASIC-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
throw e;
}
}
}

@ -1,90 +0,0 @@
package go.kr.project.api.internal.service.impl;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.internal.client.GovernmentApi;
import go.kr.project.api.internal.service.VmisCarLedgerFrmbkService;
import go.kr.project.api.internal.service.VmisRequestEnricher;
import go.kr.project.api.internal.util.ExceptionDetailUtil;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
import go.kr.project.api.service.VmisCarLedgerFrmbkLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* () ()
* - , API ,
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VmisCarLedgerFrmbkServiceImpl extends EgovAbstractServiceImpl implements VmisCarLedgerFrmbkService {
private final GovernmentApi governmentApi;
private final VmisRequestEnricher enricher;
private final VmisCarLedgerFrmbkLogService logService;
/**
* () : -> (TX) -> -> (/, TX) -> (TX).
*/
@Override
@Transactional
public ResponseEntity<Envelope<LedgerResponse>> ledger(Envelope<LedgerRequest> envelope) {
// 1) 요청 보강
enricher.enrichLedger(envelope);
String generatedId = null;
try {
// 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
if (envelope.getData() != null && !envelope.getData().isEmpty()) {
LedgerRequest req = envelope.getData().get(0);
VmisCarLedgerFrmbkVO init = VmisCarLedgerFrmbkVO.fromRequest(req);
generatedId = logService.createInitialRequestNewTx(init);
}
// 3) 외부 API 호출
ResponseEntity<Envelope<LedgerResponse>> response = governmentApi.callLedger(envelope);
// 4) 응답 로그 업데이트 (마스터 + 상세)
if (generatedId != null && response.getBody() != null &&
response.getBody().getData() != null && !response.getBody().getData().isEmpty()) {
LedgerResponse body = response.getBody().getData().get(0);
VmisCarLedgerFrmbkVO masterUpdate = VmisCarLedgerFrmbkVO.fromResponseMaster(generatedId, body);
logService.updateResponseNewTx(masterUpdate);
List<VmisCarLedgerFrmbkDtlVO> details = VmisCarLedgerFrmbkDtlVO.listFromResponse(body, generatedId);
if (details != null && !details.isEmpty()) {
logService.saveDetailsNewTx(generatedId, details);
}
}
return response;
} catch (Exception e) {
// 5) 오류 로그 업데이트
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
VmisCarLedgerFrmbkVO errorLog = VmisCarLedgerFrmbkVO.builder()
.carLedgerFrmbkId(generatedId)
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR)
.cntcResultDtls(detail)
.build();
logService.updateResponseNewTx(errorLog);
log.error("[LEDGER-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[LEDGER-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
throw e;
}
}
}

@ -1,98 +0,0 @@
package go.kr.project.api.internal.util;
import go.kr.project.api.config.properties.VmisProperties;
import lombok.Getter;
import lombok.Setter;
/**
* Wrapper utility around legacy {@link NewGpkiUtil} using configuration from YAML.
*
* Notes:
* - Place this class under src/main/java/util as requested.
* - Uses Lombok for getters/setters.
*/
@Getter
@Setter
public class GpkiCryptoUtil {
private String gpkiLicPath;
private Boolean ldap; // null -> legacy default
private String certFilePath;
private String envCertFilePathName;
private String envPrivateKeyFilePathName;
private String envPrivateKeyPasswd;
private String sigCertFilePathName;
private String sigPrivateKeyFilePathName;
private String sigPrivateKeyPasswd;
private String myServerId; // equals to certServerId (INFO system server cert id)
private String targetServerIdList; // comma joined list (can be single id)
private transient NewGpkiUtil delegate;
public static GpkiCryptoUtil from(VmisProperties.GpkiProps props) throws Exception {
GpkiCryptoUtil util = new GpkiCryptoUtil();
util.setGpkiLicPath(props.getGpkiLicPath());
util.setLdap(props.getLdap());
util.setCertFilePath(props.getCertFilePath());
util.setEnvCertFilePathName(props.getEnvCertFilePathName());
util.setEnvPrivateKeyFilePathName(props.getEnvPrivateKeyFilePathName());
util.setEnvPrivateKeyPasswd(props.getEnvPrivateKeyPasswd());
util.setSigCertFilePathName(props.getSigCertFilePathName());
util.setSigPrivateKeyFilePathName(props.getSigPrivateKeyFilePathName());
util.setSigPrivateKeyPasswd(props.getSigPrivateKeyPasswd());
util.setMyServerId(props.getCertServerId());
// Accept single targetServerId but allow list if provided by YAML in future
util.setTargetServerIdList(props.getTargetServerId());
util.initialize();
return util;
}
public void initialize() throws Exception {
NewGpkiUtil g = new NewGpkiUtil();
if (gpkiLicPath != null) g.setGpkiLicPath(gpkiLicPath);
if (ldap != null) g.setIsLDAP(ldap);
if (certFilePath != null) g.setCertFilePath(certFilePath);
if (envCertFilePathName != null) g.setEnvCertFilePathName(envCertFilePathName);
if (envPrivateKeyFilePathName != null) g.setEnvPrivateKeyFilePathName(envPrivateKeyFilePathName);
if (envPrivateKeyPasswd != null) g.setEnvPrivateKeyPasswd(envPrivateKeyPasswd);
if (sigCertFilePathName != null) g.setSigCertFilePathName(sigCertFilePathName);
if (sigPrivateKeyFilePathName != null) g.setSigPrivateKeyFilePathName(sigPrivateKeyFilePathName);
if (sigPrivateKeyPasswd != null) g.setSigPrivateKeyPasswd(sigPrivateKeyPasswd);
if (myServerId != null) g.setMyServerId(myServerId);
if (targetServerIdList != null) g.setTargetServerIdList(targetServerIdList);
g.init();
this.delegate = g;
}
public String encryptToBase64(String plain, String targetServerId, String charset) throws Exception {
ensureInit();
byte[] enc = delegate.encrypt(plain.getBytes(charset), targetServerId, true);
return delegate.encode(enc);
}
public String decryptFromBase64(String base64, String charset) throws Exception {
ensureInit();
byte[] bin = delegate.decode(base64);
byte[] dec = delegate.decrypt(bin);
return new String(dec, charset);
}
public String signToBase64(String plain, String charset) throws Exception {
ensureInit();
byte[] sig = delegate.sign(plain.getBytes(charset));
return delegate.encode(sig);
}
public String verifyAndExtractBase64(String signedBase64, String charset) throws Exception {
ensureInit();
byte[] signed = delegate.decode(signedBase64);
byte[] data = delegate.validate(signed);
return new String(data, charset);
}
private void ensureInit() {
if (delegate == null) {
throw new IllegalStateException("GpkiCryptoUtil is not initialized. Call initialize() or from(props).");
}
}
}

@ -1,382 +0,0 @@
package go.kr.project.api.internal.util;
import com.gpki.gpkiapi.GpkiApi;
import com.gpki.gpkiapi.cert.X509Certificate;
import com.gpki.gpkiapi.crypto.PrivateKey;
import com.gpki.gpkiapi.storage.Disk;
import com.gpki.gpkiapi_jni;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class NewGpkiUtil {
byte[] myEnvCert, myEnvKey, mySigCert, mySigKey;
private Map<String, X509Certificate> targetServerCertMap = new HashMap<String, X509Certificate>();
// properties
private String myServerId;
private String targetServerIdList;
private String envCertFilePathName;
private String envPrivateKeyFilePathName;
private String envPrivateKeyPasswd;
private String sigCertFilePathName;
private String sigPrivateKeyFilePathName;
private String sigPrivateKeyPasswd;
private String certFilePath;
private String gpkiLicPath = ".";
private boolean isLDAP;
private boolean testGPKI = false;
public void init() throws Exception {
GpkiApi.init(gpkiLicPath);
gpkiapi_jni gpki = this.getGPKI();
if(log.isDebugEnabled()){
if(gpki.API_GetInfo()==0)
log.debug(gpki.sReturnString);
else
log.error(gpki.sDetailErrorString);
}
if(targetServerIdList!=null){
String certIdList[] = targetServerIdList.split(",");
for(int i = 0 ; i < certIdList.length ; i++){
String certId = certIdList[i].trim();
if(!certId.equals("")){
load(gpki, certId);
}
}
}
log.info("Loading gpki certificate : myServerId="
+ this.getMyServerId());
X509Certificate _myEnvCert = Disk.readCert(this
.getEnvCertFilePathName());
myEnvCert = _myEnvCert.getCert();
PrivateKey _myEnvKey = Disk.readPriKey(this
.getEnvPrivateKeyFilePathName(), this.getEnvPrivateKeyPasswd());
myEnvKey = _myEnvKey.getKey();
X509Certificate _mySigCert = Disk.readCert(this
.getSigCertFilePathName());
mySigCert = _mySigCert.getCert();
PrivateKey _mySigKey = Disk.readPriKey(this
.getSigPrivateKeyFilePathName(), this.getSigPrivateKeyPasswd());
mySigKey = _mySigKey.getKey();
//test my cert GPKI
if(testGPKI){
load(gpki, this.getMyServerId());
testGpki(gpki);
}
this.finish(gpki);
log.info("GpkiUtil initialized");
}
private void load(gpkiapi_jni gpki, String certId) throws Exception {
log.debug("Loading gpki certificate : targetServerId="+ certId);
X509Certificate cert = targetServerCertMap.get(certId);
if (cert != null) {
return;
}
if (isLDAP) {
// String ldapUrl = "ldap://10.1.7.140:389/cn=";
// String ldapUrl = "ldap://ldap.gcc.go.kr:389/cn=";
String ldapUrl = "ldap://10.1.7.118:389/cn="; // 행정망인 경우
// String ldapUrl = "ldap://152.99.57.127:389/cn="; // 인터넷망인 경우
String ldapUri;
if (certId.charAt(3) > '9') {
ldapUri = ",ou=Group of Server,o=Public of Korea,c=KR";
} else {
ldapUri = ",ou=Group of Server,o=Government of Korea,c=KR";
}
int ret = gpki.LDAP_GetAnyDataByURL("userCertificate;binary", ldapUrl + certId + ldapUri);
this.checkResult(ret, gpki);
cert = new X509Certificate(gpki.baReturnArray);
} else {
if(certFilePath != null){
cert = Disk.readCert(certFilePath + File.separator + certId + ".cer");
}else{
log.debug("not certFilePath");
}
}
targetServerCertMap.put(certId, cert);
}
private gpkiapi_jni getGPKI(){
gpkiapi_jni gpki = new gpkiapi_jni();
if(gpki.API_Init(gpkiLicPath) != 0){
log.error(gpki.sDetailErrorString);
}
return gpki;
}
private void finish(gpkiapi_jni gpki){
if(gpki.API_Finish() != 0){
log.error(gpki.sDetailErrorString);
}
}
public byte[] encrypt(byte[] plain, String certId , boolean load) throws Exception {
X509Certificate targetEnvCert = targetServerCertMap.get(certId);
if (targetEnvCert == null) {
throw new Exception("Certificate not found : targetServerId=" + certId);
}
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.CMS_MakeEnvelopedData(targetEnvCert.getCert(), plain,
gpkiapi_jni.SYM_ALG_NEAT_CBC);
checkResult(result, "Fail to encrypt message", gpki);
return gpki.baReturnArray;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
public byte[] encrypt(byte[] plain, String certId) throws Exception {
return encrypt(plain,certId , false);
}
public byte[] decrypt(byte[] encrypted) throws Exception {
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.CMS_ProcessEnvelopedData(myEnvCert, myEnvKey,
encrypted);
checkResult(result, "Fail to decrpyt message", gpki);
return gpki.baReturnArray;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
public byte[] sign(byte[] plain) throws Exception {
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.CMS_MakeSignedData(mySigCert, mySigKey, plain, null);
checkResult(result, "Fail to sign message", gpki);
return gpki.baReturnArray;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
public byte[] validate(byte[] signed) throws Exception {
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.CMS_ProcessSignedData(signed);
checkResult(result, "Fail to validate signed message", gpki);
return gpki.baData;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
public String encode(byte[] plain) throws Exception {
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.BASE64_Encode(plain);
checkResult(result, "Fail to encode message", gpki);
return gpki.sReturnString;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
public byte[] decode(String base64) throws Exception {
gpkiapi_jni gpki = this.getGPKI();
try{
int result = gpki.BASE64_Decode(base64);
checkResult(result, "Fail to decode base64 message", gpki);
return gpki.baReturnArray;
}catch(Exception ex){
throw ex;
}finally{
finish(gpki);
}
}
private void checkResult(int result, gpkiapi_jni gpki)throws Exception{
this.checkResult(result, null, gpki);
}
private void checkResult(int result ,String message, gpkiapi_jni gpki)throws Exception{
if( 0 != result){
if(null != gpki){
throw new Exception(message + " : gpkiErrorMessage=" + gpki.sDetailErrorString);
}else{
throw new Exception(message + " : gpkiErrorCode=" + result);
}
}
}
public void testGpki(gpkiapi_jni gpki) throws Exception{
//gpki test eng
log.info("=======================================================");
log.info("================ TEST GPKI START ======================");
log.info("=======================================================");
String original_Eng = "abc";
log.info("=== TEST ENG STRING: "+ original_Eng);
try {
byte[] encrypted = encrypt(original_Eng.getBytes(), myServerId);
log.info("=== TEST ENG ENCRYPT STRING: "+ encode(encrypted));
String decrypted = new String(decrypt(encrypted));
log.info("=== TEST ENG DECRYPT STRING: "+decrypted);
if (!original_Eng.equals(decrypted)) {
throw new Exception("GpkiUtil not initialized properly(english)");
}
log.info("=== TEST ENG: OK");
} catch (Exception e) {
log.warn("Gpki Test error(english)", e);
throw e;
}
//gpki test kor
String original = "한글테스트";
log.info("=== TEST KOR STRING: "+ original);
try {
byte[] encrypted = encrypt(original.getBytes(), myServerId);
log.info("=== TEST KOR ENCRYPT STRING: "+ encode(encrypted));
String decrypted = new String(decrypt(encrypted));
log.info("=== TEST KOR DECRYPT STRING: "+decrypted);
if (!original.equals(decrypted)) {
throw new Exception("GpkiUtil not initialized properly(korean)");
}
log.info("=== TEST KOR: OK");
} catch (Exception e) {
log.warn("Gpki Test error(korean)", e);
throw e;
}finally{
log.info("=======================================================");
log.info("================ TEST GPKI END ========================");
log.info("=======================================================");
}
}
public String getMyServerId() {
return myServerId;
}
public void setMyServerId(String myServerId) {
this.myServerId = myServerId.trim();
}
public String getEnvCertFilePathName() {
return envCertFilePathName;
}
public void setEnvCertFilePathName(String envCertFilePathName) {
this.envCertFilePathName = envCertFilePathName.trim();
}
public String getEnvPrivateKeyFilePathName() {
return envPrivateKeyFilePathName;
}
public void setEnvPrivateKeyFilePathName(String envPrivateKeyFilePathName) {
this.envPrivateKeyFilePathName = envPrivateKeyFilePathName.trim();
}
public String getEnvPrivateKeyPasswd() {
return envPrivateKeyPasswd;
}
public void setEnvPrivateKeyPasswd(String envPrivateKeyPasswd) {
this.envPrivateKeyPasswd = envPrivateKeyPasswd.trim();
}
public String getSigPrivateKeyPasswd() {
return sigPrivateKeyPasswd;
}
public void setSigPrivateKeyPasswd(String sigPrivateKeyPasswd) {
this.sigPrivateKeyPasswd = sigPrivateKeyPasswd.trim();
}
public String getSigCertFilePathName() {
return sigCertFilePathName;
}
public void setSigCertFilePathName(String sigCertFilePathName) {
this.sigCertFilePathName = sigCertFilePathName.trim();
}
public String getSigPrivateKeyFilePathName() {
return sigPrivateKeyFilePathName;
}
public void setSigPrivateKeyFilePathName(String sigPrivateKeyFilePathName) {
this.sigPrivateKeyFilePathName = sigPrivateKeyFilePathName.trim();
}
public boolean getIsLDAP() {
return isLDAP;
}
public void setIsLDAP(boolean isLDAP) {
this.isLDAP = isLDAP;
}
public String getCertFilePath() {
return certFilePath;
}
public void setCertFilePath(String certFilePath) {
this.certFilePath = certFilePath.trim();
}
public String getTargetServerIdList() {
return targetServerIdList;
}
public void setTargetServerIdList(String targetServerIdList) {
this.targetServerIdList = targetServerIdList;
}
public String getGpkiLicPath() {
return gpkiLicPath;
}
public void setGpkiLicPath(String gpkiLicPath) {
this.gpkiLicPath = gpkiLicPath;
}
public boolean getTestGPKI() {
return testGPKI;
}
public void setTestGPKI(boolean testGPKI) {
this.testGPKI = testGPKI;
}
}

@ -1,18 +0,0 @@
package go.kr.project.api.internal.util;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
public final class TxIdUtil {
private static final Random RANDOM = new Random();
private TxIdUtil() {}
public static String generate() {
String time = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.KOREA).format(new Date());
int random = 100000 + RANDOM.nextInt(900000);
return time + "_" + random;
}
}

@ -1,6 +1,6 @@
package go.kr.project.api.internal.mapper;
package go.kr.project.api.mapper;
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
import go.kr.project.api.model.CarBassMatterInqireVO;
import org.apache.ibatis.annotations.Mapper;
/**
@ -32,7 +32,7 @@ public interface VmisCarBassMatterInqireMapper {
* @param carBassMatterInqireVO
* @return
*/
int insertCarBassMatterInqire(VmisCarBassMatterInqireVO carBassMatterInqireVO);
int insertCarBassMatterInqire(CarBassMatterInqireVO carBassMatterInqireVO);
/**
* API .
@ -42,7 +42,7 @@ public interface VmisCarBassMatterInqireMapper {
* @param carBassMatterInqireVO (carBassMatterInqire )
* @return
*/
int updateCarBassMatterInqire(VmisCarBassMatterInqireVO carBassMatterInqireVO);
int updateCarBassMatterInqire(CarBassMatterInqireVO carBassMatterInqireVO);
/**
* ID .
@ -50,5 +50,14 @@ public interface VmisCarBassMatterInqireMapper {
* @param carBassMatterInqire ID
* @return
*/
VmisCarBassMatterInqireVO selectCarBassMatterInqireById(String carBassMatterInqire);
CarBassMatterInqireVO selectCarBassMatterInqireById(String carBassMatterInqire);
/**
* txId CAR_FFNLG_TRGT_ID . (/ )
*
* @param txId ID
* @param carFfnlgTrgtId ID ( )
* @return
*/
int updateCarFfnlgTrgtIdByTxId(String txId, String carFfnlgTrgtId);
}

@ -0,0 +1,35 @@
package go.kr.project.api.mapper;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import org.apache.ibatis.annotations.Mapper;
/**
* () Mapper
*/
@Mapper
public interface VmisCarLedgerFrmbkMapper {
// ID 시퀀스
String selectNextCarLedgerFrmbkId();
String selectNextCarLedgerFrmbkDtlId();
// 마스터 INSERT/UPDATE/SELECT
int insertCarLedgerFrmbk(CarLedgerFrmbkVO vo);
int updateCarLedgerFrmbk(CarLedgerFrmbkVO vo);
CarLedgerFrmbkVO selectCarLedgerFrmbkById(String carLedgerFrmbkId);
// 상세 INSERT (단건)
int insertCarLedgerFrmbkDtl(CarLedgerFrmbkDtlVO vo);
// 편의: 상세 일괄 (MyBatis foreach를 XML에서 사용할 수도 있으나, 여기서는 단건 호출을 반복)
/**
* txId CAR_FFNLG_TRGT_ID . (/ )
*
* @param txId ID
* @param carFfnlgTrgtId ID ( )
* @return
*/
int updateCarFfnlgTrgtIdByTxId(String txId, String carFfnlgTrgtId);
}

@ -0,0 +1,30 @@
package go.kr.project.api.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
/**
* VMIS-interface
* { "txId": "yyyyMMddHHmmssSSS12345678", "request": {"data": [...]}, "response": {"data": [...]} }
*
* : / .
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
public class ApiExchangeEnvelope<TReq, TRes> {
/** 트랜잭션 ID */
@JsonProperty("txId")
private String txId;
/** 요청 데이터 래퍼 */
@JsonProperty("request")
private Envelope<TReq> request;
/** 응답 데이터 래퍼 */
@JsonProperty("response")
private Envelope<TRes> response;
}

@ -0,0 +1,861 @@
package go.kr.project.api.model;
import go.kr.project.api.model.request.NewBasicRequest;
import go.kr.project.api.model.request.OldBasicRequest;
import go.kr.project.api.model.response.NewBasicResponse;
import go.kr.project.api.model.response.OldBasicResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* , DB
*
* <p>API .
* INSERT, UPDATE .</p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarBassMatterInqireVO {
/**
* ID (PK)
* : CBMI000000000001
*/
private String carBassMatterInqireId;
// 신규 응답용 txId
private String txId;
// ===== 요청 정보 =====
/**
* ID
*/
private String infoSysId;
/**
* IP
*/
private String infoSysIpAddr;
/**
*
*/
private String sggCd;
/**
*
*/
private String linkInfoCd;
/**
* ID
*/
private String picId;
/**
* IP
*/
private String picIpAddr;
/**
*
*/
private String picNm;
/**
*
*/
private String dmndLevyCrtrYmd;
/**
*
*/
private String dmndInqSeCd;
/**
*
*/
private String dmndVhrno;
/**
*
*/
private String dmndVin;
// ===== 응답 정보 (결과 수신 시 UPDATE) =====
/**
* - LINK_RSLT_CD
*/
private String linkRsltCd;
/**
* - LINK_RSLT_DTL
*/
private String linkRsltDtl;
/**
* - YRIDNW
*/
private String yridnw;
/**
* - REG_YMD
*/
private String regYmd;
/**
* - ERSR_REG_SE_CD
*/
private String ersrRegSeCd;
/**
* - ERSR_REG_SE_NM
*/
private String ersrRegSeNm;
/**
* - ERSR_REG_YMD
*/
private String ersrRegYmd;
/**
* - REG_DTL_CD
*/
private String regDtlCd;
/**
* - DSPLVL
*/
private String dsplvl;
/**
* - USGSRHLD_STDG_CD
*/
private String usgsrhldStdgCd;
/**
* - USGSRHLD_DONG_CD
*/
private String usgsrhldDongCd;
/**
* - USGSRHLD_MTN_YN
*/
private String usgsrhldMtnYn;
/**
* - USGSRHLD_LNBR
*/
private String usgsrhldLnbr;
/**
* - USGSRHLD_HO
*/
private String usgsrhldHo;
/**
* - USGSRHLD_ADDR_NM
*/
private String usgsrhldAddrNm;
/**
* - USGSRHLD_ROAD_NM_CD
*/
private String usgsrhldRoadNmCd;
/**
* - USGSRHLD_UDGD_BLDG_SE_CD
*/
private String usgsrhldUdgdBldgSeCd;
/**
* - USGSRHLD_BMNO
*/
private String usgsrhldBmno;
/**
* - USGSRHLD_BSNO
*/
private String usgsrhldBsno;
/**
* - USGSRHLD_WHOL_ADDR
*/
private String usgsrhldWholAddr;
/**
* - RPRS_OWNR_MBR_SE_CD
*/
private String rprsOwnrMbrSeCd;
/**
* - RPRSV_OWNR_IDECNO
*/
private String rprsvOwnrIdecno;
/**
* - RPRS_OWNR_TELNO
*/
private String rprsOwnrTelno;
/**
* - OWNR_STDG_CD
*/
private String ownrStdgCd;
/**
* - OWNR_DONG_CD
*/
private String ownrDongCd;
/**
* - OWNR_MTN_YN
*/
private String ownrMtnYn;
/**
* - OWNR_LNBR
*/
private String ownrLnbr;
/**
* - OWNR_HO
*/
private String ownrHo;
/**
* - OWNR_ADDR_NM
*/
private String ownrAddrNm;
/**
* - OWNR_ROAD_NM_CD
*/
private String ownrRoadNmCd;
/**
* - OWNR_UDGD_BLDG_SE_CD
*/
private String ownrUdgdBldgSeCd;
/**
* - OWNR_BMNO
*/
private String ownrBmno;
/**
* - OWNR_BSNO
*/
private String ownrBsno;
/**
* - OWNR_WHOL_ADDR
*/
private String ownrWholaddr;
/**
* - REAR_VHRNO
*/
private String rearVhrno;
/**
* - USE_FUEL_CD
*/
private String useFuelCd;
/**
* - USG_SE_CD
*/
private String usgSeCd;
/**
* - MTRS_FOM_NM
*/
private String mtrsFomNm;
/**
* - BFR_VHRNO
*/
private String bfrVhrno;
/**
* - VHRNO
*/
private String vhrno;
/**
* - VIN
*/
private String vin;
/**
* - ATMB_NM
*/
private String atmbNm;
/**
* - VHCL_TOTL_WT
*/
private String vhclTotlWt;
/**
* - VEAG_END_YMD
*/
private String veagEndYmd;
/**
* - CHG_YMD
*/
private String chgYmd;
/**
* - CARMDL_ASORT_CD
*/
private String carmdlAsortCd;
/**
* - CARMDL_TYPE_CD
*/
private String carmdlTypeCd;
/**
* - CARMDL_SE_CD
*/
private String carmdlSeCd;
/**
* - MXMM_LDG
*/
private String mxmmLdg;
/**
* - CARMDL_ASORT_NM
*/
private String carmdlAsortNm;
/**
* - CARMDL_TYPE_NM
*/
private String carmdlTypeNm;
/**
* - CARMDL_CLSF_NM
*/
private String carmdlClsfNm;
/**
* - FRST_REG_YMD
*/
private String frstRegYmd;
/**
* - FOM_NM
*/
private String fomNm;
/**
* - ACQS_YMD
*/
private String acqsYmd;
/**
* - ACQS_END_YMD
*/
private String acqsEndYmd;
/**
* - FBCTN_YMD
*/
private String fbctnYmd;
/**
* - TRANSR_REG_YMD
*/
private String transrRegYmd;
/**
* - SPCF_REG_STTS_CD
*/
private String spcfRegSttsCd;
/**
* - COLOR_NM
*/
private String colorNm;
/**
* - MRTG_CNT
*/
private String mrtgCnt;
/**
* - SZR_CNT
*/
private String szrCnt;
/**
* - STRCT_CHG_CNT
*/
private String strctChgCnt;
/**
* - NOPLT_CSDY_YN
*/
private String nopltCsdyYn;
/**
* - NOPLT_CSDY_AVTSMT_YMD
*/
private String nopltCsdyAvtsmtYmd;
/**
* - SRC_SE_CD
*/
private String srcSeCd;
/**
* - NOPLT_SPCFCT_CD
*/
private String nopltSpcfctCd;
/**
* - ACQS_AMT
*/
private String acqsAmt;
/**
* - INSP_VLD_PD_BGNG_YMD
*/
private String inspVldPdBgngYmd;
/**
* - INSP_VLD_PD_END_YMD
*/
private String inspVldPdEndYmd;
/**
* - USGSRHLD_GRC_CD
*/
private String usgsrhldGrcCd;
/**
* - RDCPCT_CNT
*/
private String rdcpctCnt;
/**
* - SPMNNO
*/
private String spmnno;
/**
* - DRVNG_DSTNC
*/
private String drvngDstnc;
/**
* - FRST_REG_APLY_RCPT_NO
*/
private String frstRegAplyRcptNo;
/**
* - VLNT_ERSR_PRVNTC_AVTSMT_YMD
*/
private String vlntErsrPrvntcAvtsmtYmd;
/**
* - OGNZ_NM
*/
private String ognzNm;
/**
* - PRCS_IMPRTY_RSN_CD
*/
private String prcsImprtyRsnCd;
/**
* - PRCS_IMPRTY_RSN_DTLS
*/
private String prcsImprtyRsnDtls;
/**
* - CBD_LT
*/
private String cbdLt;
/**
* - CBD_BT
*/
private String cbdBt;
/**
* - CBD_HG
*/
private String cbdHg;
/**
* - FRST_MXMM_LDG
*/
private String frstMxmmLdg;
/**
* - FUEL_CNSMPRT
*/
private String fuelCnsmprt;
/**
* - ELCTY_CMPND_FUEL_CNSMPRT
*/
private String elctyCmpndFuelCnsmprt;
/**
* ID - CAR_FFNLG_TRGT_ID
*/
private String carFfnlgTrgtId;
/**
* - REG_DT
*/
private LocalDateTime regDt;
/**
* - RGTR
*/
private String rgtr;
/**
* - RPRS_OWNR_NM
*/
private String rprsOwnrNm;
/**
* OldBasicRequest VO ( )
*
* @param request OldBasicRequest
* @return VO
*/
public static CarBassMatterInqireVO fromOldRequest(OldBasicRequest request) {
if (request == null) {
return null;
}
// record 배열에서 첫 번째 요청 데이터 추출
OldBasicRequest.Record record = null;
if (request.getRecord() != null && !request.getRecord().isEmpty()) {
record = request.getRecord().get(0);
}
CarBassMatterInqireVO.CarBassMatterInqireVOBuilder builder = CarBassMatterInqireVO.builder()
// 헤더 정보 (request 외부 클래스에서)
.infoSysId(request.getInfoSysId()) // INFO_SYS_ID → infoSysId
.infoSysIpAddr(request.getInfoSysIp()) // INFO_SYS_IP → infoSysIpAddr
.sggCd(request.getSigunguCode()) // SIGUNGU_CODE → sggCd
.linkInfoCd(request.getCntcInfoCode()) // CNTC_INFO_CODE → linkInfoCd
.picId(request.getChargerId()) // CHARGER_ID → picId
.picIpAddr(request.getChargerIp()) // CHARGER_IP → picIpAddr
.picNm(request.getChargerNm()); // CHARGER_NM → picNm
// 요청 본문 정보 (request.record[0]에서)
if (record != null) {
builder
.dmndLevyCrtrYmd(record.getLevyStdde()) // LEVY_STDDE → dmndLevyCrtrYmd
.dmndInqSeCd(record.getInqireSeCode()) // INQIRE_SE_CODE → dmndInqSeCd
.dmndVhrno(record.getVhrno()) // VHRNO → dmndVhrno
.dmndVin(record.getVin()); // VIN → dmndVin
}
return builder.build();
}
/**
* ApiExchangeEnvelope<OldBasicRequest, OldBasicResponse> VO ( )
*
* @param generatedId PK ID
* @param envelope API Envelope
* @return VO
*/
public static CarBassMatterInqireVO fromOldExchange(
String generatedId,
ApiExchangeEnvelope<OldBasicRequest, OldBasicResponse> envelope) {
if (envelope == null || envelope.getResponse() == null) {
return null;
}
Envelope<OldBasicResponse> response = envelope.getResponse();
List<OldBasicResponse> data = response.getData();
if (data == null || data.isEmpty()) {
return null;
}
OldBasicResponse firstData = data.get(0);
List<OldBasicResponse.Record> records = firstData.getRecord();
// 응답 정보 업데이트용 VO 빌드
CarBassMatterInqireVO.CarBassMatterInqireVOBuilder builder = CarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId)
.txId(envelope.getTxId())
.linkRsltCd(firstData.getCntcResultCode())
.linkRsltDtl(firstData.getCntcResultDtls());
// record가 있는 경우 첫 번째 record의 상세 정보 매핑
if (records != null && !records.isEmpty()) {
OldBasicResponse.Record record = records.get(0);
builder
.yridnw(record.getPrye())
.regYmd(record.getRegistDe())
.ersrRegSeCd(record.getErsrRegistSeCode())
.ersrRegSeNm(record.getErsrRegistSeNm())
.ersrRegYmd(record.getErsrRegistDe())
.regDtlCd(record.getRegistDetailCode())
.dsplvl(record.getDsplvl())
.usgsrhldStdgCd(record.getUseStrnghldLegaldongCode())
.usgsrhldDongCd(record.getUseStrnghldAdstrdCode())
.usgsrhldMtnYn(record.getUseStrnghldMntn())
.usgsrhldLnbr(record.getUseStrnghldLnbr())
.usgsrhldHo(record.getUseStrnghldHo())
.usgsrhldAddrNm(record.getUseStrnghldAdresNm())
.usgsrhldRoadNmCd(record.getUseStrnghldRoadNmCode())
.usgsrhldUdgdBldgSeCd(record.getUsgsrhldUndgrndBuldSeCode())
.usgsrhldBmno(record.getUseStrnghldBuldMainNo())
.usgsrhldBsno(record.getUseStrnghldBuldSubNo())
.usgsrhldWholAddr(record.getUsgsrhldAdresFull())
.rprsOwnrMbrSeCd(record.getMberSeCode())
.rprsOwnrNm(record.getMberNm())
.rprsvOwnrIdecno(record.getMberSeNo())
.rprsOwnrTelno(record.getTelno())
.ownrStdgCd(record.getOwnerLegaldongCode())
.ownrDongCd(record.getOwnerAdstrdCode())
.ownrMtnYn(record.getOwnerMntn())
.ownrLnbr(record.getOwnerLnbr())
.ownrHo(record.getOwnerHo())
.ownrAddrNm(record.getOwnerAdresNm())
.ownrRoadNmCd(record.getOwnerRoadNmCode())
.ownrUdgdBldgSeCd(record.getOwnerUndgrndBuldSeCode())
.ownrBmno(record.getOwnerBuldMainNo())
.ownrBsno(record.getOwnerBuldSubNo())
.ownrWholaddr(record.getOwnerAdresFull())
.rearVhrno(record.getAftrVhrno())
.useFuelCd(record.getUseFuelCode())
.usgSeCd(record.getPrposSeCode())
.mtrsFomNm(record.getMtrsFomNm())
.bfrVhrno(record.getFrntVhrno())
.vhrno(record.getVhrno())
.vin(record.getVin())
.atmbNm(record.getCnm())
.vhclTotlWt(record.getVhcleTotWt())
.veagEndYmd(record.getCaagEndde())
.chgYmd(record.getChangeDe())
.carmdlAsortCd(record.getVhctyAsortCode())
.carmdlTypeCd(record.getVhctyTyCode())
.carmdlSeCd(record.getVhctySeCode())
.mxmmLdg(record.getMxmmLdg())
.carmdlAsortNm(record.getVhctyAsortNm())
.carmdlTypeNm(record.getVhctyTyNm())
.carmdlClsfNm(record.getVhctySeNm())
.frstRegYmd(record.getFrstRegistDe())
.fomNm(record.getFomNm())
.acqsYmd(record.getAcqsDe())
.acqsEndYmd(record.getAcqsEndDe())
.fbctnYmd(record.getYblMd())
.transrRegYmd(record.getTransrRegistDe())
.spcfRegSttsCd(record.getSpcfRegistSttusCode())
.colorNm(record.getColorNm())
.mrtgCnt(record.getMrtgCo())
.szrCnt(record.getSeizrCo())
.strctChgCnt(record.getStmdCo())
.nopltCsdyYn(record.getNmplCsdyAt())
.nopltCsdyAvtsmtYmd(record.getNmplCsdyRemnrDe())
.srcSeCd(record.getOriginSeCode())
.nopltSpcfctCd(record.getNmplStndrdCode())
.acqsAmt(record.getAcqsAmount())
.inspVldPdBgngYmd(record.getInsptValidPdBgnde())
.inspVldPdEndYmd(record.getInsptValidPdEndde())
.usgsrhldGrcCd(record.getUseStrnghldGrcCode())
.rdcpctCnt(record.getTkcarPscapCo())
.spmnno(record.getSpmnno())
.drvngDstnc(record.getTrvlDstnc())
.frstRegAplyRcptNo(record.getFrstRegistRqrcno())
.vlntErsrPrvntcAvtsmtYmd(record.getVlntErsrPrvntcNticeDe())
.ognzNm(record.getRegistInsttNm())
.prcsImprtyRsnCd(record.getProcessImprtyResnCode())
.prcsImprtyRsnDtls(record.getProcessImprtyResnDtls())
.cbdLt(record.getCbdLt())
.cbdBt(record.getCbdBt())
.cbdHg(record.getCbdHg())
.frstMxmmLdg(record.getFrstMxmmLdg())
.fuelCnsmprt(record.getFuelCnsmpRt())
.elctyCmpndFuelCnsmprt(record.getElctyCmpndFuelCnsmpRt());
}
return builder.build();
}
/**
* NewBasicRequest VO ( )
*
* @param request API
* @return VO
*/
public static CarBassMatterInqireVO fromNewRequest(NewBasicRequest request) {
if (request == null) {
return null;
}
// 공통 메타 정보 매핑
CarBassMatterInqireVO.CarBassMatterInqireVOBuilder builder = CarBassMatterInqireVO.builder()
.infoSysId(request.getInfoSysId())
.infoSysIpAddr(request.getInfoSysIpAddr())
.sggCd(request.getSggCd())
.linkInfoCd(request.getLinkInfoCd())
.picId(request.getPicId())
.picIpAddr(request.getPicIpAddr())
.picNm(request.getPicNm());
// record가 있는 경우 첫 번째 record의 요청 정보 매핑
if (request.getRecord() != null && !request.getRecord().isEmpty()) {
NewBasicRequest.Record record = request.getRecord().get(0);
builder
.dmndLevyCrtrYmd(record.getLevyCrtrYmd()) // 과태료 기준일자
.dmndInqSeCd(record.getInqSeCd()) // 조회 구분 코드
.dmndVhrno(record.getVhrno()) // 차량번호
.dmndVin(record.getVin()); // 차대번호
}
return builder.build();
}
/**
* ApiExchangeEnvelope<NewBasicRequest, NewBasicResponse> VO ( )
*
* @param generatedId PK ID
* @param envelope API Envelope
* @return VO
*/
public static CarBassMatterInqireVO fromNewExchange(
String generatedId,
ApiExchangeEnvelope<NewBasicRequest, NewBasicResponse> envelope) {
if (envelope == null || envelope.getResponse() == null) {
return null;
}
Envelope<NewBasicResponse> response = envelope.getResponse();
List<NewBasicResponse> data = response.getData();
if (data == null || data.isEmpty()) {
return null;
}
NewBasicResponse firstData = data.get(0);
List<NewBasicResponse.Record> records = firstData.getRecord();
// 응답 정보 업데이트용 VO 빌드
CarBassMatterInqireVO.CarBassMatterInqireVOBuilder builder = CarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId)
.txId(envelope.getTxId())
.linkRsltCd(firstData.getLinkRsltCd())
.linkRsltDtl(firstData.getLinkRsltDtl());
// record가 있는 경우 첫 번째 record의 상세 정보 매핑
if (records != null && !records.isEmpty()) {
NewBasicResponse.Record record = records.get(0);
builder
.yridnw(record.getYridnw())
.regYmd(record.getRegYmd())
.ersrRegSeCd(record.getErsrRegSeCd())
.ersrRegSeNm(record.getErsrRegSeNm())
.ersrRegYmd(record.getErsrRegYmd())
.regDtlCd(record.getRegDtlCd())
.dsplvl(record.getDsplvl())
.usgsrhldStdgCd(record.getUsgsrhldStdgCd())
.usgsrhldDongCd(record.getUsgsrhldDongCd())
.usgsrhldMtnYn(record.getUsgsrhldMtnYn())
.usgsrhldLnbr(record.getUsgsrhldLnbr())
.usgsrhldHo(record.getUsgsrhldHo())
.usgsrhldAddrNm(record.getUsgsrhldAddrNm())
.usgsrhldRoadNmCd(record.getUsgsrhldRoadNmCd())
.usgsrhldUdgdBldgSeCd(record.getUsgsrhldUdgdBldgSeCd())
.usgsrhldBmno(record.getUsgsrhldBmno())
.usgsrhldBsno(record.getUsgsrhldBsno())
.usgsrhldWholAddr(record.getUsgsrhldWholAddr())
.rprsOwnrMbrSeCd(record.getRprsOwnrMbrSeCd())
.rprsOwnrNm(record.getRprsOwnrNm())
.rprsvOwnrIdecno(record.getRprsvOwnrIdecno())
.rprsOwnrTelno(record.getRprsOwnrTelno())
.ownrStdgCd(record.getOwnrStdgCd())
.ownrDongCd(record.getOwnrDongCd())
.ownrMtnYn(record.getOwnrMtnYn())
.ownrLnbr(record.getOwnrLnbr())
.ownrHo(record.getOwnrHo())
.ownrAddrNm(record.getOwnrAddrNm())
.ownrRoadNmCd(record.getOwnrRoadNmCd())
.ownrUdgdBldgSeCd(record.getOwnrUdgdBldgSeCd())
.ownrBmno(record.getOwnrBmno())
.ownrBsno(record.getOwnrBsno())
.ownrWholaddr(record.getOwnrWholAddr())
.rearVhrno(record.getRearVhrno())
.useFuelCd(record.getUseFuelCd())
.usgSeCd(record.getUsgSeCd())
.mtrsFomNm(record.getMtrsFomNm())
.bfrVhrno(record.getBfrVhrno())
.vhrno(record.getVhrno())
.vin(record.getVin())
.atmbNm(record.getAtmbNm())
.vhclTotlWt(record.getVhclTotlWt())
.veagEndYmd(record.getVeagEndYmd())
.chgYmd(record.getChgYmd())
.carmdlAsortCd(record.getCarmdlAsortCd())
.carmdlTypeCd(record.getCarmdlTypeCd())
.carmdlSeCd(record.getCarmdlSeCd())
.mxmmLdg(record.getMxmmLdg())
.carmdlAsortNm(record.getCarmdlAsortNm())
.carmdlTypeNm(record.getCarmdlTypeNm())
.carmdlClsfNm(record.getCarmdlClsfNm())
.frstRegYmd(record.getFrstRegYmd())
.fomNm(record.getFomNm())
.acqsYmd(record.getAcqsYmd())
.acqsEndYmd(record.getAcqsEndYmd())
.fbctnYmd(record.getFbctnYmd())
.transrRegYmd(record.getTransrRegYmd())
.spcfRegSttsCd(record.getSpcfRegSttsCd())
.colorNm(record.getColorNm())
.mrtgCnt(record.getMrtgCnt())
.szrCnt(record.getSzrCnt())
.strctChgCnt(record.getStrctChgCnt())
.nopltCsdyYn(record.getNoplTcsdyYn())
.nopltCsdyAvtsmtYmd(record.getNoplTcsdyAvtsmtYmd())
.srcSeCd(record.getSrcSeCd())
.nopltSpcfctCd(record.getNoplTSpcfctCd())
.acqsAmt(record.getAcqsAmt())
.inspVldPdBgngYmd(record.getInspVldPdBgngYmd())
.inspVldPdEndYmd(record.getInspVldPdEndYmd())
.usgsrhldGrcCd(record.getUsgsrhldGrcCd())
.rdcpctCnt(record.getRdcpctCnt())
.spmnno(record.getSpmnno())
.drvngDstnc(record.getDrvngDstnc())
.frstRegAplyRcptNo(record.getFrstRegAplyRcptNo())
.vlntErsrPrvntcAvtsmtYmd(record.getVlntErsrPrvntcAvtsmtYmd())
.ognzNm(record.getOgnzNm())
.prcsImprtyRsnCd(record.getPrcsImprtyRsnCd())
.prcsImprtyRsnDtls(record.getPrcsImprtyRsnDtls())
.cbdLt(record.getCbdLt())
.cbdBt(record.getCbdBt())
.cbdHg(record.getCbdHg())
.frstMxmmLdg(record.getFrstMxmmLdg())
.fuelCnsmprt(record.getFuelCnsmprt())
.elctyCmpndFuelCnsmprt(record.getElctyCmpndFuelCnsmprt());
}
return builder.build();
}
}

@ -0,0 +1,137 @@
package go.kr.project.api.model;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.NewLedgerResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* () VO, DB
*
* <p>tb_car_ledger_frmbk_dtl </p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarLedgerFrmbkDtlVO {
// PK
private String carLedgerFrmbkDtlId;
// FK
private String carLedgerFrmbkId; // CAR_LEDGER_FRMBK_ID
// 본문 - DB 컬럼명과 정확히 일치
private String szrRmvDtlSn; // SZR_RMV_DTL_SN
private String chgTaskSeCd; // CHG_TASK_SE_CD
private String mainNo; // MAIN_NO
private String sno; // SNO
private String spcablMttr; // SPCABL_MTTR
private String hshldrNm; // HSHLDR_NM
private String hshldrIdecno; // HSHLDR_IDECNO
private String aplyRcptNo; // APLY_RCPT_NO
private String vhmno; // VHMNO
private String ledgerGroupNo; // LEDGER_GROUP_NO
private String ledgerIndivNo; // LEDGER_INDIV_NO
private String chgTaskSeNm; // CHG_TASK_SE_NM
private String chgYmd; // CHG_YMD
private String dtlSn; // DTL_SN
private String flag; // FLAG
// 감사
private String rgtr; // RGTR
/**
* ApiExchangeEnvelope VO
*
* @param envelope API Envelope
* @param generatedId PK ID
* @return VO
*/
public static List<CarLedgerFrmbkDtlVO> listNewFromExchange(
ApiExchangeEnvelope<NewLedgerRequest, NewLedgerResponse> envelope,
String generatedId) {
if (envelope == null || envelope.getResponse() == null) {
return new ArrayList<>();
}
Envelope<NewLedgerResponse> response = envelope.getResponse();
List<NewLedgerResponse> data = response.getData();
if (data == null || data.isEmpty()) {
return new ArrayList<>();
}
NewLedgerResponse firstData = data.get(0);
List<NewLedgerResponse.Record> records = firstData.getRecord();
if (records == null || records.isEmpty()) {
return new ArrayList<>();
}
// Record 리스트를 VmisCarLedgerFrmbkDtlVO 리스트로 변환
return records.stream()
.map(record -> CarLedgerFrmbkDtlVO.builder()
.carLedgerFrmbkId(generatedId)
.szrRmvDtlSn(record.getSzrRmvDtlSn())
.chgTaskSeCd(record.getChgTaskSeCd())
.mainNo(record.getMainNo())
.sno(record.getSno())
.spcablMttr(record.getSpcablMttr())
.hshldrNm(record.getHshldrNm())
.hshldrIdecno(record.getHshldrIdecno())
.aplyRcptNo(record.getAplyRcptNo())
.vhmno(record.getVhmno())
.ledgerGroupNo(record.getLedgerGroupNo())
.ledgerIndivNo(record.getLedgerIndivNo())
.chgTaskSeNm(record.getChgTaskSeNm())
.chgYmd(record.getChgYmd())
.dtlSn(record.getDtlSn())
.flag(record.getFlag())
.build())
.collect(Collectors.toList());
}
/**
* NewLedgerResponse.Record VO
*
* @param record NewLedgerResponse.Record
* @param carLedgerFrmbkId PK ID
* @return VO
*/
public static CarLedgerFrmbkDtlVO fromRecord(
NewLedgerResponse.Record record,
String carLedgerFrmbkId) {
if (record == null) {
return null;
}
return CarLedgerFrmbkDtlVO.builder()
.carLedgerFrmbkId(carLedgerFrmbkId)
.szrRmvDtlSn(record.getSzrRmvDtlSn())
.chgTaskSeCd(record.getChgTaskSeCd())
.mainNo(record.getMainNo())
.sno(record.getSno())
.spcablMttr(record.getSpcablMttr())
.hshldrNm(record.getHshldrNm())
.hshldrIdecno(record.getHshldrIdecno())
.aplyRcptNo(record.getAplyRcptNo())
.vhmno(record.getVhmno())
.ledgerGroupNo(record.getLedgerGroupNo())
.ledgerIndivNo(record.getLedgerIndivNo())
.chgTaskSeNm(record.getChgTaskSeNm())
.chgYmd(record.getChgYmd())
.dtlSn(record.getDtlSn())
.flag(record.getFlag())
.build();
}
}

@ -0,0 +1,244 @@
package go.kr.project.api.model;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.NewLedgerResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* () VO, DB
*
* <p>tb_car_ledger_frmbk </p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarLedgerFrmbkVO {
// PK
private String carLedgerFrmbkId;
// 신규 응답용 txId
private String txId;
// 요청(헤더/본문) 정보 - DB 컬럼명과 동일하게
private String infoSysId; // INFO_SYS_ID
private String infoSysIpAddr; // INFO_SYS_IP_ADDR
private String sggCd; // SGG_CD
private String linkInfoCd; // LINK_INFO_CD
private String picId; // PIC_ID
private String picIpAddr; // PIC_IP_ADDR
private String picNm; // PIC_NM
// 요청 본문 - DB 컬럼명과 동일하게
private String dmndVhrno; // DMND_VHRNO
private String dmndPrvcRls; // DMND_PRVC_RLS
private String dmndCvlprNm; // DMND_CVLPR_NM
private String dmndCvlprIdecno; // DMND_CVLPR_IDECNO
private String dmndCvlprStdgCd; // DMND_CVLPR_STDG_CD
private String dmndPathSeCd; // DMND_PATH_SE_CD
private String dmndDsctnIndct; // DMND_DSCTN_INDCT
private String dmndInqSeCd; // DMND_INQ_SE_CD
// 응답 요약 정보 - DB 컬럼명과 동일하게
private String linkRsltCd; // LINK_RSLT_CD
private String linkRsltDtl; // LINK_RSLT_DTL
// 응답 본문(마스터) - DB 컬럼명과 정확히 일치
private String ledgerGroupNo; // LEDGER_GROUP_NO
private String ledgerIndivNo; // LEDGER_INDIV_NO
private String vhmno; // VHMNO
private String vhrno; // VHRNO
private String vin; // VIN
private String carmdlAsortCd; // CARMDL_ASORT_CD
private String carmdlAsortNm; // CARMDL_ASORT_NM
private String atmbNm; // ATMB_NM
private String colorCd; // COLOR_CD
private String colorNm; // COLOR_NM
private String nopltSpcfctCd; // NOPLT_SPCFCT_CD
private String nopltSpcfctNm; // NOPLT_SPCFCT_NM
private String usgSeCd; // USG_SE_CD
private String usgSeNm; // USG_SE_NM
private String mtrsFomNm; // MTRS_FOM_NM
private String fomNm; // FOM_NM
private String acqsAmt; // ACQS_AMT
private String regDtlCd; // REG_DTL_CD
private String regDtlNm; // REG_DTL_NM
private String frstRegYmd; // FRST_REG_YMD
private String veagEndYmd; // VEAG_END_YMD
private String yridnw; // YRIDNW
private String spmnno1; // SPMNNO_1
private String spmnno2; // SPMNNO_2
private String fbctnYmd; // FBCTN_YMD
private String drvngDstnc; // DRVNG_DSTNC
private String inspVldPdBgngYmd; // INSP_VLD_PD_BGNG_YMD
private String inspVldPdEndYmd; // INSP_VLD_PD_END_YMD
private String chckVldPdBgngYmd; // CHCK_VLD_PD_BGNG_YMD
private String chckVldPdEndYmd; // CHCK_VLD_PD_END_YMD
private String regAplySeNm; // REG_APLY_SE_NM
private String frstRegAplyRcptNo; // FRST_REG_APLY_RCPT_NO
private String nopltCsdyAvtsmtYmd; // NOPLT_CSDY_AVTSMT_YMD
private String nopltCsdyYn; // NOPLT_CSDY_YN
private String bssUsePdYmd; // BSS_USE_PD_YMD
private String octhtErsrPrvntcAvtsmtYmd;// OCTHT_ERSR_PRVNTC_AVTSMT_YMD
private String ersrRegYmd; // ERSR_REG_YMD
private String ersrRegSeCd; // ERSR_REG_SE_CD
private String ersrRegSeNm; // ERSR_REG_SE_NM
private String mrtgCnt; // MRTG_CNT
private String szrCnt; // SZR_CNT
private String strctChgCnt; // STRCT_CHG_CNT
private String usgsrhldAddr1; // USGSRHLD_ADDR_1
private String usgsrhldAddrDtl1; // USGSRHLD_ADDR_DTL_1
private String ownrAddr; // OWNR_ADDR
private String ownrAddrDtl; // OWNR_ADDR_DTL
private String indvdlBzmnYn; // INDVDL_BZMN_YN
private String rprsOwnrTelno; // RPRS_OWNR_TELNO
private String rprsOwnrNm; // RPRS_OWNR_NM
private String rprsOwnrMbrSeCd; // RPRS_OWNR_MBR_SE_CD
private String rprsvOwnrIdecno; // RPRSV_OWNR_IDECNO
private String taxxmptTrprSeCd; // TAXXMPT_TRPR_SE_CD
private String taxxmptAplcnSeCd; // TAXXMPT_APLCN_SE_CD
private String spcablMttrCnt; // SPCABL_MTTR_CNT
private String usgsrhldDongNm; // USGSRHLD_DONG_NM
private String prvntcCnt; // PRVNTC_CNT
private String xportFlflYnDclrYmd; // XPORT_FLFL_YN_DCLR_YMD
private String issuNo; // ISSU_NO
private String frstTrnsfrYmd; // FRST_TRNSFR_YMD
private String drivSrgbtryIdntfNo; // DRIV_SRGBTRY_IDNTF_NO
private String prcsImprtyRsnCd; // PRCS_IMPRTY_RSN_CD
private String prcsImprtyRsnDtls; // PRCS_IMPRTY_RSN_DTLS
private String carFfnlgTrgtId; // CAR_FFNLG_TRGT_ID
// 감사
private String rgtr;
private List<CarLedgerFrmbkDtlVO> record;
/**
* NewLedgerRequest VO ( )
*
* @param request NewLedgerRequest
* @return VO
*/
public static CarLedgerFrmbkVO fromNewRequest(NewLedgerRequest request) {
if (request == null) {
return null;
}
return CarLedgerFrmbkVO.builder()
.infoSysId(request.getInfoSysId())
.infoSysIpAddr(request.getInfoSysIpAddr())
.sggCd(request.getSggCd())
.linkInfoCd(request.getLinkInfoCd())
.picId(request.getPicId())
.picIpAddr(request.getPicIpAddr())
.picNm(request.getPicNm())
.dmndVhrno(request.getVhrno())
.dmndPrvcRls(request.getPrvcRls())
.dmndCvlprNm(request.getCvlprNm())
.dmndCvlprIdecno(request.getCvlprIdecno())
.dmndCvlprStdgCd(request.getCvlprStdgCd())
.dmndPathSeCd(request.getPathSeCd())
.dmndDsctnIndct(request.getDsctnIndct())
.dmndInqSeCd(request.getInqSeCd())
.build();
}
/**
* NewLedgerResponse VO ( )
*
* @param generatedId PK ID
* @param response NewLedgerResponse
* @return VO
*/
public static CarLedgerFrmbkVO fromNewResponseMaster(
String generatedId,
NewLedgerResponse response) {
if (response == null) {
return null;
}
return CarLedgerFrmbkVO.builder()
.carLedgerFrmbkId(generatedId)
.linkRsltCd(response.getLinkRsltCd())
.linkRsltDtl(response.getLinkRsltDtl())
.ledgerGroupNo(response.getLedgerGroupNo())
.ledgerIndivNo(response.getLedgerIndivNo())
.vhmno(response.getVhmno())
.vhrno(response.getVhrno())
.vin(response.getVin())
.carmdlAsortCd(response.getCarmdlAsortCd())
.carmdlAsortNm(response.getCarmdlAsortNm())
.atmbNm(response.getAtmbNm())
.colorCd(response.getColorCd())
.colorNm(response.getColorNm())
.nopltSpcfctCd(response.getNopltSpcfctCd())
.nopltSpcfctNm(response.getNopltSpcfctNm())
.usgSeCd(response.getUsgSeCd())
.usgSeNm(response.getUsgSeNm())
.mtrsFomNm(response.getMtrsFomNm())
.fomNm(response.getFomNm())
.acqsAmt(response.getAcqsAmt())
.regDtlCd(response.getRegDtlCd())
.regDtlNm(response.getRegDtlNm())
.frstRegYmd(response.getFrstRegYmd())
.veagEndYmd(response.getVeagEndYmd())
.yridnw(response.getYridnw())
.spmnno1(response.getSpmnno1())
.spmnno2(response.getSpmnno2())
.fbctnYmd(response.getFbctnYmd())
.drvngDstnc(response.getDrvngDstnc())
.inspVldPdBgngYmd(response.getInspVldPdBgngYmd())
.inspVldPdEndYmd(response.getInspVldPdEndYmd())
.chckVldPdBgngYmd(response.getChckVldPdBgngYmd())
.chckVldPdEndYmd(response.getChckVldPdEndYmd())
.regAplySeNm(response.getRegAplySeNm())
.frstRegAplyRcptNo(response.getFrstRegAplyRcptNo())
.nopltCsdyAvtsmtYmd(response.getNopltCsdyAvtsmtYmd())
.nopltCsdyYn(response.getNopltCsdyYn())
.bssUsePdYmd(response.getBssUsePdYmd())
.octhtErsrPrvntcAvtsmtYmd(response.getOcthtErsrPrvntcAvtsmtYmd())
.ersrRegYmd(response.getErsrRegYmd())
.ersrRegSeCd(response.getErsrRegSeCd())
.ersrRegSeNm(response.getErsrRegSeNm())
.mrtgCnt(response.getMrtgCnt())
.szrCnt(response.getSzrCnt())
.strctChgCnt(response.getStrctChgCnt())
.usgsrhldAddr1(response.getUsgsrhldAddr1())
.usgsrhldAddrDtl1(response.getUsgsrhldAddrDtl1())
.ownrAddr(response.getOwnrAddr())
.ownrAddrDtl(response.getOwnrAddrDtl())
.indvdlBzmnYn(response.getIndvdlBzmnYn())
.rprsOwnrTelno(response.getRprsOwnrTelno())
.rprsOwnrNm(response.getRprsOwnrNm())
.rprsOwnrMbrSeCd(response.getRprsOwnrMbrSeCd())
.rprsvOwnrIdecno(response.getRprsvOwnrIdecno())
.taxxmptTrprSeCd(response.getTaxxmptTrprSeCd())
.taxxmptAplcnSeCd(response.getTaxxmptAplcnSeCd())
.spcablMttrCnt(response.getSpcablMttrCnt())
.usgsrhldDongNm(response.getUsgsrhldDongNm())
.prvntcCnt(response.getPrvntcCnt())
.xportFlflYnDclrYmd(response.getXportFlflYnDclrYmd())
.issuNo(response.getIssuNo())
.frstTrnsfrYmd(response.getFrstTrnsfrYmd())
.drivSrgbtryIdntfNo(response.getDrivSrgbtryIdntfNo())
.prcsImprtyRsnCd(response.getProcessImprtyResnCode())
.prcsImprtyRsnDtls(response.getProcessImprtyResnDtls())
.build();
}
/**
* NewLedgerRequest (DB )
*
* @param request NewLedgerRequest
* @return
*/
public static String extractVhrno(NewLedgerRequest request) {
return request != null ? request.getVhrno() : null;
}
}

@ -1,7 +1,7 @@
package go.kr.project.api.model;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.model.response.OldBasicResponse;
import go.kr.project.api.model.response.NewLedgerResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@ -21,8 +21,8 @@ public class VehicleApiResponseVO {
private boolean success; // 성공 여부
private String message; // 메시지
private BasicResponse basicInfo; // 차량 기본정보
private LedgerResponse ledgerInfo; // 등록원부 정보
private OldBasicResponse basicInfo; // 차량 기본정보
private NewLedgerResponse ledgerInfo; // 등록원부 정보
private String carBassMatterInqireId; // 자동차 기본 사항 조회 ID
private String carLedgerFrmbkId; // 자동차 등록 원부 갑 ID

@ -1,89 +0,0 @@
package go.kr.project.api.model.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(description = "자동차기본사항조회 요청 항목")
public class BasicRequest {
// 본문 공통 메타 (VmisRequestEnricher에서 자동 설정)
@Schema(description = "정보시스템ID (자동설정: vmis.system.infoSysId)", example = "41-345")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP (자동설정: vmis.system.infoSysIp)", example = "105.19.10.135")
@JsonProperty("INFO_SYS_IP")
private String infoSysIp;
@Schema(description = "시군구코드 (자동설정: vmis.system.sigunguCode)", example = "41460")
@JsonProperty("SIGUNGU_CODE")
private String sigunguCode;
// 서비스별 필드 (VmisRequestEnricher에서 자동 설정)
@Schema(description = "연계정보코드 (자동설정: vmis.gov.services.basic.cntcInfoCode)", example = "AC1_FD11_01")
@JsonProperty("CNTC_INFO_CODE")
private String cntcInfoCode;
@Schema(description = "담당자ID (자동설정: vmis.system.chargerId)", example = "")
@JsonProperty("CHARGER_ID")
private String chargerId;
@Schema(description = "담당자IP (자동설정: vmis.system.chargerIp)", example = "")
@JsonProperty("CHARGER_IP")
private String chargerIp;
@Schema(description = "담당자명(사용자) (자동설정: vmis.system.chargerNm)", example = "")
@JsonProperty("CHARGER_NM")
private String chargerNm;
@Schema(description = "부과기준일", example = "20250101")
@JsonProperty("LEVY_STDDE")
private String levyStdde;
@Schema(description = "조회구분코드 {2:차대번호(VIN), 3:자동차번호(VHRNO)} (VmisRequestEnricher에서 자동설정)")
@JsonProperty("INQIRE_SE_CODE")
private String inqireSeCode;
@Schema(description = "자동차등록번호", example = "12가3456")
@JsonProperty("VHRNO")
private String vhrno;
@Schema(description = "차대번호", example = "KMHAB812345678901")
@JsonProperty("VIN")
private String vin;
/*
// 추가 항목 (명세 샘플 기준)
@Schema(description = "개인정보공개", example = "Y")
@JsonProperty("ONES_INFORMATION_OPEN")
private String onesInformationOpen;
@Schema(description = "민원인성명")
@JsonProperty("CPTTR_NM")
private String cpttrNm;
@Schema(description = "민원인주민번호")
@JsonProperty("CPTTR_IHIDNUM")
@Size(max = 13)
private String cpttrIhidnum;
@Schema(description = "민원인법정동코드")
@JsonProperty("CPTTR_LEGALDONG_CODE")
private String cpttrLegaldongCode;
@Schema(description = "경로구분코드")
@JsonProperty("ROUTE_SE_CODE")
private String routeSeCode;
@Schema(description = "내역표시")
@JsonProperty("DETAIL_EXPRESSION")
private String detailExpression;
*/
}

@ -1,80 +0,0 @@
package go.kr.project.api.model.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Size;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "자동차등록원부(갑) 요청 항목")
@Getter
@Setter
public class LedgerRequest {
// 본문 공통 메타 (VmisRequestEnricher에서 자동 설정)
@Schema(description = "정보시스템ID (자동설정: vmis.system.infoSysId)", example = "41-345")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP (자동설정: vmis.system.infoSysIp)", example = "105.19.10.135")
@JsonProperty("INFO_SYS_IP")
private String infoSysIp;
@Schema(description = "시군구코드 (자동설정: vmis.system.sigunguCode)", example = "41460")
@JsonProperty("SIGUNGU_CODE")
private String sigunguCode;
// 서비스별 필드 (VmisRequestEnricher에서 자동 설정)
@Schema(description = "연계정보코드 (자동설정: vmis.gov.services.ledger.cntcInfoCode)", example = "AC1_FD11_02")
@JsonProperty("CNTC_INFO_CODE")
private String cntcInfoCode;
@Schema(description = "담당자ID (자동설정: vmis.system.chargerId)", example = "")
@JsonProperty("CHARGER_ID")
private String chargerId;
@Schema(description = "담당자IP (자동설정: vmis.system.chargerIp)", example = "")
@JsonProperty("CHARGER_IP")
private String chargerIp;
@Schema(description = "담당자명(사용자) (자동설정: vmis.system.chargerNm)", example = "")
@JsonProperty("CHARGER_NM")
private String chargerNm;
@Schema(description = "자동차등록번호 (필수)", example = "12가3456")
@JsonProperty("VHRNO")
private String vhrno;
@Schema(description = "개인정보공개 {1:소유자공개, 2:비공개, 3:비공개(주민등록번호), 4:비공개(사용본거지)} (기본값: 1, VmisRequestEnricher에서 자동설정)")
@JsonProperty("ONES_INFORMATION_OPEN")
private String onesInformationOpen;
@Schema(description = "민원인성명 (통합조회 시 BasicResponse의 MBER_NM에서 자동매핑)")
@JsonProperty("CPTTR_NM")
private String cpttrNm;
@Schema(description = "민원인주민번호 (통합조회 시 BasicResponse의 MBER_SE_NO에서 자동매핑)")
@JsonProperty("CPTTR_IHIDNUM")
@Size(max = 13)
private String cpttrIhidnum;
@Schema(description = "민원인법정동코드 (기본값: null)")
@JsonProperty("CPTTR_LEGALDONG_CODE")
private String cpttrLegaldongCode;
@Schema(description = "경로구분코드 (기본값: 3, VmisRequestEnricher에서 자동설정)")
@JsonProperty("ROUTE_SE_CODE")
private String routeSeCode;
@Schema(description = "내역표시 {1:전체내역, 2:최종내역} (기본값: 1, VmisRequestEnricher에서 자동설정)")
@JsonProperty("DETAIL_EXPRESSION")
private String detailExpression;
@Schema(description = "조회구분코드 {1:열람, 2:발급} (기본값: 1, VmisRequestEnricher에서 자동설정)")
@JsonProperty("INQIRE_SE_CODE")
private String inqireSeCode;
}

@ -0,0 +1,71 @@
package go.kr.project.api.model.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(description = "자동차기본사항조회 요청 항목 (신버전)")
public class NewBasicRequest { // 총 8개 필드 (외부 클래스 8개, 중첩 Record 클래스 4개)
// 본문 공통 메타 (application.yml에서 자동 설정)
@Schema(description = "정보시스템ID (자동설정: new.vmis.system.infoSysId)", example = "41-345")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP (자동설정: new.vmis.system.infoSysIpAddr)", example = "105.19.10.135")
@JsonProperty("INFO_SYS_IP_ADDR")
private String infoSysIpAddr;
@Schema(description = "시군구코드 (자동설정: new.vmis.system.sggCd)", example = "41460")
@JsonProperty("SGG_CD")
private String sggCd;
// 서비스별 필드 (application.yml에서 자동 설정)
@Schema(description = "연계정보코드 (자동설정: new.vmis.gov.services.basic.linkInfoCd)", example = "AC1_FD11_01")
@JsonProperty("LINK_INFO_CD")
private String linkInfoCd;
@Schema(description = "담당자ID (자동설정: new.vmis.system.picId)", example = "")
@JsonProperty("PIC_ID")
private String picId;
@Schema(description = "담당자IP (자동설정: new.vmis.system.picIpAddr)", example = "")
@JsonProperty("PIC_IP_ADDR")
private String picIpAddr;
@Schema(description = "담당자명 (자동설정: new.vmis.system.picNm)", example = "")
@JsonProperty("PIC_NM")
private String picNm;
@Schema(description = "조회 대상 record 배열")
@JsonProperty("record")
private java.util.List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(name = "NewBasicRequest.Record", description = "기본사항 요청 record 항목 (신버전)")
public static class Record {
@Schema(description = "부과기준일", example = "20250101")
@JsonProperty("LEVY_CRTR_YMD")
private String levyCrtrYmd;
@Schema(description = "조회구분코드 (자동설정: VHRNO not null → 3:자동차번호, VIN not null → 2:차대번호)")
@JsonProperty("INQ_SE_CD")
private String inqSeCd;
@Schema(description = "자동차등록번호", example = "12가3456")
@JsonProperty("VHRNO")
private String vhrno;
@Schema(description = "차대번호", example = "KMHAB812345678901")
@JsonProperty("VIN")
private String vin;
}
}

@ -0,0 +1,80 @@
package go.kr.project.api.model.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Size;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "자동차등록원부(갑) 요청 항목 (신버전)")
@Getter
@Setter
public class NewLedgerRequest { // 총 15개 필드
// 본문 공통 메타 (application.yml에서 자동 설정)
@Schema(description = "정보시스템ID (자동설정: new.vmis.system.infoSysId)", example = "41-345")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP주소 (자동설정: new.vmis.system.infoSysIpAddr)", example = "105.19.10.135")
@JsonProperty("INFO_SYS_IP_ADDR")
private String infoSysIpAddr;
@Schema(description = "시군구코드 (자동설정: new.vmis.system.sggCd)", example = "41460")
@JsonProperty("SGG_CD")
private String sggCd;
// 서비스별 필드 (application.yml에서 자동 설정)
@Schema(description = "연계정보코드 (자동설정: new.vmis.gov.services.ledger.linkInfoCd)", example = "AC1_FD11_02")
@JsonProperty("LINK_INFO_CD")
private String linkInfoCd;
@Schema(description = "담당자ID (자동설정: new.vmis.system.picId)", example = "")
@JsonProperty("PIC_ID")
private String picId;
@Schema(description = "담당자IP주소 (자동설정: new.vmis.system.picIpAddr)", example = "")
@JsonProperty("PIC_IP_ADDR")
private String picIpAddr;
@Schema(description = "담당자명 (자동설정: new.vmis.system.picNm)", example = "")
@JsonProperty("PIC_NM")
private String picNm;
@Schema(description = "자동차등록번호")
@JsonProperty("VHRNO")
private String vhrno;
@Schema(description = "개인정보공개 {1:소유자공개, 2:비공개, 3:비공개(주민등록번호), 4:비공개(사용본거지)}")
@JsonProperty("PRVC_RLS")
private String prvcRls;
@Schema(description = "민원인성명")
@JsonProperty("CVLPR_NM")
private String cvlprNm;
@Schema(description = "민원인주민번호")
@JsonProperty("CVLPR_IDECNO")
@Size(max = 13)
private String cvlprIdecno;
@Schema(description = "민원인법정동코드")
@JsonProperty("CVLPR_STDG_CD")
private String cvlprStdgCd;
@Schema(description = "경로구분코드 고정코드:3")
@JsonProperty("PATH_SE_CD")
private String pathSeCd;
@Schema(description = "내역표시 {1:전체내역, 2:최종내역}")
@JsonProperty("DSCTN_INDCT")
private String dsctnIndct;
@Schema(description = "조회구분코드 (자동설정: 1:열람 고정)")
@JsonProperty("INQ_SE_CD")
private String inqSeCd;
}

@ -0,0 +1,71 @@
package go.kr.project.api.model.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(description = "자동차기본사항조회 요청 항목 (구버전)")
public class OldBasicRequest { // 총 8개 필드 (외부 클래스 8개, 중첩 Record 클래스 4개)
// 본문 공통 메타 (application.yml에서 자동 설정)
@Schema(description = "정보시스템ID (자동설정: old.vmis.system.infoSysId)", example = "41-345")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP (자동설정: old.vmis.system.infoSysIp)", example = "105.19.10.135")
@JsonProperty("INFO_SYS_IP")
private String infoSysIp;
@Schema(description = "시군구코드 (자동설정: old.vmis.system.sigunguCode)", example = "41460")
@JsonProperty("SIGUNGU_CODE")
private String sigunguCode;
// 서비스별 필드 (application.yml에서 자동 설정)
@Schema(description = "연계정보코드 (자동설정: old.vmis.gov.services.basic.cntcInfoCode)", example = "AC1_FD11_01")
@JsonProperty("CNTC_INFO_CODE")
private String cntcInfoCode;
@Schema(description = "담당자ID (자동설정: old.vmis.system.chargerId)", example = "")
@JsonProperty("CHARGER_ID")
private String chargerId;
@Schema(description = "담당자IP (자동설정: old.vmis.system.chargerIp)", example = "")
@JsonProperty("CHARGER_IP")
private String chargerIp;
@Schema(description = "담당자명 (자동설정: old.vmis.system.chargerNm)", example = "")
@JsonProperty("CHARGER_NM")
private String chargerNm;
@Schema(description = "조회 대상 record 배열")
@JsonProperty("record")
private java.util.List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(name = "OldBasicRequest.Record", description = "기본사항 요청 record 항목 (구버전)")
public static class Record {
@Schema(description = "부과기준일", example = "20250101")
@JsonProperty("LEVY_STDDE")
private String levyStdde;
@Schema(description = "조회구분코드 (자동설정: VHRNO not null → 3:자동차번호, VIN not null → 2:차대번호)")
@JsonProperty("INQIRE_SE_CODE")
private String inqireSeCode;
@Schema(description = "자동차등록번호", example = "12가3456")
@JsonProperty("VHRNO")
private String vhrno;
@Schema(description = "차대번호", example = "KMHAB812345678901")
@JsonProperty("VIN")
private String vin;
}
}

@ -1,208 +0,0 @@
package go.kr.project.api.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "자동차기본사항조회 응답 모델")
@Getter
@Setter
public class BasicResponse {
/** 연계결과코드 */
@JsonProperty("CNTC_RESULT_CODE")
private String cntcResultCode;
/** 연계결과상세 */
@JsonProperty("CNTC_RESULT_DTLS")
private String cntcResultDtls;
/** 레코드 목록 */
@JsonProperty("record")
private List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(name = "BasicRecord", description = "기본사항 record 항목")
@Getter
@Setter
public static class Record {
/** 생산년도 */
@JsonProperty("PRYE") private String prye;
/** 등록일자 */
@JsonProperty("REGIST_DE") private String registDe;
/** 말소등록구분코드 */
@JsonProperty("ERSR_REGIST_SE_CODE") private String ersrRegistSeCode;
/** 말소등록구분명 */
@JsonProperty("ERSR_REGIST_SE_NM") private String ersrRegistSeNm;
/** 말소등록일자 */
@JsonProperty("ERSR_REGIST_DE") private String ersrRegistDe;
/** 등록상세코드 */
@JsonProperty("REGIST_DETAIL_CODE") private String registDetailCode;
/** 배기량 */
@JsonProperty("DSPLVL") private String dsplvl;
/** 사용본거지법정동코드 */
@JsonProperty("USE_STRNGHLD_LEGALDONG_CODE") private String useStrnghldLegaldongCode;
/** 사용본거지행정동코드 */
@JsonProperty("USE_STRNGHLD_ADSTRD_CODE") private String useStrnghldAdstrdCode;
/** 사용본거지산 */
@JsonProperty("USE_STRNGHLD_MNTN") private String useStrnghldMntn;
/** 사용본거지번지 */
@JsonProperty("USE_STRNGHLD_LNBR") private String useStrnghldLnbr;
/** 사용본거지호 */
@JsonProperty("USE_STRNGHLD_HO") private String useStrnghldHo;
/** 사용본거지주소명 */
@JsonProperty("USE_STRNGHLD_ADRES_NM") private String useStrnghldAdresNm;
/** 사용본거지도로명코드 */
@JsonProperty("USE_STRNGHLD_ROAD_NM_CODE") private String useStrnghldRoadNmCode;
/** 사용본거지지하건물구분코드 */
@JsonProperty("USGSRHLD_UNDGRND_BULD_SE_CODE") private String usgsrhldUndgrndBuldSeCode;
/** 사용본거지건물본번 */
@JsonProperty("USE_STRNGHLD_BULD_MAIN_NO") private String useStrnghldBuldMainNo;
/** 사용본거지건물부번 */
@JsonProperty("USE_STRNGHLD_BULD_SUB_NO") private String useStrnghldBuldSubNo;
/** 사용본거지주소전체 */
@JsonProperty("USGSRHLD_ADRES_FULL") private String usgsrhldAdresFull;
/** 소유자구분코드 */
@JsonProperty("MBER_SE_CODE") private String mberSeCode;
/** 소유자명 */
@JsonProperty("MBER_NM") private String mberNm;
/** 소유자구분번호 */
@JsonProperty("MBER_SE_NO") private String mberSeNo;
/** 전화번호 */
@JsonProperty("TELNO") private String telno;
/** 소유자법정동코드 */
@JsonProperty("OWNER_LEGALDONG_CODE") private String ownerLegaldongCode;
/** 소유자행정동코드 */
@JsonProperty("OWNER_ADSTRD_CODE") private String ownerAdstrdCode;
/** 소유자산 */
@JsonProperty("OWNER_MNTN") private String ownerMntn;
/** 소유자번지 */
@JsonProperty("OWNER_LNBR") private String ownerLnbr;
/** 소유자호 */
@JsonProperty("OWNER_HO") private String ownerHo;
/** 소유자주소명 */
@JsonProperty("OWNER_ADRES_NM") private String ownerAdresNm;
/** 소유자도로명코드 */
@JsonProperty("OWNER_ROAD_NM_CODE") private String ownerRoadNmCode;
/** 소유자지하건물구분코드 */
@JsonProperty("OWNER_UNDGRND_BULD_SE_CODE") private String ownerUndgrndBuldSeCode;
/** 소유자건물본번 */
@JsonProperty("OWNER_BULD_MAIN_NO") private String ownerBuldMainNo;
/** 소유자건물부번 */
@JsonProperty("OWNER_BULD_SUB_NO") private String ownerBuldSubNo;
/** 소유자주소전체 */
@JsonProperty("OWNER_ADRES_FULL") private String ownerAdresFull;
/** 변경후차량번호 */
@JsonProperty("AFTR_VHRNO") private String aftrVhrno;
/** 사용연료코드 */
@JsonProperty("USE_FUEL_CODE") private String useFuelCode;
/** 용도구분코드 */
@JsonProperty("PRPOS_SE_CODE") private String prposSeCode;
/** 제작사명 */
@JsonProperty("MTRS_FOM_NM") private String mtrsFomNm;
/** 변경전차량번호 */
@JsonProperty("FRNT_VHRNO") private String frntVhrno;
/** 차량번호 */
@JsonProperty("VHRNO") private String vhrno;
/** 차대번호 */
@JsonProperty("VIN") private String vin;
/** 차명 */
@JsonProperty("CNM") private String cnm;
/** 차량총중량 */
@JsonProperty("VHCLE_TOT_WT") private String vhcleTotWt;
/** 자동차보험종료일자 */
@JsonProperty("CAAG_ENDDE") private String caagEndde;
/** 변경일자 */
@JsonProperty("CHANGE_DE") private String changeDe;
/** 차종분류코드 */
@JsonProperty("VHCTY_ASORT_CODE") private String vhctyAsortCode;
/** 차종유형코드 */
@JsonProperty("VHCTY_TY_CODE") private String vhctyTyCode;
/** 차종구분코드 */
@JsonProperty("VHCTY_SE_CODE") private String vhctySeCode;
/** 최대적재량 */
@JsonProperty("MXMM_LDG") private String mxmmLdg;
/** 차종분류명 */
@JsonProperty("VHCTY_ASORT_NM") private String vhctyAsortNm;
/** 차종유형명 */
@JsonProperty("VHCTY_TY_NM") private String vhctyTyNm;
/** 차종구분명 */
@JsonProperty("VHCTY_SE_NM") private String vhctySeNm;
/** 최초등록일자 */
@JsonProperty("FRST_REGIST_DE") private String frstRegistDe;
/** 형식명 */
@JsonProperty("FOM_NM") private String fomNm;
/** 취득일자 */
@JsonProperty("ACQS_DE") private String acqsDe;
/** 취득종료일자 */
@JsonProperty("ACQS_END_DE") private String acqsEndDe;
/** 연식월 */
@JsonProperty("YBL_MD") private String yblMd;
/** 이전등록일자 */
@JsonProperty("TRANSR_REGIST_DE") private String transrRegistDe;
/** 특정등록상태코드 */
@JsonProperty("SPCF_REGIST_STTUS_CODE") private String spcfRegistSttusCode;
/** 색상명 */
@JsonProperty("COLOR_NM") private String colorNm;
/** 저당건수 */
@JsonProperty("MRTG_CO") private String mrtgCo;
/** 압류건수 */
@JsonProperty("SEIZR_CO") private String seizrCo;
/** 압인건수 */
@JsonProperty("STMD_CO") private String stmdCo;
/** 번호판보관여부 */
@JsonProperty("NMPL_CSDY_AT") private String nmplCsdyAt;
/** 번호판보관반납일자 */
@JsonProperty("NMPL_CSDY_REMNR_DE") private String nmplCsdyRemnrDe;
/** 원산지구분코드 */
@JsonProperty("ORIGIN_SE_CODE") private String originSeCode;
/** 번호판규격코드 */
@JsonProperty("NMPL_STNDRD_CODE") private String nmplStndrdCode;
/** 취득금액 */
@JsonProperty("ACQS_AMOUNT") private String acqsAmount;
/** 검사유효기간시작일자 */
@JsonProperty("INSPT_VALID_PD_BGNDE") private String insptValidPdBgnde;
/** 검사유효기간종료일자 */
@JsonProperty("INSPT_VALID_PD_ENDDE") private String insptValidPdEndde;
/** 사용본거지우편번호코드 */
@JsonProperty("USE_STRNGHLD_GRC_CODE") private String useStrnghldGrcCode;
/** 화물차승차정원수 */
@JsonProperty("TKCAR_PSCAP_CO") private String tkcarPscapCo;
/** 사양번호 */
@JsonProperty("SPMNNO") private String spmnno;
/** 주행거리 */
@JsonProperty("TRVL_DSTNC") private String trvlDstnc;
/** 최초등록신청번호 */
@JsonProperty("FRST_REGIST_RQRCNO") private String frstRegistRqrcno;
/** 자진말소예방공지일자 */
@JsonProperty("VLNT_ERSR_PRVNTC_NTICE_DE") private String vlntErsrPrvntcNticeDe;
/** 등록기관명 */
@JsonProperty("REGIST_INSTT_NM") private String registInsttNm;
/** 처리불가사유코드 */
@JsonProperty("PROCESS_IMPRTY_RESN_CODE") private String processImprtyResnCode;
/** 처리불가사유상세 */
@JsonProperty("PROCESS_IMPRTY_RESN_DTLS") private String processImprtyResnDtls;
/** 차체길이 */
@JsonProperty("CBD_LT") private String cbdLt;
/** 차체너비 */
@JsonProperty("CBD_BT") private String cbdBt;
/** 차체높이 */
@JsonProperty("CBD_HG") private String cbdHg;
/** 최초최대적재량 */
@JsonProperty("FRST_MXMM_LDG") private String frstMxmmLdg;
/** 연료소비율 */
@JsonProperty("FUEL_CNSMP_RT") private String fuelCnsmpRt;
/** 전기복합연료소비율 */
@JsonProperty("ELCTY_CMPND_FUEL_CNSMP_RT") private String elctyCmpndFuelCnsmpRt;
}
}

@ -1,324 +0,0 @@
package go.kr.project.api.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "자동차등록원부(갑) 응답 모델")
@Getter
@Setter
public class LedgerResponse {
/** 연계결과코드 */
@JsonProperty("CNTC_RESULT_CODE")
private String cntcResultCode;
/** 연계결과상세 */
@JsonProperty("CNTC_RESULT_DTLS")
private String cntcResultDtls;
/** 원부그룹번호 */
@JsonProperty("LEDGER_GROUP_NO")
private String ledgerGroupNo;
/** 원부개별화번호 */
@JsonProperty("LEDGER_INDVDLZ_NO")
private String ledgerIndvdlzNo;
/** 차량관리번호 */
@JsonProperty("VHMNO")
private String vhmno;
/** 차량번호 */
@JsonProperty("VHRNO")
private String vhrno;
/** 차대번호 */
@JsonProperty("VIN")
private String vin;
/** 차종분류코드 */
@JsonProperty("VHCTY_ASORT_CODE")
private String vhctyAsortCode;
/** 차종분류명 */
@JsonProperty("VHCTY_ASORT_NM")
private String vhctyAsortNm;
/** 차명 */
@JsonProperty("CNM")
private String cnm;
/** 색상코드 */
@JsonProperty("COLOR_CODE")
private String colorCode;
/** 색상명 */
@JsonProperty("COLOR_NM")
private String colorNm;
/** 번호판규격코드 */
@JsonProperty("NMPL_STNDRD_CODE")
private String nmplStndrdCode;
/** 번호판규격명 */
@JsonProperty("NMPL_STNDRD_NM")
private String nmplStndrdNm;
/** 용도구분코드 */
@JsonProperty("PRPOS_SE_CODE")
private String prposSeCode;
/** 용도구분명 */
@JsonProperty("PRPOS_SE_NM")
private String prposSeNm;
/** 제작사명 */
@JsonProperty("MTRS_FOM_NM")
private String mtrsFomNm;
/** 형식명 */
@JsonProperty("FOM_NM")
private String fomNm;
/** 취득금액 */
@JsonProperty("ACQS_AMOUNT")
private String acqsAmount;
/** 등록상세코드 */
@JsonProperty("REGIST_DETAIL_CODE")
private String registDetailCode;
/** 등록상세명 */
@JsonProperty("REGIST_DETAIL_NM")
private String registDetailNm;
/** 최초등록일자 */
@JsonProperty("FRST_REGIST_DE")
private String frstRegistDe;
/** 자동차보험종료일자 */
@JsonProperty("CAAG_ENDDE")
private String caagEndde;
/** 생산년도 */
@JsonProperty("PRYE")
private String prye;
/** 사양번호1 */
@JsonProperty("SPMNNO1")
private String spmnno1;
/** 사양번호2 */
@JsonProperty("SPMNNO2")
private String spmnno2;
/** 연식월 */
@JsonProperty("YBL_MD")
private String yblMd;
/** 주행거리 */
@JsonProperty("TRVL_DSTNC")
private String trvlDstnc;
/** 검사유효기간시작일자 */
@JsonProperty("INSPT_VALID_PD_BGNDE")
private String insptValidPdBgnde;
/** 검사유효기간종료일자 */
@JsonProperty("INSPT_VALID_PD_ENDDE")
private String insptValidPdEndde;
/** 점검유효기간시작일자 */
@JsonProperty("CHCK_VALID_PD_BGNDE")
private String chckValidPdBgnde;
/** 점검유효기간종료일자 */
@JsonProperty("CHCK_VALID_PD_ENDDE")
private String chckValidPdEndde;
/** 등록신청구분명 */
@JsonProperty("REGIST_REQST_SE_NM")
private String registReqstSeNm;
/** 최초등록신청번호 */
@JsonProperty("FRST_REGIST_RQRCNO")
private String frstRegistRqrcno;
/** 번호판보관반납일자 */
@JsonProperty("NMPL_CSDY_REMNR_DE")
private String nmplCsdyRemnrDe;
/** 번호판보관여부 */
@JsonProperty("NMPL_CSDY_AT")
private String nmplCsdyAt;
/** 영업용사용기간 */
@JsonProperty("BSS_USE_PD")
private String bssUsePd;
/** 직권말소예방공지일자 */
@JsonProperty("OCTHT_ERSR_PRVNTC_NTICE_DE")
private String octhtErsrPrvntcNticeDe;
/** 말소등록일자 */
@JsonProperty("ERSR_REGIST_DE")
private String ersrRegistDe;
/** 말소등록구분코드 */
@JsonProperty("ERSR_REGIST_SE_CODE")
private String ersrRegistSeCode;
/** 말소등록구분명 */
@JsonProperty("ERSR_REGIST_SE_NM")
private String ersrRegistSeNm;
/** 저당건수 */
@JsonProperty("MRTGCNT")
private String mrtgcnt;
/** 차량건수 */
@JsonProperty("VHCLECNT")
private String vhclecnt;
/** 압인건수 */
@JsonProperty("STMDCNT")
private String stmdcnt;
/** 주소1 */
@JsonProperty("ADRES1")
private String adres1;
/** 주소명1 */
@JsonProperty("ADRES_NM1")
private String adresNm1;
/** 주소 */
@JsonProperty("ADRES")
private String adres;
/** 주소명 */
@JsonProperty("ADRES_NM")
private String adresNm;
/** 개인법인여부 */
@JsonProperty("INDVDL_BSNM_AT")
private String indvdlBsnmAt;
/** 전화번호 */
@JsonProperty("TELNO")
private String telno;
/** 소유자명 */
@JsonProperty("MBER_NM")
private String mberNm;
/** 소유자구분코드 */
@JsonProperty("MBER_SE_CODE")
private String mberSeCode;
/** 소유자구분번호 */
@JsonProperty("MBER_SE_NO")
private String mberSeNo;
/** 면세대상자구분코드 */
@JsonProperty("TAXXMPT_TRGTER_SE_CODE")
private String taxxmptTrgterSeCode;
/** 면세대상자구분코드명 */
@JsonProperty("TAXXMPT_TRGTER_SE_CODE_NM")
private String taxxmptTrgterSeCodeNm;
/** 건수사항 */
@JsonProperty("CNT_MATTER")
private String cntMatter;
/** 읍면동명 */
@JsonProperty("EMD_NM")
private String emdNm;
/** 예방건수 */
@JsonProperty("PRVNTCCNT")
private String prvntccnt;
/** 수출이행여부확인일자 */
@JsonProperty("XPORT_FLFL_AT_STTEMNT_DE")
private String xportFlflAtSttemntDe;
/** 협력사신청번호 */
@JsonProperty("PARTN_RQRCNO")
private String partnRqrcno;
/** 레코드 목록 */
@JsonProperty("record")
private List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(name = "LedgerRecord", description = "원부 변경내역 record")
@Getter
@Setter
public static class Record {
/** 메인체크 */
@JsonProperty("MAINCHK") private String mainchk;
/** 변경작업구분코드 */
@JsonProperty("CHANGE_JOB_SE_CODE") private String changeJobSeCode;
/** 주번호 */
@JsonProperty("MAINNO") private String mainno;
/** 부번호 */
@JsonProperty("SUBNO") private String subno;
/** 상세내역 */
@JsonProperty("DTLS") private String dtls;
/** 신청번호 */
@JsonProperty("RQRCNO") private String rqrcno;
/** 차량관리번호 */
@JsonProperty("VHMNO") private String vhmno;
/** 원부그룹번호 */
@JsonProperty("LEDGER_GROUP_NO") private String ledgerGroupNo;
/** 원부개별화번호 */
@JsonProperty("LEDGER_INDVDLZ_NO") private String ledgerIndvdlzNo;
/** 구분명 */
@JsonProperty("GUBUN_NM") private String gubunNm;
/** 변경일자 */
@JsonProperty("CHANGE_DE") private String changeDe;
/** 상세일련번호 */
@JsonProperty("DETAIL_SN") private String detailSn;
/** 플래그 */
@JsonProperty("FLAG") private String flag;
public String getMainchk() { return mainchk; }
public void setMainchk(String mainchk) { this.mainchk = mainchk; }
public String getChangeJobSeCode() { return changeJobSeCode; }
public void setChangeJobSeCode(String changeJobSeCode) { this.changeJobSeCode = changeJobSeCode; }
public String getMainno() { return mainno; }
public void setMainno(String mainno) { this.mainno = mainno; }
public String getSubno() { return subno; }
public void setSubno(String subno) { this.subno = subno; }
public String getDtls() { return dtls; }
public void setDtls(String dtls) { this.dtls = dtls; }
public String getRqrcno() { return rqrcno; }
public void setRqrcno(String rqrcno) { this.rqrcno = rqrcno; }
public String getVhmno() { return vhmno; }
public void setVhmno(String vhmno) { this.vhmno = vhmno; }
public String getLedgerGroupNo() { return ledgerGroupNo; }
public void setLedgerGroupNo(String ledgerGroupNo) { this.ledgerGroupNo = ledgerGroupNo; }
public String getLedgerIndvdlzNo() { return ledgerIndvdlzNo; }
public void setLedgerIndvdlzNo(String ledgerIndvdlzNo) { this.ledgerIndvdlzNo = ledgerIndvdlzNo; }
public String getGubunNm() { return gubunNm; }
public void setGubunNm(String gubunNm) { this.gubunNm = gubunNm; }
public String getChangeDe() { return changeDe; }
public void setChangeDe(String changeDe) { this.changeDe = changeDe; }
public String getDetailSn() { return detailSn; }
public void setDetailSn(String detailSn) { this.detailSn = detailSn; }
public String getFlag() { return flag; }
public void setFlag(String flag) { this.flag = flag; }
}
}

@ -0,0 +1,136 @@
package go.kr.project.api.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* ()
*
* <p>
* . ,
* record .
* </p>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "자동차기본사항조회 응답 모델 (신버전)")
@Getter
@Setter
public class NewBasicResponse { // 총 3개 필드 (외부 클래스 3개, 중첩 Record 클래스 84개)
/** LOG 생성 키 (내부 처리용) */
private String generatedId;
/** 트랜잭션 ID (내부 처리용) */
private String txId;
/** 연계결과코드 (성공/실패 코드) */
@JsonProperty("LINK_RSLT_CD")
private String linkRsltCd;
/** 연계결과상세 (에러 메시지 등 상세 사유) */
@JsonProperty("LINK_RSLT_DTL")
private String linkRsltDtl;
/** 응답 레코드 목록 */
@JsonProperty("record")
private List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(name = "NewBasicRecord", description = "기본사항 record 항목 (신버전)")
@Getter
@Setter
public static class Record {
@JsonProperty("VHRNO") private String vhrno; // 차량번호
@JsonProperty("ATMB_NM") private String atmbNm; // 차명
@JsonProperty("RPRS_OWNR_NM") private String rprsOwnrNm; // 대표소유자성명
@JsonProperty("RPRSV_OWNR_IDECNO") private String rprsvOwnrIdecno; // 대표소유자회원번호
@JsonProperty("ERSR_REG_YMD") private String ersrRegYmd; // 말소등록일
@JsonProperty("PRCS_IMPRTY_RSN_CD") private String prcsImprtyRsnCd; // 처리불가사유코드
@JsonProperty("PRCS_IMPRTY_RSN_DTLS") private String prcsImprtyRsnDtls; // 처리불가사유명세
@JsonProperty("YRIDNW") private String yridnw; // 연식
@JsonProperty("VIN") private String vin; // 차대번호
@JsonProperty("CARMDL_ASORT_NM") private String carmdlAsortNm; // 차종종별명
@JsonProperty("FRST_REG_YMD") private String frstRegYmd; // 최초등록일
@JsonProperty("COLOR_NM") private String colorNm; // 색상명
@JsonProperty("STRCT_CHG_CNT") private String strctChgCnt; // 구조변경수
@JsonProperty("NOPLT_CSDY_YN") private String noplTcsdyYn; // 번호판영치여부
@JsonProperty("NOPLT_CSDY_AVTSMT_YMD") private String noplTcsdyAvtsmtYmd; // 번호판영치최고일
@JsonProperty("INSP_VLD_PD_BGNG_YMD") private String inspVldPdBgngYmd; // 검사유효기간시작일
@JsonProperty("INSP_VLD_PD_END_YMD") private String inspVldPdEndYmd; // 검사유효기간종료일
@JsonProperty("SPMNNO") private String spmnno; // 제원관리번호
@JsonProperty("DRVNG_DSTNC") private String drvngDstnc; // 주행거리
@JsonProperty("FOM_NM") private String fomNm; // 형식
@JsonProperty("DSPLVL") private String dsplvl; // 배기량
@JsonProperty("CARMDL_CLSF_NM") private String carmdlClsfNm; // 차종분류명
@JsonProperty("FBCTN_YMD") private String fbctnYmd; // 제작년월일
@JsonProperty("USGSRHLD_ADDR_NM") private String usgsrhldAddrNm; // 사용본거지상세주소
@JsonProperty("MTRS_FOM_NM") private String mtrsFomNm; // 원동기형식명
@JsonProperty("RDCPCT_CNT") private String rdcpctCnt; // 승차정원수
@JsonProperty("FRST_REG_APLY_RCPT_NO") private String frstRegAplyRcptNo; // 최초등록접수번호
@JsonProperty("OGNZ_NM") private String ognzNm; // 등록기관명
@JsonProperty("ERSR_REG_SE_NM") private String ersrRegSeNm; // 말소등록구분명
@JsonProperty("BFR_VHRNO") private String bfrVhrno; // 이전차량번호
@JsonProperty("USE_FUEL_CD") private String useFuelCd; // 사용연료코드
@JsonProperty("RPRS_OWNR_MBR_SE_CD") private String rprsOwnrMbrSeCd; // 대표소유자회원구분코드
@JsonProperty("RPRS_OWNR_TELNO") private String rprsOwnrTelno; // 대표소유자전화번호
@JsonProperty("OWNR_STDG_CD") private String ownrStdgCd; // 소유자법정동코드
@JsonProperty("OWNR_WHOL_ADDR") private String ownrWholAddr; // 소유자전체주소
@JsonProperty("VHCL_TOTL_WT") private String vhclTotlWt; // 차량총중량
@JsonProperty("MXMM_LDG") private String mxmmLdg; // 최대적재량
@JsonProperty("CBD_LT") private String cbdLt; // 차체길이
@JsonProperty("CBD_BT") private String cbdBt; // 차체너비
@JsonProperty("CBD_HG") private String cbdHg; // 차체높이
@JsonProperty("CARMDL_ASORT_CD") private String carmdlAsortCd; // 차종종별코드
@JsonProperty("CARMDL_TYPE_CD") private String carmdlTypeCd; // 차종유형코드
@JsonProperty("FUEL_CNSMPRT") private String fuelCnsmprt; // 연료소비율
@JsonProperty("ERSR_REG_SE_CD") private String ersrRegSeCd; // 말소등록구분코드
@JsonProperty("REG_DTL_CD") private String regDtlCd; // 등록상세코드
@JsonProperty("USGSRHLD_STDG_CD") private String usgsrhldStdgCd; // 사용본거지법정동코드
@JsonProperty("USGSRHLD_DONG_CD") private String usgsrhldDongCd; // 사용본거지행정동코드
@JsonProperty("USGSRHLD_MTN_YN") private String usgsrhldMtnYn; // 사용본거지산 여부
@JsonProperty("USGSRHLD_LNBR") private String usgsrhldLnbr; // 사용본거지번지
@JsonProperty("USGSRHLD_HO") private String usgsrhldHo; // 사용본거지호
@JsonProperty("USGSRHLD_ROAD_NM_CD") private String usgsrhldRoadNmCd; // 사용본거지도로명코드
@JsonProperty("USGSRHLD_UDGD_BLDG_SE_CD") private String usgsrhldUdgdBldgSeCd; // 사용본거지지하건물구분코드
@JsonProperty("USGSRHLD_BMNO") private String usgsrhldBmno; // 사용본거지건물주요번호
@JsonProperty("USGSRHLD_BSNO") private String usgsrhldBsno; // 사용본거지건물부번호
@JsonProperty("OWNR_DONG_CD") private String ownrDongCd; // 소유자행정동코드
@JsonProperty("OWNR_MTN_YN") private String ownrMtnYn; // 소유자산 여부
@JsonProperty("OWNR_LNBR") private String ownrLnbr; // 소유자번지
@JsonProperty("OWNR_HO") private String ownrHo; // 소유자호
@JsonProperty("OWNR_ADDR_NM") private String ownrAddrNm; // 소유자상세주소
@JsonProperty("OWNR_ROAD_NM_CD") private String ownrRoadNmCd; // 소유자도로명코드
@JsonProperty("OWNR_UDGD_BLDG_SE_CD") private String ownrUdgdBldgSeCd; // 소유자지하건물구분코드
@JsonProperty("OWNR_BMNO") private String ownrBmno; // 소유자건물주요번호
@JsonProperty("OWNR_BSNO") private String ownrBsno; // 소유자건물부번호
@JsonProperty("REAR_VHRNO") private String rearVhrno; // 신차량번호
@JsonProperty("USG_SE_CD") private String usgSeCd; // 용도구분코드
@JsonProperty("VEAG_END_YMD") private String veagEndYmd; // 차령만료일자
@JsonProperty("CHG_YMD") private String chgYmd; // 차번호변경시기
@JsonProperty("CARMDL_SE_CD") private String carmdlSeCd; // 차종분류코드
@JsonProperty("CARMDL_TYPE_NM") private String carmdlTypeNm; // 차종유형명
@JsonProperty("ACQS_YMD") private String acqsYmd; // 취득일자
@JsonProperty("ACQS_END_YMD") private String acqsEndYmd; // 취득종료일자
@JsonProperty("TRANSR_REG_YMD") private String transrRegYmd; // 이전등록일(양수일)
@JsonProperty("SPCF_REG_STTS_CD") private String spcfRegSttsCd; // 제원등록상태코드
@JsonProperty("SRC_SE_CD") private String srcSeCd; // 출처구분코드
@JsonProperty("NOPLT_SPCFCT_CD") private String noplTSpcfctCd; // 번호판규격코드
@JsonProperty("ACQS_AMT") private String acqsAmt; // 취득금액
@JsonProperty("USGSRHLD_GRC_CD") private String usgsrhldGrcCd; // 사용본거지관청코드
@JsonProperty("VLNT_ERSR_PRVNTC_AVTSMT_YMD") private String vlntErsrPrvntcAvtsmtYmd; // 예고통지일
@JsonProperty("FRST_MXMM_LDG") private String frstMxmmLdg; // 최초최대적재량
@JsonProperty("REG_YMD") private String regYmd; // 등록일(변경일)
@JsonProperty("ELCTY_CMPND_FUEL_CNSMPRT") private String elctyCmpndFuelCnsmprt; // 전기복합연료소비율
@JsonProperty("USGSRHLD_WHOL_ADDR") private String usgsrhldWholAddr; // 사용본거지전체주소
@JsonProperty("MRTG_CNT") private String mrtgCnt; // 저당수
@JsonProperty("SZR_CNT") private String szrCnt; // 압류건수
}
}

@ -0,0 +1,349 @@
package go.kr.project.api.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "자동차등록원부(갑) 응답 모델 (신버전)")
@Getter
@Setter
public class NewLedgerResponse { // 총 64개 필드 (외부 클래스 66개, 중첩 Record 클래스 16개)
/** LOG 생성 키 (내부 처리용) */
private String generatedId;
/** 트랜잭션 ID (내부 처리용) */
private String txId;
@JsonProperty("LINK_RSLT_CD")
private String linkRsltCd;
@JsonProperty("LINK_RSLT_DTL")
private String linkRsltDtl;
/** 원부그룹번호 */
@JsonProperty("LEDGER_GROUP_NO")
private String ledgerGroupNo;
/** 원부개별번호 */
@JsonProperty("LEDGER_INDIV_NO")
private String ledgerIndivNo;
/** 차량관리번호 */
@JsonProperty("VHMNO")
private String vhmno;
/** 차량등록번호 */
@JsonProperty("VHRNO")
private String vhrno;
/** 차대번호 */
@JsonProperty("VIN")
private String vin;
/** 차종종별코드 */
@JsonProperty("CARMDL_ASORT_CD")
private String carmdlAsortCd;
/** 차종종별명 */
@JsonProperty("CARMDL_ASORT_NM")
private String carmdlAsortNm;
/** 차명 */
@JsonProperty("ATMB_NM")
private String atmbNm;
/** 색상코드 */
@JsonProperty("COLOR_CD")
private String colorCd;
/** 색상명 */
@JsonProperty("COLOR_NM")
private String colorNm;
/** 번호판규격코드 */
@JsonProperty("NOPLT_SPCFCT_CD")
private String nopltSpcfctCd;
/** 번호판규격명 */
@JsonProperty("NOPLT_SPCFCT_NM")
private String nopltSpcfctNm;
/** 용도구분코드 */
@JsonProperty("USG_SE_CD")
private String usgSeCd;
/** 용도구분명 */
@JsonProperty("USG_SE_NM")
private String usgSeNm;
/** 원동기형식명 */
@JsonProperty("MTRS_FOM_NM")
private String mtrsFomNm;
/** 형식명 */
@JsonProperty("FOM_NM")
private String fomNm;
/** 취득금액 */
@JsonProperty("ACQS_AMT")
private String acqsAmt;
/** 등록상세코드 */
@JsonProperty("REG_DTL_CD")
private String regDtlCd;
/** 등록상세명 */
@JsonProperty("REG_DTL_NM")
private String regDtlNm;
/** 최초등록일 */
@JsonProperty("FRST_REG_YMD")
private String frstRegYmd;
/** 차령종료일 */
@JsonProperty("VEAG_END_YMD")
private String veagEndYmd;
/** 연식 */
@JsonProperty("YRIDNW")
private String yridnw;
/** 제원관리번호1 */
@JsonProperty("SPMNNO_1")
private String spmnno1;
/** 제원관리번호2 */
@JsonProperty("SPMNNO_2")
private String spmnno2;
/** 제작년월일 */
@JsonProperty("FBCTN_YMD")
private String fbctnYmd;
/** 주행거리 */
@JsonProperty("DRVNG_DSTNC")
private String drvngDstnc;
/** 검사유효기간시작일 */
@JsonProperty("INSP_VLD_PD_BGNG_YMD")
private String inspVldPdBgngYmd;
/** 검사유효기간종료일 */
@JsonProperty("INSP_VLD_PD_END_YMD")
private String inspVldPdEndYmd;
/** 점검유효기간시작일 */
@JsonProperty("CHCK_VLD_PD_BGNG_YMD")
private String chckVldPdBgngYmd;
/** 점검유효기간종료일 */
@JsonProperty("CHCK_VLD_PD_END_YMD")
private String chckVldPdEndYmd;
/** 등록신청구분명 */
@JsonProperty("REG_APLY_SE_NM")
private String regAplySeNm;
/** 최초등록접수번호 */
@JsonProperty("FRST_REG_APLY_RCPT_NO")
private String frstRegAplyRcptNo;
/** 번호판영치최고일 */
@JsonProperty("NOPLT_CSDY_AVTSMT_YMD")
private String nopltCsdyAvtsmtYmd;
/** 번호판영치여부 */
@JsonProperty("NOPLT_CSDY_YN")
private String nopltCsdyYn;
/** 사업용사용기간 */
@JsonProperty("BSS_USE_PD_YMD")
private String bssUsePdYmd;
/** 직권말소예고통지일 */
@JsonProperty("OCTHT_ERSR_PRVNTC_AVTSMT_YMD")
private String octhtErsrPrvntcAvtsmtYmd;
/** 말소등록일 */
@JsonProperty("ERSR_REG_YMD")
private String ersrRegYmd;
/** 말소등록구분코드 */
@JsonProperty("ERSR_REG_SE_CD")
private String ersrRegSeCd;
/** 말소등록구분명 */
@JsonProperty("ERSR_REG_SE_NM")
private String ersrRegSeNm;
/** 저당수 */
@JsonProperty("MRTG_CNT")
private String mrtgCnt;
/** 압류건수 */
@JsonProperty("SZR_CNT")
private String szrCnt;
/** 구조변경수 */
@JsonProperty("STRCT_CHG_CNT")
private String strctChgCnt;
/** 사용본거지주소 */
@JsonProperty("USGSRHLD_ADDR_1")
private String usgsrhldAddr1;
/** 사용본거지주소상세 */
@JsonProperty("USGSRHLD_ADDR_DTL_1")
private String usgsrhldAddrDtl1;
/** 소유자주소 */
@JsonProperty("OWNR_ADDR")
private String ownrAddr;
/** 소유자주소상세 */
@JsonProperty("OWNR_ADDR_DTL")
private String ownrAddrDtl;
/** 개인사업자여부 */
@JsonProperty("INDVDL_BZMN_YN")
private String indvdlBzmnYn;
/** 대표소유자전화번호 */
@JsonProperty("RPRS_OWNR_TELNO")
private String rprsOwnrTelno;
/** 대표소유자성명 */
@JsonProperty("RPRS_OWNR_NM")
private String rprsOwnrNm;
/** 대표소유자회원구분코드 */
@JsonProperty("RPRS_OWNR_MBR_SE_CD")
private String rprsOwnrMbrSeCd;
/** 대표소유자회원번호 */
@JsonProperty("RPRSV_OWNR_IDECNO")
private String rprsvOwnrIdecno;
/** 비과세대상구분코드명 */
@JsonProperty("TAXXMPT_APLCN_SE_CD")
private String taxxmptAplcnSeCd;
/** 비과세대상구분코드 */
@JsonProperty("TAXXMPT_TRPR_SE_CD")
private String taxxmptTrprSeCd;
/** 특기사항건수 */
@JsonProperty("SPCABL_MTTR_CNT")
private String spcablMttrCnt;
/** 사용본거지행정동명 */
@JsonProperty("USGSRHLD_DONG_NM")
private String usgsrhldDongNm;
/** 예고수 */
@JsonProperty("PRVNTC_CNT")
private String prvntcCnt;
/** 수출이행여부신고일 */
@JsonProperty("XPORT_FLFL_YN_DCLR_YMD")
private String xportFlflYnDclrYmd;
/** 발급번호 */
@JsonProperty("ISSU_NO")
private String issuNo;
/** 최초양도일 */
@JsonProperty("FRST_TRNSFR_YMD")
private String frstTrnsfrYmd;
/** 구동축전지식별번호 */
@JsonProperty("DRIV_SRGBTRY_IDNTF_NO")
private String drivSrgbtryIdntfNo;
/** 처리불가사유코드 */
@JsonProperty("PROCESS_IMPRTY_RESN_CODE")
private String processImprtyResnCode;
/** 처리불가사유상세 */
@JsonProperty("PROCESS_IMPRTY_RESN_DTLS")
private String processImprtyResnDtls;
@JsonProperty("record")
private List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(name = "NewLedgerRecord", description = "원부 변경내역 record (신버전)")
@Getter
@Setter
public static class Record {
/** 해제여부 */
@JsonProperty("SZR_RMV_DTL_SN")
private String szrRmvDtlSn;
/** 변경업무구분코드 */
@JsonProperty("CHG_TASK_SE_CD")
private String chgTaskSeCd;
/** 주번호 */
@JsonProperty("MAIN_NO")
private String mainNo;
/** 부번호 */
@JsonProperty("SNO")
private String sno;
/** 사항란 */
@JsonProperty("SPCABL_MTTR")
private String spcablMttr;
/** 세대주명 */
@JsonProperty("HSHLDR_NM")
private String hshldrNm;
/** 세대주개인암호화번호 */
@JsonProperty("HSHLDR_IDECNO")
private String hshldrIdecno;
/** 접수번호 */
@JsonProperty("APLY_RCPT_NO")
private String aplyRcptNo;
/** 차량관리번호 */
@JsonProperty("VHMNO")
private String vhmno;
/** 원부그룹번호 */
@JsonProperty("LEDGER_GROUP_NO")
private String ledgerGroupNo;
/** 원부개별번호 */
@JsonProperty("LEDGER_INDIV_NO")
private String ledgerIndivNo;
/** 변경업무구분명 */
@JsonProperty("CHG_TASK_SE_NM")
private String chgTaskSeNm;
/** 변경일자 */
@JsonProperty("CHG_YMD")
private String chgYmd;
/** 상세순번 */
@JsonProperty("DTL_SN")
private String dtlSn;
/** 표기여부 */
@JsonProperty("FLAG")
private String flag;
}
}

@ -0,0 +1,132 @@
package go.kr.project.api.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* ()
*
* <p>
* (/ ) .
* / , record .
* </p>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "자동차기본사항조회 응답 모델 (구버전)")
@Getter
@Setter
public class OldBasicResponse { // 총 3개 필드 (외부 클래스 3개, 중첩 Record 클래스 84개)
/** 연계결과코드 */
@JsonProperty("CNTC_RESULT_CODE")
private String cntcResultCode;
/** 연계결과상세 */
@JsonProperty("CNTC_RESULT_DTLS")
private String cntcResultDtls;
/** 응답 레코드 목록 */
@JsonProperty("record")
private List<Record> record;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(name = "OldBasicRecord", description = "기본사항 record 항목 (구버전)")
@Getter
@Setter
public static class Record {
@JsonProperty("PRYE") private String prye; // 연식
@JsonProperty("REGIST_DE") private String registDe; // 등록일자(변경일)
@JsonProperty("ERSR_REGIST_SE_CODE") private String ersrRegistSeCode; // 말소등록구분코드
@JsonProperty("ERSR_REGIST_SE_NM") private String ersrRegistSeNm; // 말소등록구분명
@JsonProperty("ERSR_REGIST_DE") private String ersrRegistDe; // 말소등록일자
@JsonProperty("REGIST_DETAIL_CODE") private String registDetailCode; // 등록상세코드
@JsonProperty("DSPLVL") private String dsplvl; // 배기량
@JsonProperty("USE_STRNGHLD_LEGALDONG_CODE") private String useStrnghldLegaldongCode; // 사용본거지법정동코드
@JsonProperty("USE_STRNGHLD_ADSTRD_CODE") private String useStrnghldAdstrdCode; // 사용본거지행정동코드
@JsonProperty("USE_STRNGHLD_MNTN") private String useStrnghldMntn; // 사용본거지산 여부
@JsonProperty("USE_STRNGHLD_LNBR") private String useStrnghldLnbr; // 사용본거지번지
@JsonProperty("USE_STRNGHLD_HO") private String useStrnghldHo; // 사용본거지호
@JsonProperty("USE_STRNGHLD_ADRES_NM") private String useStrnghldAdresNm; // 사용본거지상세주소
@JsonProperty("USE_STRNGHLD_ROAD_NM_CODE") private String useStrnghldRoadNmCode; // 사용본거지도로명코드
@JsonProperty("USGSRHLD_UNDGRND_BULD_SE_CODE") private String usgsrhldUndgrndBuldSeCode; // 사용본거지지하건물구분코드
@JsonProperty("USE_STRNGHLD_BULD_MAIN_NO") private String useStrnghldBuldMainNo; // 사용본거지건물주요번호
@JsonProperty("USE_STRNGHLD_BULD_SUB_NO") private String useStrnghldBuldSubNo; // 사용본거지건물부번호
@JsonProperty("USGSRHLD_ADRES_FULL") private String usgsrhldAdresFull; // 사용본거지전체주소
@JsonProperty("MBER_SE_CODE") private String mberSeCode; // 대표소유자회원구분코드
@JsonProperty("MBER_NM") private String mberNm; // 대표소유자성명
@JsonProperty("MBER_SE_NO") private String mberSeNo; // 대표소유자회원번호
@JsonProperty("TELNO") private String telno; // 대표소유자전화번호
@JsonProperty("OWNER_LEGALDONG_CODE") private String ownerLegaldongCode; // 소유자법정동코드
@JsonProperty("OWNER_ADSTRD_CODE") private String ownerAdstrdCode; // 소유자행정동코드
@JsonProperty("OWNER_MNTN") private String ownerMntn; // 소유자산 여부
@JsonProperty("OWNER_LNBR") private String ownerLnbr; // 소유자번지
@JsonProperty("OWNER_HO") private String ownerHo; // 소유자호
@JsonProperty("OWNER_ADRES_NM") private String ownerAdresNm; // 소유자상세주소
@JsonProperty("OWNER_ROAD_NM_CODE") private String ownerRoadNmCode; // 소유자도로명코드
@JsonProperty("OWNER_UNDGRND_BULD_SE_CODE") private String ownerUndgrndBuldSeCode; // 소유자지하건물구분코드
@JsonProperty("OWNER_BULD_MAIN_NO") private String ownerBuldMainNo; // 소유자건물주요번호
@JsonProperty("OWNER_BULD_SUB_NO") private String ownerBuldSubNo; // 소유자건물부번호
@JsonProperty("OWNER_ADRES_FULL") private String ownerAdresFull; // 소유자전체주소
@JsonProperty("AFTR_VHRNO") private String aftrVhrno; // 신차량번호
@JsonProperty("USE_FUEL_CODE") private String useFuelCode; // 사용연료코드
@JsonProperty("PRPOS_SE_CODE") private String prposSeCode; // 용도구분코드
@JsonProperty("MTRS_FOM_NM") private String mtrsFomNm; // 원동기형식명
@JsonProperty("FRNT_VHRNO") private String frntVhrno; // 이전차량번호
@JsonProperty("VHRNO") private String vhrno; // 차량번호
@JsonProperty("VIN") private String vin; // 차대번호
@JsonProperty("CNM") private String cnm; // 차명
@JsonProperty("VHCLE_TOT_WT") private String vhcleTotWt; // 차량총중량
@JsonProperty("CAAG_ENDDE") private String caagEndde; // 차령만료일자
@JsonProperty("CHANGE_DE") private String changeDe; // 차번호변경시기
@JsonProperty("VHCTY_ASORT_CODE") private String vhctyAsortCode; // 차종종별코드
@JsonProperty("VHCTY_TY_CODE") private String vhctyTyCode; // 차종유형코드
@JsonProperty("VHCTY_SE_CODE") private String vhctySeCode; // 차종분류코드
@JsonProperty("MXMM_LDG") private String mxmmLdg; // 최대적재량
@JsonProperty("VHCTY_ASORT_NM") private String vhctyAsortNm; // 차종종별명
@JsonProperty("VHCTY_TY_NM") private String vhctyTyNm; // 차종유형명
@JsonProperty("VHCTY_SE_NM") private String vhctySeNm; // 차종분류명
@JsonProperty("FRST_REGIST_DE") private String frstRegistDe; // 최초등록일
@JsonProperty("FOM_NM") private String fomNm; // 형식
@JsonProperty("ACQS_DE") private String acqsDe; // 취득일자
@JsonProperty("ACQS_END_DE") private String acqsEndDe; // 취득종료일자
@JsonProperty("YBL_MD") private String yblMd; // 제작연월(추정)
@JsonProperty("TRANSR_REGIST_DE") private String transrRegistDe; // 이전등록일(양수일)
@JsonProperty("SPCF_REGIST_STTUS_CODE") private String spcfRegistSttusCode; // 제원등록상태코드
@JsonProperty("COLOR_NM") private String colorNm; // 색상명
@JsonProperty("MRTG_CO") private String mrtgCo; // 저당수
@JsonProperty("SEIZR_CO") private String seizrCo; // 압류건수
@JsonProperty("STMD_CO") private String stmdCo; // 기타 설정/처분 건수(추정)
@JsonProperty("NMPL_CSDY_AT") private String nmplCsdyAt; // 번호판영치여부
@JsonProperty("NMPL_CSDY_REMNR_DE") private String nmplCsdyRemnrDe; // 번호판영치최고일
@JsonProperty("ORIGIN_SE_CODE") private String originSeCode; // 출처구분코드
@JsonProperty("NMPL_STNDRD_CODE") private String nmplStndrdCode; // 번호판규격코드
@JsonProperty("ACQS_AMOUNT") private String acqsAmount; // 취득금액
@JsonProperty("INSPT_VALID_PD_BGNDE") private String insptValidPdBgnde; // 검사유효기간시작일
@JsonProperty("INSPT_VALID_PD_ENDDE") private String insptValidPdEndde; // 검사유효기간종료일
@JsonProperty("USE_STRNGHLD_GRC_CODE") private String useStrnghldGrcCode; // 사용본거지관청코드
@JsonProperty("TKCAR_PSCAP_CO") private String tkcarPscapCo; // 승차정원수
@JsonProperty("SPMNNO") private String spmnno; // 제원관리번호
@JsonProperty("TRVL_DSTNC") private String trvlDstnc; // 주행거리
@JsonProperty("FRST_REGIST_RQRCNO") private String frstRegistRqrcno; // 최초등록접수번호
@JsonProperty("VLNT_ERSR_PRVNTC_NTICE_DE") private String vlntErsrPrvntcNticeDe; // 예고통지일
@JsonProperty("REGIST_INSTT_NM") private String registInsttNm; // 등록기관명
@JsonProperty("PROCESS_IMPRTY_RESN_CODE") private String processImprtyResnCode; // 처리불가사유코드
@JsonProperty("PROCESS_IMPRTY_RESN_DTLS") private String processImprtyResnDtls; // 처리불가사유명세
@JsonProperty("CBD_LT") private String cbdLt; // 차체길이
@JsonProperty("CBD_BT") private String cbdBt; // 차체너비
@JsonProperty("CBD_HG") private String cbdHg; // 차체높이
@JsonProperty("FRST_MXMM_LDG") private String frstMxmmLdg; // 최초최대적재량
@JsonProperty("FUEL_CNSMP_RT") private String fuelCnsmpRt; // 연료소비율
@JsonProperty("ELCTY_CMPND_FUEL_CNSMP_RT") private String elctyCmpndFuelCnsmpRt; // 전기복합연료소비율
}
}

@ -1,647 +0,0 @@
package go.kr.project.api.model.response;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.model.Envelope;
import go.kr.project.api.model.request.BasicRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* , DB
*
* <p>API .
* INSERT, UPDATE .</p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VmisCarBassMatterInqireVO {
// ==== Static factory/mapping methods (moved from Service) ====
public static VmisCarBassMatterInqireVO fromRequest(BasicRequest request) {
return VmisCarBassMatterInqireVO.builder()
.infoSysId(request.getInfoSysId())
.infoSysIp(request.getInfoSysIp())
.sigunguCode(request.getSigunguCode())
.cntcInfoCode(request.getCntcInfoCode())
.chargerId(request.getChargerId())
.chargerIp(request.getChargerIp())
.chargerNm(request.getChargerNm())
.dmndLevyStdde(request.getLevyStdde())
.dmndInqireSeCode(request.getInqireSeCode())
.dmndVhrno(request.getVhrno())
.dmndVin(request.getVin())
.rgtr(ApiConstant.DEFAULT_REGISTRANT)
.build();
}
public static VmisCarBassMatterInqireVO fromResponse(String id, Envelope<BasicResponse> envelope) {
if (envelope == null || envelope.getData() == null || envelope.getData().isEmpty()) return null;
BasicResponse response = envelope.getData().get(0);
VmisCarBassMatterInqireVO.VmisCarBassMatterInqireVOBuilder builder = VmisCarBassMatterInqireVO.builder()
.carBassMatterInqireId(id)
.cntcResultCode(response.getCntcResultCode())
.cntcResultDtls(response.getCntcResultDtls());
if (response.getRecord() != null && !response.getRecord().isEmpty()) {
BasicResponse.Record record = response.getRecord().get(0);
applyRecord(builder, record);
}
return builder.build();
}
private static void applyRecord(VmisCarBassMatterInqireVO.VmisCarBassMatterInqireVOBuilder builder, BasicResponse.Record record) {
builder
.prye(record.getPrye()) // 연식
.registDe(record.getRegistDe()) // 등록일
.ersrRegistSeCode(record.getErsrRegistSeCode()) // 말소 등록 구분 코드
.ersrRegistSeNm(record.getErsrRegistSeNm()) // 말소 등록 구분명
.ersrRegistDe(record.getErsrRegistDe()) // 말소 등록일
.registDetailCode(record.getRegistDetailCode()) // 등록 상세 코드
.dsplvl(record.getDsplvl()) // 배기량
.useStrnghldLegaldongCode(record.getUseStrnghldLegaldongCode()) // 사용 본거지 법정동 코드
.useStrnghldAdstrdCode(record.getUseStrnghldAdstrdCode()) // 사용 본거지 행정동 코드
.useStrnghldMntn(record.getUseStrnghldMntn()) // 사용 본거지 산
.useStrnghldLnbr(record.getUseStrnghldLnbr()) // 사용 본거지 번지
.useStrnghldHo(record.getUseStrnghldHo()) // 사용 본거지 호
.useStrnghldAdresNm(record.getUseStrnghldAdresNm()) // 사용 본거지 상세주소
.useStrnghldRoadNmCode(record.getUseStrnghldRoadNmCode()) // 사용 본거지 도로명 코드
.usgsrhldUndgrndBuldSeCode(record.getUsgsrhldUndgrndBuldSeCode()) // 사용 본거지 지하 건물 구분 코드
.useStrnghldBuldMainNo(record.getUseStrnghldBuldMainNo()) // 사용 본거지 건물 주요 번호
.useStrnghldBuldSubNo(record.getUseStrnghldBuldSubNo()) // 사용 본거지 건물 부 번호
.usgsrhldAdresFull(record.getUsgsrhldAdresFull()) // 사용 본거지 전체주소
.mberSeCode(record.getMberSeCode()) // 대표소유자 회원 구분 코드
.mberSeNo(record.getMberSeNo()) // 대표소유자 회원 번호
.mberNm(record.getMberNm()) // 대표소유자 성명
.telno(record.getTelno()) // 대표소유자 전화번호
.ownerLegaldongCode(record.getOwnerLegaldongCode()) // 소유자 법정동 코드
.ownerAdstrdCode(record.getOwnerAdstrdCode()) // 소유자 행정동 코드
.ownerMntn(record.getOwnerMntn()) // 소유자 산
.ownerLnbr(record.getOwnerLnbr()) // 소유자 번지
.ownerHo(record.getOwnerHo()) // 소유자 호
.ownerAdresNm(record.getOwnerAdresNm()) // 소유자 상세주소
.ownerRoadNmCode(record.getOwnerRoadNmCode()) // 소유자 도로명 코드
.ownerUndgrndBuldSeCode(record.getOwnerUndgrndBuldSeCode()) // 소유자 지하건물 구분 코드
.ownerBuldMainNo(record.getOwnerBuldMainNo()) // 소유자 건물 주요 번호
.ownerBuldSubNo(record.getOwnerBuldSubNo()) // 소유자 건물 부 번호
.ownrWholaddr(record.getOwnerAdresFull()) // 소유자 전체주소
.aftrVhrno(record.getAftrVhrno()) // 신 차량번호
.useFuelCode(record.getUseFuelCode()) // 사용 연료 코드
.prposSeCode(record.getPrposSeCode()) // 용도 구분 코드
.mtrsFomNm(record.getMtrsFomNm()) // 원동기 형식명
.frntVhrno(record.getFrntVhrno()) // 이전 차량번호
.vhrno(record.getVhrno()) // 차량번호
.vin(record.getVin()) // 차대번호
.cnm(record.getCnm()) // 차명
.vhcleTotWt(record.getVhcleTotWt()) // 차량 총 중량
.caagEndde(record.getCaagEndde()) // 차령 만료일자
.changeDe(record.getChangeDe()) // 차번호 변경시기
.vhctyAsortCode(record.getVhctyAsortCode()) // 차종 종별 코드
.vhctyTyCode(record.getVhctyTyCode()) // 차종 유형 코드
.vhctySeCode(record.getVhctySeCode()) // 차종 분류 코드
.mxmmLdg(record.getMxmmLdg()) // 최대 적재량
.vhctyAsortNm(record.getVhctyAsortNm()) // 차종 종별명
.vhctyTyNm(record.getVhctyTyNm()) // 차종 유형명
.vhctySeNm(record.getVhctySeNm()) // 차종 분류명
.frstRegistDe(record.getFrstRegistDe()) // 최초 등록일
.fomNm(record.getFomNm()) // 형식
.acqsDe(record.getAcqsDe()) // 취득 일자
.acqsEndDe(record.getAcqsEndDe()) // 취득 종료일자
.yblMd(record.getYblMd()) // 제작 년월일
.transrRegistDe(record.getTransrRegistDe()) // 이전 등록일
.spcfRegistSttusCode(record.getSpcfRegistSttusCode()) // 제원 등록 상태 코드
.colorNm(record.getColorNm()) // 색상명
.mrtgCo(record.getMrtgCo()) // 저당수
.seizrCo(record.getSeizrCo()) // 압류건수
.stmdCo(record.getStmdCo()) // 구조변경수
.nmplCsdyAt(record.getNmplCsdyAt()) // 번호판 영치 여부
.nmplCsdyRemnrDe(record.getNmplCsdyRemnrDe()) // 번호판 영치 최고일
.originSeCode(record.getOriginSeCode()) // 출처 구분 코드
.nmplStndrdCode(record.getNmplStndrdCode()) // 번호판 규격 코드
.acqsAmount(record.getAcqsAmount()) // 취득 금액
.insptValidPdBgnde(record.getInsptValidPdBgnde()) // 검사 유효 기간 시작일
.insptValidPdEndde(record.getInsptValidPdEndde()) // 검사 유효 기간 종료일
.useStrnghldGrcCode(record.getUseStrnghldGrcCode()) // 사용 본거지 관청 코드
.tkcarPscapCo(record.getTkcarPscapCo()) // 승차정원수
.spmnno(record.getSpmnno()) // 제원관리번호
.trvlDstnc(record.getTrvlDstnc()) // 주행거리
.frstRegistRqrcno(record.getFrstRegistRqrcno()) // 최초 등록 접수번호
.vlntErsrPrvntcNticeDe(record.getVlntErsrPrvntcNticeDe()) // 예고통지일
.registInsttNm(record.getRegistInsttNm()) // 등록 기관명
.processImprtyResnCode(record.getProcessImprtyResnCode()) // 처리 불가 사유 코드
.processImprtyResnDtls(record.getProcessImprtyResnDtls()) // 처리 불가 사유 명세
.cbdLt(record.getCbdLt()) // 차체 길이
.cbdBt(record.getCbdBt()) // 차체 너비
.cbdHg(record.getCbdHg()) // 차체 높이
.frstMxmmLdg(record.getFrstMxmmLdg()) // 최초 최대 적재량
.fuelCnsmpRt(record.getFuelCnsmpRt()) // 연료 소비율
.elctyCmpndFuelCnsmpRt(record.getElctyCmpndFuelCnsmpRt()); // 전기 복합 연료 소비율
}
/**
* ID (PK)
* : CBMI000000000001
*/
private String carBassMatterInqireId;
// ===== 요청 정보 =====
/**
* ID
*/
private String infoSysId;
/**
* IP
*/
private String infoSysIp;
/**
*
*/
private String sigunguCode;
/**
*
*/
private String cntcInfoCode;
/**
* ID
*/
private String chargerId;
/**
* IP
*/
private String chargerIp;
/**
*
*/
private String chargerNm;
/**
*
*/
private String dmndLevyStdde;
/**
*
*/
private String dmndInqireSeCode;
/**
*
*/
private String dmndVhrno;
/**
*
*/
private String dmndVin;
// ===== 응답 정보 (결과 수신 시 UPDATE) =====
/**
*
*/
private String cntcResultCode;
/**
*
*/
private String cntcResultDtls;
/**
*
*/
private String prye;
/**
*
*/
private String registDe;
/**
*
*/
private String ersrRegistSeCode;
/**
*
*/
private String ersrRegistSeNm;
/**
*
*/
private String ersrRegistDe;
/**
*
*/
private String registDetailCode;
/**
*
*/
private String dsplvl;
/**
*
*/
private String useStrnghldLegaldongCode;
/**
*
*/
private String useStrnghldAdstrdCode;
/**
*
*/
private String useStrnghldMntn;
/**
*
*/
private String useStrnghldLnbr;
/**
*
*/
private String useStrnghldHo;
/**
*
*/
private String useStrnghldAdresNm;
/**
*
*/
private String useStrnghldRoadNmCode;
/**
*
*/
private String usgsrhldUndgrndBuldSeCode;
/**
*
*/
private String useStrnghldBuldMainNo;
/**
*
*/
private String useStrnghldBuldSubNo;
/**
*
*/
private String usgsrhldAdresFull;
/**
*
*/
private String mberSeCode;
/**
*
*/
private String mberSeNo;
/**
*
*/
private String telno;
/**
*
*/
private String ownerLegaldongCode;
/**
*
*/
private String ownerAdstrdCode;
/**
*
*/
private String ownerMntn;
/**
*
*/
private String ownerLnbr;
/**
*
*/
private String ownerHo;
/**
*
*/
private String ownerAdresNm;
/**
*
*/
private String ownerRoadNmCode;
/**
*
*/
private String ownerUndgrndBuldSeCode;
/**
*
*/
private String ownerBuldMainNo;
/**
*
*/
private String ownerBuldSubNo;
/**
*
*/
private String ownrWholaddr;
/**
*
*/
private String aftrVhrno;
/**
*
*/
private String useFuelCode;
/**
*
*/
private String prposSeCode;
/**
*
*/
private String mtrsFomNm;
/**
*
*/
private String frntVhrno;
/**
*
*/
private String vhrno;
/**
*
*/
private String vin;
/**
*
*/
private String cnm;
/**
*
*/
private String vhcleTotWt;
/**
*
*/
private String caagEndde;
/**
*
*/
private String changeDe;
/**
*
*/
private String vhctyAsortCode;
/**
*
*/
private String vhctyTyCode;
/**
*
*/
private String vhctySeCode;
/**
*
*/
private String mxmmLdg;
/**
*
*/
private String vhctyAsortNm;
/**
*
*/
private String vhctyTyNm;
/**
*
*/
private String vhctySeNm;
/**
*
*/
private String frstRegistDe;
/**
*
*/
private String fomNm;
/**
*
*/
private String acqsDe;
/**
*
*/
private String acqsEndDe;
/**
*
*/
private String yblMd;
/**
*
*/
private String transrRegistDe;
/**
*
*/
private String spcfRegistSttusCode;
/**
*
*/
private String colorNm;
/**
*
*/
private String mrtgCo;
/**
*
*/
private String seizrCo;
/**
*
*/
private String stmdCo;
/**
*
*/
private String nmplCsdyAt;
/**
*
*/
private String nmplCsdyRemnrDe;
/**
*
*/
private String originSeCode;
/**
*
*/
private String nmplStndrdCode;
/**
*
*/
private String acqsAmount;
/**
*
*/
private String insptValidPdBgnde;
/**
*
*/
private String insptValidPdEndde;
/**
*
*/
private String useStrnghldGrcCode;
/**
*
*/
private String tkcarPscapCo;
/**
*
*/
private String spmnno;
/**
*
*/
private String trvlDstnc;
/**
*
*/
private String frstRegistRqrcno;
/**
*
*/
private String vlntErsrPrvntcNticeDe;
/**
*
*/
private String registInsttNm;
/**
*
*/
private String processImprtyResnCode;
/**
*
*/
private String processImprtyResnDtls;
/**
*
*/
private String cbdLt;
/**
*
*/
private String cbdBt;
/**
*
*/
private String cbdHg;
/**
*
*/
private String frstMxmmLdg;
/**
*
*/
private String fuelCnsmpRt;
/**
*
*/
private String elctyCmpndFuelCnsmpRt;
/**
*
*/
private LocalDateTime regDt;
/**
*
*/
private String rgtr;
/**
*
*/
private String mberNm;
}

@ -1,73 +0,0 @@
package go.kr.project.api.model.response;
import go.kr.project.api.config.ApiConstant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* () VO, DB
*
* <p>tb_car_ledger_frmbk_dtl </p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VmisCarLedgerFrmbkDtlVO {
// ==== Static factory/mapping methods (moved from Service) ====
public static List<VmisCarLedgerFrmbkDtlVO> listFromResponse(LedgerResponse res, String masterId) {
List<VmisCarLedgerFrmbkDtlVO> list = new ArrayList<>();
if (res == null || res.getRecord() == null) return list;
for (LedgerResponse.Record r : res.getRecord()) {
VmisCarLedgerFrmbkDtlVO vo = VmisCarLedgerFrmbkDtlVO.builder()
.carLedgerFrmbkId(masterId) // 원부 마스터 ID (FK)
.mainchk(r.getMainchk()) // 주요 체크
.changeJobSeCode(r.getChangeJobSeCode()) // 변경 작업 구분 코드
.mainno(r.getMainno()) // 주번호
.subno(r.getSubno()) // 부번호
.dtls(r.getDtls()) // 상세 내역
.rqrcno(r.getRqrcno()) // 접수번호
.vhmno(r.getVhmno()) // 차량관리번호
.ledgerGroupNo(r.getLedgerGroupNo()) // 원부 그룹 번호
.ledgerIndvdlzNo(r.getLedgerIndvdlzNo()) // 원부 개별화 번호
.gubunNm(r.getGubunNm()) // 구분명
.changeDe(r.getChangeDe()) // 변경일자
.detailSn(r.getDetailSn()) // 상세 순번
.flag(r.getFlag()) // 플래그
.rgtr(ApiConstant.DEFAULT_REGISTRANT) // 등록자
.build();
list.add(vo);
}
return list;
}
// PK
private String carLedgerFrmbkDtlId;
// FK
private String carLedgerFrmbkId;
// 본문
private String mainchk;
private String changeJobSeCode;
private String mainno;
private String subno;
private String dtls;
private String rqrcno;
private String vhmno;
private String ledgerGroupNo;
private String ledgerIndvdlzNo;
private String gubunNm;
private String changeDe;
private String detailSn;
private String flag;
// 감사
private String rgtr;
}

@ -1,192 +0,0 @@
package go.kr.project.api.model.response;
import go.kr.project.api.config.ApiConstant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* () VO, DB
*
* <p>tb_car_ledger_frmbk </p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VmisCarLedgerFrmbkVO {
// ==== Static factory/mapping methods (moved from Service) ====
public static VmisCarLedgerFrmbkVO fromRequest(go.kr.project.api.model.request.LedgerRequest request) {
return VmisCarLedgerFrmbkVO.builder()
.infoSysId(request.getInfoSysId())
.infoSysIp(request.getInfoSysIp())
.sigunguCode(request.getSigunguCode())
.cntcInfoCode(request.getCntcInfoCode())
.chargerId(request.getChargerId())
.chargerIp(request.getChargerIp())
.chargerNm(request.getChargerNm())
.dmndVhrno(request.getVhrno())
.rgtr(ApiConstant.DEFAULT_REGISTRANT)
.build();
}
public static VmisCarLedgerFrmbkVO fromResponseMaster(String id, go.kr.project.api.model.response.LedgerResponse res) {
return VmisCarLedgerFrmbkVO.builder()
.carLedgerFrmbkId(id) // 등록원부 ID
.cntcResultCode(res.getCntcResultCode()) // 연계 결과 코드
.cntcResultDtls(res.getCntcResultDtls()) // 연계 결과 상세
.ledgerGroupNo(res.getLedgerGroupNo()) // 원부 그룹 번호
.ledgerIndvdlzNo(res.getLedgerIndvdlzNo()) // 원부 개별화 번호
.vhmno(res.getVhmno()) // 차량관리번호
.vhrno(res.getVhrno()) // 차량번호
.vin(res.getVin()) // 차대번호
.vhctyAsortCode(res.getVhctyAsortCode()) // 차종 종별 코드
.vhctyAsortNm(res.getVhctyAsortNm()) // 차종 종별명
.cnm(res.getCnm()) // 차명
.colorCode(res.getColorCode()) // 색상 코드
.colorNm(res.getColorNm()) // 색상명
.nmplStndrdCode(res.getNmplStndrdCode()) // 번호판 규격 코드
.nmplStndrdNm(res.getNmplStndrdNm()) // 번호판 규격명
.prposSeCode(res.getPrposSeCode()) // 용도 구분 코드
.prposSeNm(res.getPrposSeNm()) // 용도 구분명
.mtrsFomNm(res.getMtrsFomNm()) // 원동기 형식명
.fomNm(res.getFomNm()) // 형식명
.acqsAmount(res.getAcqsAmount()) // 취득 금액
.registDetailCode(res.getRegistDetailCode()) // 등록 상세 코드
.registDetailNm(res.getRegistDetailNm()) // 등록 상세명
.frstRegistDe(res.getFrstRegistDe()) // 최초 등록일
.caagEndde(res.getCaagEndde()) // 차령 만료일자
.prye(res.getPrye()) // 연식
.spmnno1(res.getSpmnno1()) // 제원관리번호1
.spmnno2(res.getSpmnno2()) // 제원관리번호2
.yblMd(res.getYblMd()) // 제작 년월일
.trvlDstnc(res.getTrvlDstnc()) // 주행거리
.insptValidPdBgnde(res.getInsptValidPdBgnde()) // 검사 유효 기간 시작일
.insptValidPdEndde(res.getInsptValidPdEndde()) // 검사 유효 기간 종료일
.chckValidPdBgnde(res.getChckValidPdBgnde()) // 점검 유효 기간 시작일
.chckValidPdEndde(res.getChckValidPdEndde()) // 점검 유효 기간 종료일
.registReqstSeNm(res.getRegistReqstSeNm()) // 등록 청구 구분명
.frstRegistRqrcno(res.getFrstRegistRqrcno()) // 최초 등록 접수번호
.nmplCsdyRemnrDe(res.getNmplCsdyRemnrDe()) // 번호판 영치 최고일
.nmplCsdyAt(res.getNmplCsdyAt()) // 번호판 영치 여부
.bssUsePd(res.getBssUsePd()) // 사업용 기간
.octhtErsrPrvntcNticeDe(res.getOcthtErsrPrvntcNticeDe()) // 직권말소 예고통지일
.ersrRegistDe(res.getErsrRegistDe()) // 말소 등록일
.ersrRegistSeCode(res.getErsrRegistSeCode()) // 말소 등록 구분 코드
.ersrRegistSeNm(res.getErsrRegistSeNm()) // 말소 등록 구분명
.mrtgcnt(res.getMrtgcnt()) // 저당건수
.vhclecnt(res.getVhclecnt()) // 압류건수
.stmdcnt(res.getStmdcnt()) // 구조변경건수
.adres1(res.getAdres1()) // 주소1
.adresNm1(res.getAdresNm1()) // 주소명1
.adres(res.getAdres()) // 주소
.adresNm(res.getAdresNm()) // 주소명
.indvdlBsnmAt(res.getIndvdlBsnmAt()) // 개인사업자 여부
.telno(res.getTelno()) // 전화번호
.mberNm(res.getMberNm()) // 소유자명
.mberSeCode(res.getMberSeCode()) // 소유자 구분 코드
.mberSeNo(res.getMberSeNo()) // 소유자 번호
.taxxmptTrgterSeCode(res.getTaxxmptTrgterSeCode()) // 면세 대상 구분 코드
.taxxmptTrgterSeCodeNm(res.getTaxxmptTrgterSeCodeNm()) // 면세 대상 구분명
.cntMatter(res.getCntMatter()) // 내용 사항
.emdNm(res.getEmdNm()) // 읍면동명
.prvntccnt(res.getPrvntccnt()) // 예방건수
.xportFlflAtSttemntDe(res.getXportFlflAtSttemntDe()) // 수출이행신고일
.partnRqrcno(res.getPartnRqrcno()) // 협력업체 접수번호
.build();
}
// PK
private String carLedgerFrmbkId;
// 요청(헤더/본문) 정보
private String infoSysId;
private String infoSysIp;
private String sigunguCode;
private String cntcInfoCode;
private String chargerId;
private String chargerIp;
private String chargerNm;
// 요청 본문
private String dmndVhrno;
private String dmndOnesInformationOpen;
private String dmndCpttrNm;
private String dmndCpttrIhidnum;
private String dmndCpttrLegaldongCode;
private String dmndRouteSeCode;
private String dmndDetailExpression;
private String dmndInqireSeCode;
// 응답 요약 정보
private String cntcResultCode;
private String cntcResultDtls;
// 응답 본문(마스터)
private String ledgerGroupNo;
private String ledgerIndvdlzNo;
private String vhmno;
private String vhrno;
private String vin;
private String vhctyAsortCode;
private String vhctyAsortNm;
private String cnm;
private String colorCode;
private String colorNm;
private String nmplStndrdCode;
private String nmplStndrdNm;
private String prposSeCode;
private String prposSeNm;
private String mtrsFomNm;
private String fomNm;
private String acqsAmount;
private String registDetailCode;
private String registDetailNm;
private String frstRegistDe;
private String caagEndde;
private String prye;
private String spmnno1;
private String spmnno2;
private String yblMd;
private String trvlDstnc;
private String insptValidPdBgnde;
private String insptValidPdEndde;
private String chckValidPdBgnde;
private String chckValidPdEndde;
private String registReqstSeNm;
private String frstRegistRqrcno;
private String nmplCsdyRemnrDe;
private String nmplCsdyAt;
private String bssUsePd;
private String octhtErsrPrvntcNticeDe;
private String ersrRegistDe;
private String ersrRegistSeCode;
private String ersrRegistSeNm;
private String mrtgcnt;
private String vhclecnt;
private String stmdcnt;
private String adres1;
private String adresNm1;
private String adres;
private String adresNm;
private String indvdlBsnmAt;
private String telno;
private String mberNm;
private String mberSeCode;
private String mberSeNo;
private String taxxmptTrgterSeCode;
private String taxxmptTrgterSeCodeNm;
private String cntMatter;
private String emdNm;
private String prvntccnt;
private String xportFlflAtSttemntDe;
private String partnRqrcno;
private String frstTrnsfrDe;
private String processImprtyResnCode;
private String processImprtyResnDtls;
// 감사
private String rgtr;
}

@ -0,0 +1,27 @@
package go.kr.project.api.service;
import go.kr.project.api.model.request.NewBasicRequest;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.NewBasicResponse;
import go.kr.project.api.model.response.NewLedgerResponse;
/**
* VMIS-interface API
* VMIS-interface API
*/
public interface ExternalVehicleApiService {
/**
* (YAML old/new )
* vmis.external.api.url.basic.old-or-new
* @param request ( old/new )
*/
NewBasicResponse getBasicInfo(NewBasicRequest request);
/**
* () (YAML old/new )
* vmis.external.api.url.ledger.old-or-new
* @param request ( old/new )
*/
NewLedgerResponse getLedgerInfo(NewLedgerRequest request);
}

@ -1,76 +0,0 @@
package go.kr.project.api.service;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
/**
*
*
* <p> :</p>
* <ul>
* <li>InternalVehicleInfoServiceImpl: VMIS (vmis.integration.mode=internal)</li>
* <li>ExternalVehicleInfoServiceImpl: REST API (vmis.integration.mode=external)</li>
* </ul>
*
* <h3> :</h3>
* <pre>
* # application.yml
* vmis:
* integration:
* mode: internal # external
* </pre>
*
* <h3> :</h3>
* <pre>
* {@code
* @Autowired
* private VehicleInfoService vehicleInfoService;
*
* // 단일 차량 조회
* VehicleApiResponseVO response = vehicleInfoService.getVehicleInfo("12가3456");
*
* // 여러 차량 일괄 조회
* List<VehicleApiResponseVO> responses = vehicleInfoService.getVehiclesInfo(
* Arrays.asList("12가3456", "34나5678")
* );
*
* // 단독 조회 (기본/등록원부)
* BasicResponse basic = vehicleInfoService.getBasicInfo("12가3456");
* LedgerResponse ledger = vehicleInfoService.getLedgerInfo("12가3456");
* }
* </pre>
*/
public interface VehicleInfoService {
/**
* ( )
*
* <p> .</p>
* <p> , , .</p>
*
* @param basicRequest (, , )
* @return ( + )
*/
VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest);
/**
* ()
* : , , BasicRequest
*
* @param request (, , )
* @return
*/
BasicResponse getBasicInfo(BasicRequest request);
/**
* () ()
* : , LedgerRequest
*
* @param request (, , )
* @return
*/
LedgerResponse getLedgerInfo(LedgerRequest request);
}

@ -1,6 +1,7 @@
package go.kr.project.api.service;
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.response.NewBasicResponse;
/**
*
@ -15,11 +16,18 @@ public interface VmisCarBassMatterInqireLogService {
* @param request
* @return ID
*/
String createInitialRequestNewTx(VmisCarBassMatterInqireVO request);
String createInitialRequestNewTx(CarBassMatterInqireVO request);
/**
* / . (REQUIRES_NEW)
* @param response
*/
void updateResponseNewTx(VmisCarBassMatterInqireVO response);
void updateResponseNewTx(CarBassMatterInqireVO response);
/**
* txId CAR_FFNLG_TRGT_ID . (REQUIRES_NEW, / )
* @param response API
* @param carFfnlgTrgtId ID ( )
*/
void updateCarFfnlgTrgtIdByTxIdNewTx(NewBasicResponse response, String carFfnlgTrgtId);
}

@ -1,7 +1,8 @@
package go.kr.project.api.service;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.api.model.response.NewLedgerResponse;
import java.util.List;
@ -16,18 +17,25 @@ public interface VmisCarLedgerFrmbkLogService {
* @param request
* @return ID
*/
String createInitialRequestNewTx(VmisCarLedgerFrmbkVO request);
String createInitialRequestNewTx(CarLedgerFrmbkVO request);
/**
* . (REQUIRES_NEW)
* @param response
*/
void updateResponseNewTx(VmisCarLedgerFrmbkVO response);
void updateResponseNewTx(CarLedgerFrmbkVO response);
/**
* . (REQUIRES_NEW)
* @param masterId ID
* @param details
*/
void saveDetailsNewTx(String masterId, List<VmisCarLedgerFrmbkDtlVO> details);
void saveDetailsNewTx(String masterId, List<CarLedgerFrmbkDtlVO> details);
/**
* txId CAR_FFNLG_TRGT_ID . (REQUIRES_NEW, / )
* @param response API
* @param carFfnlgTrgtId ID ( )
*/
void updateCarFfnlgTrgtIdByTxIdNewTx(NewLedgerResponse response, String carFfnlgTrgtId);
}

@ -0,0 +1,434 @@
package go.kr.project.api.service.impl;
import egovframework.exception.MessageException;
import go.kr.project.api.config.ApiConstant;
import go.kr.project.api.config.properties.VmisProperties;
import go.kr.project.api.model.*;
import go.kr.project.api.service.ExternalVehicleApiService;
import go.kr.project.api.util.ExceptionDetailUtil;
import go.kr.project.api.model.request.OldBasicRequest;
import go.kr.project.api.model.request.NewBasicRequest;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.*;
import go.kr.project.api.service.VmisCarBassMatterInqireLogService;
import go.kr.project.api.service.VmisCarLedgerFrmbkLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.stream.Collectors;
/**
* VMIS-interface API
* VMIS-interface API
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ExternalVehicleApiServiceImpl extends EgovAbstractServiceImpl implements ExternalVehicleApiService {
private final RestTemplate restTemplate;
private final VmisProperties vmisProperties;
private final VmisCarBassMatterInqireLogService bassMatterLogService;
private final VmisCarLedgerFrmbkLogService ledgerLogService;
/**
* (YAML old/new )
* vmis.external.api.url.basic.old-or-new
*/
@Override
public NewBasicResponse getBasicInfo(NewBasicRequest request) {
String apiVersion = vmisProperties.getExternal().getApi().getUrl().getBasic().getOldOrNew();
log.debug("[AUTO-BASIC] YAML 설정에 따른 API 버전: {}", apiVersion);
if ("old".equalsIgnoreCase(apiVersion)) {
return callOldBasicApi(request);
} else {
return callNewBasicApi(request);
}
}
/**
* () (YAML old/new )
* vmis.external.api.url.ledger.old-or-new
*/
@Override
public NewLedgerResponse getLedgerInfo(NewLedgerRequest request) {
String apiVersion = vmisProperties.getExternal().getApi().getUrl().getLedger().getOldOrNew();
log.debug("[AUTO-LEDGER] YAML 설정에 따른 API 버전: {}", apiVersion);
if ("old".equalsIgnoreCase(apiVersion)) {
return null;
} else {
return callNewLedgerApi(request);
}
}
/**
* API ()
*/
private NewBasicResponse callOldBasicApi(NewBasicRequest newRequest) {
log.debug("[OLD-BASIC] 자동차 기본정보 조회 API 호출");
String generatedId = null;
try {
// NewBasicRequest → OldBasicRequest 변환
OldBasicRequest oldRequest = convertToOldBasicRequest(newRequest);
CarBassMatterInqireVO logEntity = CarBassMatterInqireVO.fromOldRequest(oldRequest);
generatedId = bassMatterLogService.createInitialRequestNewTx(logEntity);
Envelope<OldBasicRequest> requestEnvelope = new Envelope<>(oldRequest);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Envelope<OldBasicRequest>> requestEntity = new HttpEntity<>(requestEnvelope, headers);
ResponseEntity<ApiExchangeEnvelope<OldBasicRequest, OldBasicResponse>> responseEntity = restTemplate.exchange(
vmisProperties.getExternal().getApi().getUrl().buildOldBasicUrl(),
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<ApiExchangeEnvelope<OldBasicRequest, OldBasicResponse>>() {}
);
if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) {
ApiExchangeEnvelope<OldBasicRequest, OldBasicResponse> env = responseEntity.getBody();
List<OldBasicResponse> data = (env.getResponse() != null) ? env.getResponse().getData() : null;
if (data != null && !data.isEmpty()) {
OldBasicResponse oldBody = data.get(0);
// 응답 정보 업데이트 (txId 포함)
CarBassMatterInqireVO responseUpdate = CarBassMatterInqireVO.fromOldExchange(generatedId, env);
bassMatterLogService.updateResponseNewTx(responseUpdate);
log.debug("[OLD-BASIC] txId: {}", env.getTxId());
// OldBasicResponse를 NewBasicResponse로 변환하여 반환
NewBasicResponse newResponse = convertToNewBasicResponse(oldBody);
// txId를 response에 설정
newResponse.setTxId(env.getTxId());
newResponse.setGeneratedId(generatedId);
return newResponse;
}
}
log.warn("[OLD-BASIC] 응답이 비어있음");
return null;
} catch (Exception e) {
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
CarBassMatterInqireVO errorLog = CarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId)
.linkRsltCd(ApiConstant.CNTC_RESULT_CODE_ERROR)
.linkRsltDtl(detail)
.build();
bassMatterLogService.updateResponseNewTx(errorLog);
log.error("[EXTERNAL-OLD-BASIC-ERR-LOG] 저장 완료 - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[EXTERNAL-OLD-BASIC-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
throw new MessageException("[OLD-BASIC] 차량 기본정보 조회 실패: " + e.getMessage(), e);
}
}
/**
* API ()
*/
private NewBasicResponse callNewBasicApi(NewBasicRequest request) {
log.debug("[NEW-BASIC] 자동차 기본정보 조회 API 호출");
String generatedId = null;
try {
CarBassMatterInqireVO logEntity = CarBassMatterInqireVO.fromNewRequest(request);
generatedId = bassMatterLogService.createInitialRequestNewTx(logEntity);
Envelope<NewBasicRequest> requestEnvelope = new Envelope<>(request);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Envelope<NewBasicRequest>> requestEntity = new HttpEntity<>(requestEnvelope, headers);
ResponseEntity<ApiExchangeEnvelope<NewBasicRequest, NewBasicResponse>> responseEntity = restTemplate.exchange(
vmisProperties.getExternal().getApi().getUrl().buildNewBasicUrl(),
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<ApiExchangeEnvelope<NewBasicRequest, NewBasicResponse>>() {}
);
if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) {
ApiExchangeEnvelope<NewBasicRequest, NewBasicResponse> env = responseEntity.getBody();
List<NewBasicResponse> data = (env.getResponse() != null) ? env.getResponse().getData() : null;
if (data != null && !data.isEmpty()) {
NewBasicResponse body = data.get(0);
// 응답 정보 업데이트 (txId 포함)
CarBassMatterInqireVO responseUpdate = CarBassMatterInqireVO.fromNewExchange(generatedId, env);
bassMatterLogService.updateResponseNewTx(responseUpdate);
// txId를 response에 설정
body.setTxId(env.getTxId());
body.setGeneratedId(generatedId);
log.debug("[NEW-BASIC] txId: {}", env.getTxId());
return body;
}
}
log.warn("[NEW-BASIC] 응답이 비어있음");
return null;
} catch (Exception e) {
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
CarBassMatterInqireVO errorLog = CarBassMatterInqireVO.builder()
.carBassMatterInqireId(generatedId)
.linkRsltCd(ApiConstant.CNTC_RESULT_CODE_ERROR)
.linkRsltDtl(detail)
.build();
bassMatterLogService.updateResponseNewTx(errorLog);
log.error("[EXTERNAL-NEW-BASIC-ERR-LOG] 저장 완료 - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[EXTERNAL-NEW-BASIC-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
throw new MessageException("[NEW-BASIC] 차량 기본정보 조회 실패: " + e.getMessage(), e);
}
}
/**
* API ()
*/
private NewLedgerResponse callNewLedgerApi(NewLedgerRequest request) {
log.debug("[NEW-LEDGER] 자동차 등록원부 조회 API 호출");
String generatedId = null;
try {
CarLedgerFrmbkVO init = CarLedgerFrmbkVO.fromNewRequest(request);
generatedId = ledgerLogService.createInitialRequestNewTx(init);
Envelope<NewLedgerRequest> requestEnvelope = new Envelope<>(request);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Envelope<NewLedgerRequest>> requestEntity = new HttpEntity<>(requestEnvelope, headers);
ResponseEntity<ApiExchangeEnvelope<NewLedgerRequest, NewLedgerResponse>> responseEntity = restTemplate.exchange(
vmisProperties.getExternal().getApi().getUrl().buildNewLedgerUrl(),
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<ApiExchangeEnvelope<NewLedgerRequest, NewLedgerResponse>>() {}
);
if (responseEntity.getStatusCode() == HttpStatus.OK && responseEntity.getBody() != null) {
ApiExchangeEnvelope<NewLedgerRequest, NewLedgerResponse> env = responseEntity.getBody();
List<NewLedgerResponse> data = (env.getResponse() != null) ? env.getResponse().getData() : null;
if (data != null && !data.isEmpty()) {
NewLedgerResponse body = data.get(0);
CarLedgerFrmbkVO masterUpdate = CarLedgerFrmbkVO.fromNewResponseMaster(generatedId, body);
masterUpdate.setTxId(env.getTxId());
ledgerLogService.updateResponseNewTx(masterUpdate);
List<CarLedgerFrmbkDtlVO> details = CarLedgerFrmbkDtlVO.listNewFromExchange(env, generatedId);
if (details != null && !details.isEmpty()) {
ledgerLogService.saveDetailsNewTx(generatedId, details);
}
// txId를 response에 설정
body.setTxId(env.getTxId());
body.setGeneratedId(generatedId);
log.debug("[NEW-LEDGER] txId: {}", env.getTxId());
return body;
}
}
log.warn("[NEW-LEDGER] 자동차 등록원부 조회 응답이 비어있음");
return null;
} catch (Exception e) {
if (generatedId != null) {
try {
String detail = ExceptionDetailUtil.buildForLog(e);
CarLedgerFrmbkVO errorLog = CarLedgerFrmbkVO.builder()
.carLedgerFrmbkId(generatedId)
.linkRsltCd(ApiConstant.CNTC_RESULT_CODE_ERROR)
.linkRsltDtl(detail)
.build();
ledgerLogService.updateResponseNewTx(errorLog);
log.error("[EXTERNAL-NEW-LEDGER-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
} catch (Exception ignore) {
log.error("[EXTERNAL-NEW-LEDGER-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
}
}
log.error("[NEW-LEDGER] 자동차 등록원부 조회 API 호출 실패", e);
throw new MessageException("[NEW-LEDGER] 자동차 등록원부 조회 실패: " + e.getMessage(), e);
}
}
/**
* NewBasicRequest OldBasicRequest
*/
private OldBasicRequest convertToOldBasicRequest(NewBasicRequest newRequest) {
OldBasicRequest oldRequest = new OldBasicRequest();
// 공통 메타 정보 복사 (필드명이 다름)
oldRequest.setInfoSysId(newRequest.getInfoSysId());
oldRequest.setInfoSysIp(newRequest.getInfoSysIpAddr());
oldRequest.setSigunguCode(newRequest.getSggCd());
oldRequest.setCntcInfoCode(newRequest.getLinkInfoCd());
oldRequest.setChargerId(newRequest.getPicId());
oldRequest.setChargerIp(newRequest.getPicIpAddr());
oldRequest.setChargerNm(newRequest.getPicNm());
// Record 변환
if (newRequest.getRecord() != null && !newRequest.getRecord().isEmpty()) {
List<OldBasicRequest.Record> oldRecords = newRequest.getRecord().stream()
.map(this::convertToOldRecord)
.collect(Collectors.toList());
oldRequest.setRecord(oldRecords);
}
return oldRequest;
}
/**
* NewBasicRequest.Record OldBasicRequest.Record
*/
private OldBasicRequest.Record convertToOldRecord(NewBasicRequest.Record newRecord) {
OldBasicRequest.Record oldRecord = new OldBasicRequest.Record();
oldRecord.setLevyStdde(newRecord.getLevyCrtrYmd());
oldRecord.setInqireSeCode(newRecord.getInqSeCd());
oldRecord.setVhrno(newRecord.getVhrno());
oldRecord.setVin(newRecord.getVin());
return oldRecord;
}
/**
* OldBasicResponse NewBasicResponse
*
* @param oldResponse API
* @return API
*/
private NewBasicResponse convertToNewBasicResponse(OldBasicResponse oldResponse) {
if (oldResponse == null) {
return null;
}
NewBasicResponse newResponse = new NewBasicResponse();
// 공통 응답 필드 변환 (필드명이 다름)
newResponse.setLinkRsltCd(oldResponse.getCntcResultCode());
newResponse.setLinkRsltDtl(oldResponse.getCntcResultDtls());
// Record 변환
if (oldResponse.getRecord() != null && !oldResponse.getRecord().isEmpty()) {
List<NewBasicResponse.Record> newRecords = oldResponse.getRecord().stream()
.map(this::convertOldRecordToNewRecord)
.collect(Collectors.toList());
newResponse.setRecord(newRecords);
}
return newResponse;
}
/**
* OldBasicResponse.Record NewBasicResponse.Record
*
* @param oldRecord API Record
* @return API Record
*/
private NewBasicResponse.Record convertOldRecordToNewRecord(OldBasicResponse.Record oldRecord) {
NewBasicResponse.Record newRecord = new NewBasicResponse.Record();
// 필드명 매핑 (구 → 신)
newRecord.setYridnw(oldRecord.getPrye());
newRecord.setRegYmd(oldRecord.getRegistDe());
newRecord.setErsrRegSeCd(oldRecord.getErsrRegistSeCode());
newRecord.setErsrRegSeNm(oldRecord.getErsrRegistSeNm());
newRecord.setErsrRegYmd(oldRecord.getErsrRegistDe());
newRecord.setRegDtlCd(oldRecord.getRegistDetailCode());
newRecord.setDsplvl(oldRecord.getDsplvl());
newRecord.setUsgsrhldStdgCd(oldRecord.getUseStrnghldLegaldongCode());
newRecord.setUsgsrhldDongCd(oldRecord.getUseStrnghldAdstrdCode());
newRecord.setUsgsrhldMtnYn(oldRecord.getUseStrnghldMntn());
newRecord.setUsgsrhldLnbr(oldRecord.getUseStrnghldLnbr());
newRecord.setUsgsrhldHo(oldRecord.getUseStrnghldHo());
newRecord.setUsgsrhldAddrNm(oldRecord.getUseStrnghldAdresNm());
newRecord.setUsgsrhldRoadNmCd(oldRecord.getUseStrnghldRoadNmCode());
newRecord.setUsgsrhldUdgdBldgSeCd(oldRecord.getUsgsrhldUndgrndBuldSeCode());
newRecord.setUsgsrhldBmno(oldRecord.getUseStrnghldBuldMainNo());
newRecord.setUsgsrhldBsno(oldRecord.getUseStrnghldBuldSubNo());
newRecord.setUsgsrhldWholAddr(oldRecord.getUsgsrhldAdresFull());
newRecord.setRprsOwnrMbrSeCd(oldRecord.getMberSeCode());
newRecord.setRprsOwnrNm(oldRecord.getMberNm());
newRecord.setRprsvOwnrIdecno(oldRecord.getMberSeNo());
newRecord.setRprsOwnrTelno(oldRecord.getTelno());
newRecord.setOwnrStdgCd(oldRecord.getOwnerLegaldongCode());
newRecord.setOwnrDongCd(oldRecord.getOwnerAdstrdCode());
newRecord.setOwnrMtnYn(oldRecord.getOwnerMntn());
newRecord.setOwnrLnbr(oldRecord.getOwnerLnbr());
newRecord.setOwnrHo(oldRecord.getOwnerHo());
newRecord.setOwnrAddrNm(oldRecord.getOwnerAdresNm());
newRecord.setOwnrRoadNmCd(oldRecord.getOwnerRoadNmCode());
newRecord.setOwnrUdgdBldgSeCd(oldRecord.getOwnerUndgrndBuldSeCode());
newRecord.setOwnrBmno(oldRecord.getOwnerBuldMainNo());
newRecord.setOwnrBsno(oldRecord.getOwnerBuldSubNo());
newRecord.setOwnrWholAddr(oldRecord.getOwnerAdresFull());
newRecord.setRearVhrno(oldRecord.getAftrVhrno());
newRecord.setUseFuelCd(oldRecord.getUseFuelCode());
newRecord.setUsgSeCd(oldRecord.getPrposSeCode());
newRecord.setMtrsFomNm(oldRecord.getMtrsFomNm());
newRecord.setBfrVhrno(oldRecord.getFrntVhrno());
newRecord.setVhrno(oldRecord.getVhrno());
newRecord.setVin(oldRecord.getVin());
newRecord.setAtmbNm(oldRecord.getCnm());
newRecord.setVhclTotlWt(oldRecord.getVhcleTotWt());
newRecord.setVeagEndYmd(oldRecord.getCaagEndde());
newRecord.setChgYmd(oldRecord.getChangeDe());
newRecord.setCarmdlAsortCd(oldRecord.getVhctyAsortCode());
newRecord.setCarmdlTypeCd(oldRecord.getVhctyTyCode());
newRecord.setCarmdlSeCd(oldRecord.getVhctySeCode());
newRecord.setMxmmLdg(oldRecord.getMxmmLdg());
newRecord.setCarmdlAsortNm(oldRecord.getVhctyAsortNm());
newRecord.setCarmdlTypeNm(oldRecord.getVhctyTyNm());
newRecord.setCarmdlClsfNm(oldRecord.getVhctySeNm());
newRecord.setFrstRegYmd(oldRecord.getFrstRegistDe());
newRecord.setFomNm(oldRecord.getFomNm());
newRecord.setAcqsYmd(oldRecord.getAcqsDe());
newRecord.setAcqsEndYmd(oldRecord.getAcqsEndDe());
newRecord.setFbctnYmd(oldRecord.getYblMd());
newRecord.setTransrRegYmd(oldRecord.getTransrRegistDe());
newRecord.setSpcfRegSttsCd(oldRecord.getSpcfRegistSttusCode());
newRecord.setColorNm(oldRecord.getColorNm());
newRecord.setMrtgCnt(oldRecord.getMrtgCo());
newRecord.setSzrCnt(oldRecord.getSeizrCo());
newRecord.setStrctChgCnt(oldRecord.getStmdCo());
newRecord.setNoplTcsdyYn(oldRecord.getNmplCsdyAt());
newRecord.setNoplTcsdyAvtsmtYmd(oldRecord.getNmplCsdyRemnrDe());
newRecord.setSrcSeCd(oldRecord.getOriginSeCode());
newRecord.setNoplTSpcfctCd(oldRecord.getNmplStndrdCode());
newRecord.setAcqsAmt(oldRecord.getAcqsAmount());
newRecord.setInspVldPdBgngYmd(oldRecord.getInsptValidPdBgnde());
newRecord.setInspVldPdEndYmd(oldRecord.getInsptValidPdEndde());
newRecord.setUsgsrhldGrcCd(oldRecord.getUseStrnghldGrcCode());
newRecord.setRdcpctCnt(oldRecord.getTkcarPscapCo());
newRecord.setSpmnno(oldRecord.getSpmnno());
newRecord.setDrvngDstnc(oldRecord.getTrvlDstnc());
newRecord.setFrstRegAplyRcptNo(oldRecord.getFrstRegistRqrcno());
newRecord.setVlntErsrPrvntcAvtsmtYmd(oldRecord.getVlntErsrPrvntcNticeDe());
newRecord.setOgnzNm(oldRecord.getRegistInsttNm());
newRecord.setPrcsImprtyRsnCd(oldRecord.getProcessImprtyResnCode());
newRecord.setPrcsImprtyRsnDtls(oldRecord.getProcessImprtyResnDtls());
newRecord.setCbdLt(oldRecord.getCbdLt());
newRecord.setCbdBt(oldRecord.getCbdBt());
newRecord.setCbdHg(oldRecord.getCbdHg());
newRecord.setFrstMxmmLdg(oldRecord.getFrstMxmmLdg());
newRecord.setFuelCnsmprt(oldRecord.getFuelCnsmpRt());
newRecord.setElctyCmpndFuelCnsmprt(oldRecord.getElctyCmpndFuelCnsmpRt());
return newRecord;
}
}

@ -1,8 +1,10 @@
package go.kr.project.api.service.impl;
import egovframework.exception.MessageException;
import egovframework.util.SessionUtil;
import go.kr.project.api.internal.mapper.VmisCarBassMatterInqireMapper;
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
import go.kr.project.api.mapper.VmisCarBassMatterInqireMapper;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.response.NewBasicResponse;
import go.kr.project.api.service.VmisCarBassMatterInqireLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -31,7 +33,7 @@ public class VmisCarBassMatterInqireLogServiceImpl extends EgovAbstractServiceIm
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String createInitialRequestNewTx(VmisCarBassMatterInqireVO request) {
public String createInitialRequestNewTx(CarBassMatterInqireVO request) {
String generatedId = carBassMatterInqireMapper.selectNextCarBassMatterInqireId();
request.setCarBassMatterInqireId(generatedId);
if( SessionUtil.getUserId() != null ){
@ -39,7 +41,7 @@ public class VmisCarBassMatterInqireLogServiceImpl extends EgovAbstractServiceIm
}
int result = carBassMatterInqireMapper.insertCarBassMatterInqire(request);
if (result != 1) {
throw new RuntimeException("자동차 기본 사항 조회 정보 등록 실패");
throw new MessageException("자동차 기본 사항 조회 정보 등록 실패");
}
log.info("[BASIC-REQ-LOG] 요청 정보 저장 완료(별도TX) - ID: {}, 차량번호: {}", generatedId, request.getDmndVhrno());
return generatedId;
@ -51,14 +53,34 @@ public class VmisCarBassMatterInqireLogServiceImpl extends EgovAbstractServiceIm
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateResponseNewTx(VmisCarBassMatterInqireVO response) {
public void updateResponseNewTx(CarBassMatterInqireVO response) {
if (response.getCarBassMatterInqireId() == null) {
throw new IllegalArgumentException("자동차 기본 사항 조회 ID는 필수입니다.");
}
int result = carBassMatterInqireMapper.updateCarBassMatterInqire(response);
if (result != 1) {
throw new RuntimeException("자동차 기본 사항 조회 정보 업데이트 실패 - ID: " + response.getCarBassMatterInqireId());
throw new MessageException("자동차 기본 사항 조회 정보 업데이트 실패 - ID: " + response.getCarBassMatterInqireId());
}
log.info("[BASIC-RES-LOG] 응답/에러 정보 저장 완료(별도TX) - ID: {}, 결과코드: {}", response.getCarBassMatterInqireId(), response.getCntcResultCode());
log.info("[BASIC-RES-LOG] 응답/에러 정보 저장 완료(별도TX) - ID: {}, 결과코드: {}", response.getCarBassMatterInqireId(), response.getLinkRsltCd());
}
/**
* txId CAR_FFNLG_TRGT_ID . (REQUIRES_NEW)
* @param response API
* @param carFfnlgTrgtId ID
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateCarFfnlgTrgtIdByTxIdNewTx(NewBasicResponse response, String carFfnlgTrgtId) {
try {
if (response != null && response.getTxId() != null) {
carBassMatterInqireMapper.updateCarFfnlgTrgtIdByTxId(response.getTxId(), carFfnlgTrgtId);
log.info("[BASIC-FFNLG-LOG] CAR_FFNLG_TRGT_ID 업데이트 완료(별도TX) - txId: {}, CAR_FFNLG_TRGT_ID: {}", response.getTxId(), carFfnlgTrgtId);
}
} catch (Exception e) {
log.error("[BASIC-FFNLG-LOG] CAR_FFNLG_TRGT_ID 업데이트 중 오류 발생 - txId: {}", response != null ? response.getTxId() : null, e);
// 저장 실패해도 비교 로직은 계속 진행
}
}
}

@ -1,9 +1,11 @@
package go.kr.project.api.service.impl;
import egovframework.exception.MessageException;
import egovframework.util.SessionUtil;
import go.kr.project.api.internal.mapper.VmisCarLedgerFrmbkMapper;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
import go.kr.project.api.mapper.VmisCarLedgerFrmbkMapper;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.api.model.response.NewLedgerResponse;
import go.kr.project.api.service.VmisCarLedgerFrmbkLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -27,7 +29,7 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String createInitialRequestNewTx(VmisCarLedgerFrmbkVO request) {
public String createInitialRequestNewTx(CarLedgerFrmbkVO request) {
String id = mapper.selectNextCarLedgerFrmbkId();
request.setCarLedgerFrmbkId(id);
if( SessionUtil.getUserId() != null ){
@ -35,7 +37,7 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im
}
int result = mapper.insertCarLedgerFrmbk(request);
if (result != 1) {
throw new RuntimeException("자동차 등록 원부(갑) 최초요청 등록 실패");
throw new MessageException("자동차 등록 원부(갑) 최초요청 등록 실패");
}
log.info("[LEDGER-REQ-LOG] 최초 요청 저장(별도TX) - ID: {}, 차량번호: {}", id, request.getDmndVhrno());
return id;
@ -43,23 +45,23 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateResponseNewTx(VmisCarLedgerFrmbkVO response) {
public void updateResponseNewTx(CarLedgerFrmbkVO response) {
if (response.getCarLedgerFrmbkId() == null) {
throw new IllegalArgumentException("자동차 등록 원부(갑) ID는 필수입니다.");
}
int updated = mapper.updateCarLedgerFrmbk(response);
if (updated != 1) {
throw new RuntimeException("자동차 등록 원부(갑) 정보 업데이트 실패 - ID: " + response.getCarLedgerFrmbkId());
throw new MessageException("자동차 등록 원부(갑) 정보 업데이트 실패 - ID: " + response.getCarLedgerFrmbkId());
}
log.info("[LEDGER-RES-LOG] 마스터 응답 업데이트(별도TX) - ID: {}, 결과코드: {}",
response.getCarLedgerFrmbkId(), response.getCntcResultCode());
response.getCarLedgerFrmbkId(), response.getLinkRsltCd());
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveDetailsNewTx(String masterId, List<VmisCarLedgerFrmbkDtlVO> details) {
public void saveDetailsNewTx(String masterId, List<CarLedgerFrmbkDtlVO> details) {
if (details == null || details.isEmpty()) return;
for (VmisCarLedgerFrmbkDtlVO dtl : details) {
for (CarLedgerFrmbkDtlVO dtl : details) {
String dtlId = mapper.selectNextCarLedgerFrmbkDtlId();
dtl.setCarLedgerFrmbkDtlId(dtlId);
dtl.setCarLedgerFrmbkId(masterId);
@ -67,4 +69,24 @@ public class VmisCarLedgerFrmbkLogServiceImpl extends EgovAbstractServiceImpl im
}
log.info("[LEDGER-RES-LOG] 상세 {}건 저장(별도TX) - ID: {}", details.size(), masterId);
}
/**
* txId CAR_FFNLG_TRGT_ID . (REQUIRES_NEW)
* @param response API
* @param carFfnlgTrgtId ID
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateCarFfnlgTrgtIdByTxIdNewTx(NewLedgerResponse response, String carFfnlgTrgtId) {
try {
if (response != null && response.getTxId() != null) {
mapper.updateCarFfnlgTrgtIdByTxId(response.getTxId(), carFfnlgTrgtId);
log.info("[LEDGER-FFNLG-LOG] CAR_FFNLG_TRGT_ID 업데이트 완료(별도TX) - txId: {}, CAR_FFNLG_TRGT_ID: {}", response.getTxId(), carFfnlgTrgtId);
}
} catch (Exception e) {
log.error("[LEDGER-FFNLG-LOG] CAR_FFNLG_TRGT_ID 업데이트 중 오류 발생 - txId: {}", response != null ? response.getTxId() : null, e);
// 저장 실패해도 비교 로직은 계속 진행
}
}
}

@ -1,4 +1,4 @@
package go.kr.project.api.internal.util;
package go.kr.project.api.util;
/**
* Common helper to extract root-cause message and truncate to DB column limit (default 4000 chars).

@ -2,12 +2,11 @@ package go.kr.project.carInspectionPenalty.callApi.controller;
import egovframework.constant.TilesConstants;
import egovframework.util.ApiResponseUtil;
import go.kr.project.api.model.VehicleApiResponseVO;
import go.kr.project.api.model.request.BasicRequest;
import go.kr.project.api.model.request.LedgerRequest;
import go.kr.project.api.model.response.BasicResponse;
import go.kr.project.api.model.response.LedgerResponse;
import go.kr.project.api.service.VehicleInfoService;
import go.kr.project.api.model.request.NewBasicRequest;
import go.kr.project.api.model.request.NewLedgerRequest;
import go.kr.project.api.model.response.NewBasicResponse;
import go.kr.project.api.model.response.NewLedgerResponse;
import go.kr.project.api.service.ExternalVehicleApiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -29,7 +28,7 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "차량 정보 조회", description = "차량 정보 조회 API")
public class VehicleInquiryController {
private final VehicleInfoService vehicleInfoService;
private final ExternalVehicleApiService service;
/**
*
@ -40,67 +39,47 @@ public class VehicleInquiryController {
return "carInspectionPenalty/callApi/inquiry" + TilesConstants.BASE;
}
/**
* ( + )
*
* @param request
* @return
*/
@PostMapping("/getIntegratedInfo.do")
@ResponseBody
@Operation(summary = "자동차 통합 조회", description = "차량 기본정보와 등록원부 정보를 함께 조회합니다.")
public ResponseEntity<?> getIntegratedInfo(@RequestBody BasicRequest request) {
log.info("========== 자동차 통합 조회 시작 ==========");
log.info("요청 차량번호: {}", request.getVhrno());
log.info("부과기준일: {}", request.getLevyStdde());
log.info("조회구분코드: {}", request.getInqireSeCode());
log.info("차대번호: {}", request.getVin());
// 입력값 검증
if (!StringUtils.hasText(request.getVhrno())) {
log.warn("차량번호가 입력되지 않았습니다.");
return ApiResponseUtil.error("차량번호를 입력해주세요.");
}
// 차량 정보 조회
VehicleApiResponseVO response = vehicleInfoService.getVehicleInfo(request);
if(!response.isSuccess()) {
log.warn("자동차 통합 조회 실패 - 차량번호: {}, 메시지: {}", request.getVhrno(), response.getMessage());
log.warn("========== 자동차 통합 조회 실패 ==========");
return ApiResponseUtil.error(response.getMessage());
}
log.info("자동차 통합 조회 성공 - 차량번호: {}", request.getVhrno());
log.info("========== 자동차 통합 조회 완료 ==========");
return ApiResponseUtil.success(response, "자동차 통합 조회가 완료되었습니다.");
}
/**
* ()
* YAML old/new API
*
* @param request
* @param request ( )
* @return
*/
@PostMapping("/getBasicInfo.do")
@ResponseBody
@Operation(summary = "자동차 기본사항 조회", description = "차량 기본정보만 조회합니다.")
public ResponseEntity<?> getBasicInfo(@RequestBody BasicRequest request) {
@Operation(summary = "자동차 기본사항 조회", description = "YAML 설정에 따라 차량 기본정보를 조회합니다.")
public ResponseEntity<?> getBasicInfo(@RequestBody NewBasicRequest request) {
log.info("========== 자동차 기본사항 조회 시작 ==========");
log.info("요청 차량번호: {}", request.getVhrno());
log.info("부과기준일: {}", request.getLevyStdde());
log.info("조회구분코드: {}", request.getInqireSeCode());
log.info("차대번호: {}", request.getVin());
// 입력값 검증
if (!StringUtils.hasText(request.getVhrno())) {
log.warn("차량번호가 입력되지 않았습니다.");
return ApiResponseUtil.error("차량번호를 입력해주세요.");
// 입력값 검증 - record 배열 확인
if (request.getRecord() == null || request.getRecord().isEmpty()) {
log.warn("조회 대상 record가 입력되지 않았습니다.");
return ApiResponseUtil.error("조회 대상 정보를 입력해주세요.");
}
NewBasicRequest.Record record = request.getRecord().get(0);
log.info("요청 차량번호: {}", record.getVhrno());
log.info("부과기준일: {}", record.getLevyCrtrYmd());
log.info("조회구분코드: {}", record.getInqSeCd());
log.info("차대번호: {}", record.getVin());
// 입력값 검증 - 부과기준일 필수
if (!StringUtils.hasText(record.getLevyCrtrYmd())) {
log.warn("부과기준일이 입력되지 않았습니다.");
return ApiResponseUtil.error("부과기준일을 입력해주세요.");
}
// 입력값 검증 - 차량번호 또는 차대번호 중 하나 필수
if (!StringUtils.hasText(record.getVhrno()) && !StringUtils.hasText(record.getVin())) {
log.warn("차량번호 또는 차대번호가 입력되지 않았습니다.");
return ApiResponseUtil.error("차량번호 또는 차대번호 중 하나를 입력해주세요.");
}
// 차량 기본정보 조회
BasicResponse response = vehicleInfoService.getBasicInfo(request);
// 차량 기본정보 조회 (YAML 설정에 따라 old/new 자동 선택)
NewBasicResponse response = service.getBasicInfo(request);
log.info("자동차 기본사항 조회 성공 - 차량번호: {}", request.getVhrno());
log.info("자동차 기본사항 조회 성공 - 차량번호: {}, 차대번호: {}", record.getVhrno(), record.getVin());
log.info("========== 자동차 기본사항 조회 완료 ==========");
return ApiResponseUtil.success(response, "자동차 기본사항 조회가 완료되었습니다.");
@ -108,27 +87,41 @@ public class VehicleInquiryController {
/**
* () ()
* YAML old/new API
*
* @param request
* @param request ( )
* @return
*/
@PostMapping("/getLedgerInfo.do")
@ResponseBody
@Operation(summary = "자동차 등록원부(갑) 조회", description = "차량 등록원부 정보만 조회합니다.")
public ResponseEntity<?> getLedgerInfo(@RequestBody LedgerRequest request) {
@Operation(summary = "자동차 등록원부(갑) 조회", description = "YAML 설정에 따라 차량 등록원부 정보를 조회합니다.")
public ResponseEntity<?> getLedgerInfo(@RequestBody NewLedgerRequest request) {
log.info("========== 자동차 등록원부(갑) 조회 시작 ==========");
log.info("요청 차량번호: {}", request.getVhrno());
log.info("조회구분코드: {}", request.getInqireSeCode());
log.info("민원인성명: {}", request.getCpttrNm());
log.info("조회구분코드: {}", request.getInqSeCd());
log.info("민원인성명: {}", request.getCvlprNm());
log.info("민원인주민번호: {}", request.getCvlprIdecno() != null ? "********" : null);
// 입력값 검증
// 입력값 검증 - 차량번호 필수
if (!StringUtils.hasText(request.getVhrno())) {
log.warn("차량번호가 입력되지 않았습니다.");
return ApiResponseUtil.error("차량번호를 입력해주세요.");
}
// 차량 등록원부 조회
LedgerResponse response = vehicleInfoService.getLedgerInfo(request);
// 입력값 검증 - 소유자명 필수
if (!StringUtils.hasText(request.getCvlprNm())) {
log.warn("소유자명이 입력되지 않았습니다.");
return ApiResponseUtil.error("소유자명을 입력해주세요.");
}
// 입력값 검증 - 주민번호 필수
if (!StringUtils.hasText(request.getCvlprIdecno())) {
log.warn("주민번호가 입력되지 않았습니다.");
return ApiResponseUtil.error("주민번호를 입력해주세요.");
}
// 차량 등록원부 조회 (YAML 설정에 따라 old/new 자동 선택)
NewLedgerResponse response = service.getLedgerInfo(request);
log.info("자동차 등록원부(갑) 조회 성공 - 차량번호: {}", request.getVhrno());
log.info("========== 자동차 등록원부(갑) 조회 완료 ==========");

@ -2,9 +2,9 @@ package go.kr.project.carInspectionPenalty.history.controller;
import egovframework.constant.TilesConstants;
import egovframework.util.ApiResponseUtil;
import go.kr.project.carInspectionPenalty.history.model.CarBassMatterInqireVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkDtlVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkVO;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.carInspectionPenalty.history.model.VehicleApiHistorySearchVO;
import go.kr.project.carInspectionPenalty.history.service.VehicleApiHistoryService;
import io.swagger.v3.oas.annotations.Operation;

@ -1,8 +1,8 @@
package go.kr.project.carInspectionPenalty.history.mapper;
import go.kr.project.carInspectionPenalty.history.model.CarBassMatterInqireVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkDtlVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkVO;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.carInspectionPenalty.history.model.VehicleApiHistorySearchVO;
import org.apache.ibatis.annotations.Mapper;

@ -1,331 +0,0 @@
package go.kr.project.carInspectionPenalty.history.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* packageName : go.kr.project.carInspectionPenalty.history.model
* fileName : CarBassMatterInqireVO
* author :
* date : 2025-11-06
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-11-06
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CarBassMatterInqireVO {
/** 자동차 기본 사항 조회 ID */
private String carBassMatterInqireId;
/** 정보 시스템 ID */
private String infoSysId;
/** 정보 시스템 IP */
private String infoSysIp;
/** 시군구 코드 */
private String sigunguCode;
/** 연계 정보 코드 */
private String cntcInfoCode;
/** 담당자 ID */
private String chargerId;
/** 담당자 IP */
private String chargerIp;
/** 담당자명 */
private String chargerNm;
/** 요청 부과 기준일 */
private String dmndLevyStdde;
/** 요청 조회 구분 코드 */
private String dmndInqireSeCode;
/** 요청 자동차등록번호 */
private String dmndVhrno;
/** 요청 차대번호 */
private String dmndVin;
/** 연계 결과 코드 */
private String cntcResultCode;
/** 연계 결과 상세 */
private String cntcResultDtls;
/** 연식 */
private String prye;
/** 등록일 */
private String registDe;
/** 말소 등록 구분 코드 */
private String ersrRegistSeCode;
/** 말소 등록 구분명 */
private String ersrRegistSeNm;
/** 말소 등록일 */
private String ersrRegistDe;
/** 등록 상세 코드 */
private String registDetailCode;
/** 배기량 */
private String dsplvl;
/** 사용 본거지 법정동 코드 */
private String useStrnghldLegaldongCode;
/** 사용 본거지 행정동 코드 */
private String useStrnghldAdstrdCode;
/** 사용 본거지 산 */
private String useStrnghldMntn;
/** 사용 본거지 번지 */
private String useStrnghldLnbr;
/** 사용 본거지 호 */
private String useStrnghldHo;
/** 사용 본거지 상세주소 */
private String useStrnghldAdresNm;
/** 사용 본거지 도로명 코드 */
private String useStrnghldRoadNmCode;
/** 사용 본거지 지하 건물 구분 코드 */
private String usgsrhldUndgrndBuldSeCode;
/** 사용 본거지 건물 주요 번호 */
private String useStrnghldBuldMainNo;
/** 사용 본거지 건물 부 번호 */
private String useStrnghldBuldSubNo;
/** 사용 본거지 전체주소 */
private String usgsrhldAdresFull;
/** 대표소유자 회원 구분 코드 */
private String mberSeCode;
/** 대표소유자 회원 번호 */
private String mberSeNo;
/** 대표소유자 전화번호 */
private String telno;
/** 소유자 법정동 코드 */
private String ownerLegaldongCode;
/** 소유자 행정동 코드 */
private String ownerAdstrdCode;
/** 소유자 산 */
private String ownerMntn;
/** 소유자 번지 */
private String ownerLnbr;
/** 소유자 호 */
private String ownerHo;
/** 소유자 상세주소 */
private String ownerAdresNm;
/** 소유자 도로명 코드 */
private String ownerRoadNmCode;
/** 소유자 지하건물 구분 코드 */
private String ownerUndgrndBuldSeCode;
/** 소유자 건물 주요 번호 */
private String ownerBuldMainNo;
/** 소유자 건물 부 번호 */
private String ownerBuldSubNo;
/** 소유자 전체주소 */
private String ownrWholaddr;
/** 신 차량번호 */
private String aftrVhrno;
/** 사용 연료 코드 */
private String useFuelCode;
/** 용도 구분 코드 */
private String prposSeCode;
/** 원동기 형식명 */
private String mtrsFomNm;
/** 이전 차량번호 */
private String frntVhrno;
/** 차량번호 */
private String vhrno;
/** 차대번호 */
private String vin;
/** 차명 */
private String cnm;
/** 차량 총 중량 */
private String vhcleTotWt;
/** 차령 만료일자 */
private String caagEndde;
/** 차번호 변경시기 */
private String changeDe;
/** 차종 종별 코드 */
private String vhctyAsortCode;
/** 차종 유형 코드 */
private String vhctyTyCode;
/** 차종 분류 코드 */
private String vhctySeCode;
/** 최대 적재량 */
private String mxmmLdg;
/** 차종 종별명 */
private String vhctyAsortNm;
/** 차종 유형명 */
private String vhctyTyNm;
/** 차종 분류명 */
private String vhctySeNm;
/** 최초 등록일 */
private String frstRegistDe;
/** 형식 */
private String fomNm;
/** 취득 일자 */
private String acqsDe;
/** 취득 종료일자 */
private String acqsEndDe;
/** 제작 년월일 */
private String yblMd;
/** 이전 등록일 */
private String transrRegistDe;
/** 제원 등록 상태 코드 */
private String spcfRegistSttusCode;
/** 색상명 */
private String colorNm;
/** 저당수 */
private String mrtgCo;
/** 압류건수 */
private String seizrCo;
/** 구조변경수 */
private String stmdCo;
/** 번호판 영치 여부 */
private String nmplCsdyAt;
/** 번호판 영치 최고일 */
private String nmplCsdyRemnrDe;
/** 출처 구분 코드 */
private String originSeCode;
/** 번호판 규격 코드 */
private String nmplStndrdCode;
/** 취득 금액 */
private String acqsAmount;
/** 검사 유효 기간 시작일 */
private String insptValidPdBgnde;
/** 검사 유효 기간 종료일 */
private String insptValidPdEndde;
/** 사용 본거지 관청 코드 */
private String useStrnghldGrcCode;
/** 승차정원수 */
private String tkcarPscapCo;
/** 제원관리번호 */
private String spmnno;
/** 주행거리 */
private String trvlDstnc;
/** 최초 등록 접수번호 */
private String frstRegistRqrcno;
/** 예고통지일 */
private String vlntErsrPrvntcNticeDe;
/** 등록 기관명 */
private String registInsttNm;
/** 처리 불가 사유 코드 */
private String processImprtyResnCode;
/** 처리 불가 사유 명세 */
private String processImprtyResnDtls;
/** 차체 길이 */
private String cbdLt;
/** 차체 너비 */
private String cbdBt;
/** 차체 높이 */
private String cbdHg;
/** 최초 최대 적재량 */
private String frstMxmmLdg;
/** 연료 소비율 */
private String fuelCnsmpRt;
/** 전기 복합 연료 소비율 */
private String elctyCmpndFuelCnsmpRt;
/** 등록 일시 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime regDt;
/** 등록자 */
private String rgtr;
/** 등록자명 */
private String rgtrNm;
/** 대표소유자 성명 */
private String mberNm;
}

@ -1,79 +0,0 @@
package go.kr.project.carInspectionPenalty.history.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* packageName : go.kr.project.carInspectionPenalty.history.model
* fileName : CarLedgerFrmbkDtlVO
* author :
* date : 2025-11-06
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-11-06
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CarLedgerFrmbkDtlVO {
/** 자동차 등록 원부 갑 상세 ID */
private String carLedgerFrmbkDtlId;
/** 자동차 등록 원부 갑 ID */
private String carLedgerFrmbkId;
/** 해제여부 */
private String mainchk;
/** 변경 업무 구분 코드 */
private String changeJobSeCode;
/** 주번호 */
private String mainno;
/** 부번호 */
private String subno;
/** 사항란 */
private String dtls;
/** 접수번호 */
private String rqrcno;
/** 차량관리번호 */
private String vhmno;
/** 원부 그룹 번호 */
private String ledgerGroupNo;
/** 원부 개별 번호 */
private String ledgerIndvdlzNo;
/** 변경 업무 구분명 */
private String gubunNm;
/** 변경 일자 */
private String changeDe;
/** 상세 순번 */
private String detailSn;
/** 표기여부 */
private String flag;
/** 등록 일시 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime regDt;
/** 등록자 */
private String rgtr;
}

@ -1,278 +0,0 @@
package go.kr.project.carInspectionPenalty.history.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
/**
* packageName : go.kr.project.carInspectionPenalty.history.model
* fileName : CarLedgerFrmbkVO
* author :
* date : 2025-11-06
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-11-06
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CarLedgerFrmbkVO {
/** 자동차 등록 원부 갑 ID */
private String carLedgerFrmbkId;
/** 정보 시스템 ID */
private String infoSysId;
/** 정보 시스템 IP */
private String infoSysIp;
/** 시군구 코드 */
private String sigunguCode;
/** 연계 정보 코드 */
private String cntcInfoCode;
/** 담당자 ID */
private String chargerId;
/** 담당자 IP */
private String chargerIp;
/** 담당자명 */
private String chargerNm;
/** 요청 자동차등록번호 */
private String dmndVhrno;
/** 요청 개인 정보 공개 */
private String dmndOnesInformationOpen;
/** 요청 민원인 성명 */
private String dmndCpttrNm;
/** 요청 민원인 주민번호 */
private String dmndCpttrIhidnum;
/** 요청 민원인 법정동 코드 */
private String dmndCpttrLegaldongCode;
/** 요청 경로 구분 코드 */
private String dmndRouteSeCode;
/** 요청 내역 표시 */
private String dmndDetailExpression;
/** 요청 조회 구분 코드 */
private String dmndInqireSeCode;
/** 연계 결과 코드 */
private String cntcResultCode;
/** 연계 결과 상세 */
private String cntcResultDtls;
/** 원부 그룹 번호 */
private String ledgerGroupNo;
/** 원부 개별 번호 */
private String ledgerIndvdlzNo;
/** 차량관리번호 */
private String vhmno;
/** 차량등록번호 */
private String vhrno;
/** 차대번호 */
private String vin;
/** 차종 종별 코드 */
private String vhctyAsortCode;
/** 차종 종별명 */
private String vhctyAsortNm;
/** 차명 */
private String cnm;
/** 색상 코드 */
private String colorCode;
/** 색상명 */
private String colorNm;
/** 번호판 규격 코드 */
private String nmplStndrdCode;
/** 번호판 규격명 */
private String nmplStndrdNm;
/** 용도 구분 코드 */
private String prposSeCode;
/** 용도 구분명 */
private String prposSeNm;
/** 원동기 형식명 */
private String mtrsFomNm;
/** 형식명 */
private String fomNm;
/** 취득 금액 */
private String acqsAmount;
/** 등록 상세 코드 */
private String registDetailCode;
/** 등록 상세명 */
private String registDetailNm;
/** 최초 등록일 */
private String frstRegistDe;
/** 차령 종료일 */
private String caagEndde;
/** 연식 */
private String prye;
/** 제원관리번호1 */
private String spmnno1;
/** 제원관리번호2 */
private String spmnno2;
/** 제작 년월일 */
private String yblMd;
/** 주행 거리 */
private String trvlDstnc;
/** 검사 유효 기간 시작일 */
private String insptValidPdBgnde;
/** 검사 유효 기간 종료일 */
private String insptValidPdEndde;
/** 점검 유효 기간 시작일 */
private String chckValidPdBgnde;
/** 점검 유효 기간 종료일 */
private String chckValidPdEndde;
/** 등록 신청 구분명 */
private String registReqstSeNm;
/** 최초 등록 접수번호 */
private String frstRegistRqrcno;
/** 번호판 영치 최고일 */
private String nmplCsdyRemnrDe;
/** 번호판 영치 여부 */
private String nmplCsdyAt;
/** 사업용 사용 기간 */
private String bssUsePd;
/** 직권 말소 예고 통지일 */
private String octhtErsrPrvntcNticeDe;
/** 말소 등록일 */
private String ersrRegistDe;
/** 말소 등록 구분 코드 */
private String ersrRegistSeCode;
/** 말소 등록 구분명 */
private String ersrRegistSeNm;
/** 저당수 */
private String mrtgcnt;
/** 압류건수 */
private String vhclecnt;
/** 구조변경수 */
private String stmdcnt;
/** 사용 본거지 주소 */
private String adres1;
/** 사용 본거지 주소상세 */
private String adresNm1;
/** 소유자 주소 */
private String adres;
/** 소유자 주소상세 */
private String adresNm;
/** 개인 사업자 여부 */
private String indvdlBsnmAt;
/** 대표소유자 전화번호 */
private String telno;
/** 대표소유자 성명 */
private String mberNm;
/** 대표소유자 회원 구분 코드 */
private String mberSeCode;
/** 대표소유자 회원 번호 */
private String mberSeNo;
/** 비과세 대상 구분 코드 */
private String taxxmptTrgterSeCode;
/** 비과세 대상 구분 코드명 */
private String taxxmptTrgterSeCodeNm;
/** 특기사항 건수 */
private String cntMatter;
/** 사용 본거지 행정동명 */
private String emdNm;
/** 예고수 */
private String prvntccnt;
/** 수출 이행 여부 신고일 */
private String xportFlflAtSttemntDe;
/** 발급번호 */
private String partnRqrcno;
/** 최초 양도일 */
private String frstTrnsfrDe;
/** 처리 불가 사유 코드 */
private String processImprtyResnCode;
/** 처리 불가 사유 명세 */
private String processImprtyResnDtls;
/** 등록 일시 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime regDt;
/** 등록자 */
private String rgtr;
/** 등록자명 */
private String rgtrNm;
/** 등록원부 변경 이력 */
private List<CarLedgerFrmbkDtlVO> record;
}

@ -22,7 +22,7 @@ import lombok.*;
@ToString
public class VehicleApiHistorySearchVO extends PagingVO {
/** 조회 API 구분 (basic: 기본정보, ledger: 등록원부, ledgerDtl: 등록원부상세) */
/** 조회 API 구분 (basic: 기본정보, ledger: 등록원부) */
private String apiType;
/** 조회 시작일 (yyyyMMdd) */
@ -32,17 +32,15 @@ public class VehicleApiHistorySearchVO extends PagingVO {
private String searchEndDate;
/** 차량번호 */
private String vhrno;
private String searchVhrno;
/** 담당자명 */
private String chargerNm;
/** 차대번호 */
private String searchVin;
/** 연계 결과 코드 */
private String cntcResultCode;
private String searchLinkRsltCd;
/** 차대번호 */
private String vin;
/** 대상자 ID */
private String searchCarFfnlgTrgtId;
/** 담당자 ID */
private String chargerId;
}

@ -1,8 +1,8 @@
package go.kr.project.carInspectionPenalty.history.service;
import go.kr.project.carInspectionPenalty.history.model.CarBassMatterInqireVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkDtlVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkVO;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.carInspectionPenalty.history.model.VehicleApiHistorySearchVO;
import java.util.List;

@ -1,9 +1,9 @@
package go.kr.project.carInspectionPenalty.history.service.impl;
import go.kr.project.api.model.CarBassMatterInqireVO;
import go.kr.project.api.model.CarLedgerFrmbkDtlVO;
import go.kr.project.api.model.CarLedgerFrmbkVO;
import go.kr.project.carInspectionPenalty.history.mapper.VehicleApiHistoryMapper;
import go.kr.project.carInspectionPenalty.history.model.CarBassMatterInqireVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkDtlVO;
import go.kr.project.carInspectionPenalty.history.model.CarLedgerFrmbkVO;
import go.kr.project.carInspectionPenalty.history.model.VehicleApiHistorySearchVO;
import go.kr.project.carInspectionPenalty.history.service.VehicleApiHistoryService;
import lombok.RequiredArgsConstructor;

@ -4,6 +4,10 @@ import egovframework.constant.MessageConstants;
import egovframework.constant.TilesConstants;
import egovframework.util.ApiResponseUtil;
import egovframework.util.SessionUtil;
import egovframework.util.excel.ExcelSheetData;
import egovframework.util.excel.SxssfExcelFile;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtExcelVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtModifiedDataVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
import go.kr.project.common.model.CmmnCodeSearchVO;
@ -20,35 +24,40 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Controller
* , TXT
* Controller
* , TXT
*/
@Controller
@RequestMapping("/carInspectionPenalty/registration")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "자동차 과태료 대상 등록", description = "자동차 과태료 대상 등록 및 목록 조회 API")
@Tag(name = "자동차 지연 과태료 대상 등록", description = "자동차 지연 과태료 대상 등록 및 목록 조회 API")
public class CarFfnlgTrgtController {
private final CarFfnlgTrgtService service;
private final CommonCodeService commonCodeService;
/**
*
*
* @param model
* @return
*/
@GetMapping("/list.do")
@Operation(summary = "과태료 대상 목록 화면", description = "과태료 대상 목록 조회 화면을 제공합니다.")
@Operation(summary = "지연 과태료 대상 목록 화면", description = "지연 과태료 대상 목록 조회 화면을 제공합니다.")
public String list(Model model) {
log.debug("과태료 대상 목록 화면 요청");
log.debug("지연 과태료 대상 목록 화면 요청");
// 업무 처리 상태 코드 조회 (공통코드)
CmmnCodeSearchVO taskPrcsSttsCdSearchVO = CmmnCodeSearchVO.builder()
@ -58,19 +67,28 @@ public class CarFfnlgTrgtController {
.sortAscending(true)
.build();
model.addAttribute("taskPrcsSttsCdList", commonCodeService.selectCodeDetailList(taskPrcsSttsCdSearchVO));
// 지연 과태료 대상 구분 코드 조회 (공통코드)
CmmnCodeSearchVO ffnlgTrgtSeCdSearchVO = CmmnCodeSearchVO.builder()
.searchCdGroupId("FFNLG_TRGT_SE_CD")
.searchUseYn("Y")
.sortColumn("SORT_ORDR")
.sortAscending(true)
.build();
model.addAttribute("ffnlgTrgtSeCdList", commonCodeService.selectCodeDetailList(ffnlgTrgtSeCdSearchVO));
return "carInspectionPenalty/registration/list" + TilesConstants.BASE;
}
/**
* AJAX
* AJAX
* @param paramVO
* @return
*/
@PostMapping("/list.ajax")
@Operation(summary = "과태료 대상 목록 조회", description = "과태료 대상 목록을 조회하고 JSON 형식으로 반환합니다.")
@Operation(summary = "지연 과태료 대상 목록 조회", description = "지연 과태료 대상 목록을 조회하고 JSON 형식으로 반환합니다.")
public ResponseEntity<?> listAjax(@ModelAttribute CarFfnlgTrgtVO paramVO) {
log.debug("과태료 대상 목록 조회 AJAX - 검색조건: {}", paramVO);
log.debug("지연 과태료 대상 목록 조회 AJAX - 검색조건: {}", paramVO);
// 1. 총 개수 조회
int totalCount = service.selectListTotalCount(paramVO);
@ -78,9 +96,6 @@ public class CarFfnlgTrgtController {
// 2. totalCount 설정
paramVO.setTotalCount(totalCount);
// 3. 페이징 활성화
paramVO.setPagingYn("Y");
// 목록 조회
List<CarFfnlgTrgtVO> list = service.selectList(paramVO);
@ -88,13 +103,13 @@ public class CarFfnlgTrgtController {
}
/**
* (EUC-KR )
* (EUC-KR )
* .
* - : EUC-KR ( 2)
* - // docs/-EUC-KR.txt
*/
@GetMapping("/download.do")
@Operation(summary = "과태료 대상 목록 다운로드", description = "EUC-KR 인코딩의 고정폭 텍스트로 목록을 샘플과 동일한 포맷으로 다운로드합니다.")
@Operation(summary = "지연 과태료 대상 목록 다운로드", description = "EUC-KR 인코딩의 고정폭 텍스트로 목록을 샘플과 동일한 포맷으로 다운로드합니다.")
public void download(
@ModelAttribute CarFfnlgTrgtVO paramVO,
HttpServletResponse response
@ -107,15 +122,20 @@ public class CarFfnlgTrgtController {
// 서비스에서 EUC-KR 텍스트 콘텐츠 생성
byte[] fileBytes = service.generateEucKrDownloadBytes(paramVO);
String fileName = URLEncoder.encode("유효기간경과_과태료부과대상_리스트.txt", "UTF-8");
// EUC-KR 바이트를 UTF-8 바이트로 변환 (다운로드 시에만)
String content = new String(fileBytes, "EUC-KR");
// TODO : ECU-KR -> UTF-8 변환
byte[] utfFileBytes = content.getBytes(StandardCharsets.UTF_8);
// 응답 헤더 설정 (텍스트 파일, EUC-KR 인코딩)
response.setContentType("text/plain; charset=EUC-KR");
String fileName = URLEncoder.encode("유효기간경과_과태료부과대상_리스트.prn", "UTF-8");
// 응답 헤더 설정 (텍스트 파일, UTF-8 인코딩)
response.setContentType("text/plain; charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentLength(fileBytes.length);
response.setContentLength(utfFileBytes.length);
// 바이트 스트림으로 전송 (본문은 EUC-KR 바이트)
response.getOutputStream().write(fileBytes);
// UTF-8 바이트 스트림으로 전송
response.getOutputStream().write(utfFileBytes);
response.getOutputStream().flush();
} catch (Exception e) {
log.error("목록 다운로드 중 오류", e);
@ -133,7 +153,7 @@ public class CarFfnlgTrgtController {
* @return
*/
@GetMapping("/uploadPopup.do")
@Operation(summary = "파일 업로드 팝업", description = "TXT 파일 업로드 팝업 화면을 제공합니다.")
@Operation(summary = "파일 업로드 팝업", description = "TXT, PRN 파일 업로드 팝업 화면을 제공합니다.")
public ModelAndView uploadPopup() {
log.debug("파일 업로드 팝업 화면 요청");
@ -151,11 +171,11 @@ public class CarFfnlgTrgtController {
* @return
*/
@PostMapping("/upload.ajax")
@Operation(summary = "TXT 파일 업로드", description = "TXT 파일을 업로드하고 파싱하여 DB에 저장합니다. 한 건이라도 실패 시 전체 롤백됩니다.")
@Operation(summary = "TXT, PRN 파일 업로드", description = "TXT, PRN 파일을 업로드하고 파싱하여 DB에 저장합니다. 한 건이라도 실패 시 전체 롤백됩니다.")
public ResponseEntity<?> upload(
@Parameter(description = "TXT 파일") @RequestParam("file") MultipartFile file) {
@Parameter(description = "TXT, PRN 파일") @RequestParam("file") MultipartFile file) {
log.info("TXT 파일 업로드 요청 - 파일명: {}", file != null ? file.getOriginalFilename() : "null");
log.info("TXT, PRN 파일 업로드 요청 - 파일명: {}", file != null ? file.getOriginalFilename() : "null");
try {
// 세션에서 사용자 ID 가져오기
@ -163,9 +183,12 @@ public class CarFfnlgTrgtController {
if (rgtr == null || rgtr.isEmpty()) {
return ApiResponseUtil.error("로그인 정보가 없습니다.");
}
// TODO : UTF-8 파일을 EUC-KR로 변환 (시스템은 EUC-KR 기준으로 처리)
MultipartFile convertedFile = convertUtf8ToEucKr(file);
// 파일 업로드 및 파싱 (한 건이라도 실패 시 전체 롤백)
Map<String, Object> result = service.uploadAndParseTxtFile(file, rgtr);
Map<String, Object> result = service.uploadAndParseTxtFile(convertedFile, rgtr);
boolean success = (boolean) result.get("success");
int successCount = (int) result.get("successCount");
@ -191,26 +214,26 @@ public class CarFfnlgTrgtController {
} catch (RuntimeException e) {
// 데이터 처리 중 오류 발생 - 전체 롤백됨
log.error("TXT 파일 업로드 중 오류 발생 - 전체 롤백", e);
log.error("TXT, PRN 파일 업로드 중 오류 발생 - 전체 롤백", e);
return ApiResponseUtil.error(e.getMessage());
} catch (Exception e) {
// 예상치 못한 오류
log.error("TXT 파일 업로드 중 예상치 못한 오류 발생", e);
log.error("TXT, PRN 파일 업로드 중 예상치 못한 오류 발생", e);
return ApiResponseUtil.error("파일 업로드 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
*
* @param carFfnlgTrgtId ID
*
* @param carFfnlgTrgtId ID
* @return
*/
@GetMapping("/selectOne.ajax")
@Operation(summary = "과태료 대상 상세 조회", description = "과태료 대상 상세 정보를 조회합니다.")
@Operation(summary = "지연 과태료 대상 상세 조회", description = "지연 과태료 대상 상세 정보를 조회합니다.")
public ResponseEntity<?> selectOne(
@Parameter(description = "과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
@Parameter(description = "지연 과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
log.debug("과태료 대상 상세 조회 - ID: {}", carFfnlgTrgtId);
log.debug("지연 과태료 대상 상세 조회 - ID: {}", carFfnlgTrgtId);
try {
CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
@ -225,22 +248,22 @@ public class CarFfnlgTrgtController {
}
} catch (Exception e) {
log.error("과태료 대상 상세 조회 중 오류 발생", e);
log.error("지연 과태료 대상 상세 조회 중 오류 발생", e);
return ApiResponseUtil.error("조회 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* ()
* @param carFfnlgTrgtId ID
* ()
* @param carFfnlgTrgtId ID
* @return
*/
@PostMapping("/delete.ajax")
@Operation(summary = "과태료 대상 삭제", description = "과태료 대상을 삭제(논리삭제)합니다.")
@Operation(summary = "지연 과태료 대상 삭제", description = "지연 과태료 대상을 삭제(논리삭제)합니다.")
public ResponseEntity<?> delete(
@Parameter(description = "과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
@Parameter(description = "지연 과태료 대상 ID") @RequestParam String carFfnlgTrgtId) {
log.info("과태료 대상 삭제 요청 - ID: {}", carFfnlgTrgtId);
log.info("지연 과태료 대상 삭제 요청 - ID: {}", carFfnlgTrgtId);
try {
String dltr = SessionUtil.getUserId();
@ -261,14 +284,14 @@ public class CarFfnlgTrgtController {
}
} catch (Exception e) {
log.error("과태료 대상 삭제 중 오류 발생", e);
log.error("지연 과태료 대상 삭제 중 오류 발생", e);
return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* API /
* @param targetList
* @param targetList
* @return
*/
@PostMapping("/compareWithApi.ajax")
@ -343,13 +366,13 @@ public class CarFfnlgTrgtController {
}
/**
*
* @param deleteIds ID
*
* @param deleteIds ID
* @return
*/
@PostMapping("/deleteBatch.ajax")
@ResponseBody
@Operation(summary = "과태료 대상 일괄 삭제", description = "선택된 과태료 대상 목록을 일괄 삭제합니다.")
@Operation(summary = "지연 과태료 대상 일괄 삭제", description = "선택된 지연 과태료 대상 목록을 일괄 삭제합니다.")
public ResponseEntity<?> deleteBatch(@RequestBody List<String> deleteIds) {
log.info("일괄 삭제 요청 - 선택된 데이터 건수: {}", deleteIds != null ? deleteIds.size() : 0);
@ -391,4 +414,151 @@ public class CarFfnlgTrgtController {
return ApiResponseUtil.error("삭제 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* AJAX
* , , .
*
* @param modifyData // VO
* @return / ResponseEntity
*/
@PostMapping("/saveAll.ajax")
@ResponseBody
@Operation(summary = "지연 과태료 대상 정보 일괄 저장", description = "생성, 수정, 삭제된 지연 과태료 대상 데이터를 일괄 처리합니다.")
public ResponseEntity<?> saveAllAjax(@RequestBody CarFfnlgTrgtModifiedDataVO modifyData) {
log.info("지연 과태료 대상 일괄 저장 요청 - 수정: {}건, 생성: {}건, 삭제: {}건",
modifyData.getUpdatedRows() != null ? modifyData.getUpdatedRows().size() : 0,
modifyData.getCreatedRows() != null ? modifyData.getCreatedRows().size() : 0,
modifyData.getDeletedRows() != null ? modifyData.getDeletedRows().size() : 0);
try {
int result = service.saveCarFfnlgTrgts(modifyData);
if (result > 0) {
return ApiResponseUtil.success("지연 과태료 대상 정보가 저장되었습니다.");
} else {
return ApiResponseUtil.error("저장할 데이터가 없습니다.");
}
} catch (Exception e) {
log.error("지연 과태료 대상 일괄 저장 중 오류 발생", e);
return ApiResponseUtil.error("저장 중 오류가 발생했습니다: " + e.getMessage());
}
}
/**
* UTF-8 MultipartFile EUC-KR
*
* @param utf8File UTF-8
* @return EUC-KR
* @throws IOException /
*/
private MultipartFile convertUtf8ToEucKr(MultipartFile utf8File) throws IOException {
// UTF-8로 파일 내용 읽기
String content = new String(utf8File.getBytes(), StandardCharsets.UTF_8);
// EUC-KR 바이트로 변환
byte[] eucKrBytes = content.getBytes("EUC-KR");
String eucKrContent = new String(eucKrBytes, "EUC-KR");
log.info("파일 인코딩 변환 - UTF-8({} bytes) → EUC-KR({} bytes)",
utf8File.getSize(), eucKrBytes.length);
// EUC-KR 바이트로 변환된 새로운 MultipartFile 반환
return new EucKrMultipartFile(
utf8File.getName(),
utf8File.getOriginalFilename(),
utf8File.getContentType(),
eucKrBytes
);
}
/**
* EUC-KR MultipartFile
*/
private static class EucKrMultipartFile implements MultipartFile {
private final String name;
private final String originalFilename;
private final String contentType;
private final byte[] bytes;
public EucKrMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) {
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.bytes = bytes;
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return bytes == null || bytes.length == 0;
}
@Override
public long getSize() {
return bytes.length;
}
@Override
public byte[] getBytes() {
return bytes;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
@Override
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) {
fos.write(bytes);
}
}
}
/**
*
*
* @param paramVO VO
* @param request HTTP
* @param response HTTP
*/
@PostMapping("/excel.do")
@Operation(summary = "지연 과태료 대상 목록 엑셀 다운로드", description = "지연 과태료 대상 목록을 엑셀 파일로 다운로드합니다.")
public void downloadExcel(
@ModelAttribute CarFfnlgTrgtVO paramVO,
HttpServletRequest request,
HttpServletResponse response) {
try {
log.debug("지연 과태료 대상 목록 엑셀 다운로드 요청");
// 페이징 처리 없이 전체 데이터 조회
paramVO.setPagingYn("N");
// 지연 과태료 대상 목록 조회
List<CarFfnlgTrgtExcelVO> excelList = service.selectListForExcel(paramVO);
// 엑셀 파일 생성 및 다운로드
String filename = "과태료대상목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
new SxssfExcelFile(ExcelSheetData.of(excelList, CarFfnlgTrgtExcelVO.class, "지연 과태료 대상 목록 " + excelList.size() + "건"), request, response, filename);
log.debug("지연 과태료 대상 목록 엑셀 다운로드 완료 - 파일명: {}, 건수: {}", filename, excelList.size());
} catch (Exception e) {
log.error("엑셀 다운로드 중 오류 발생", e);
}
}
}

@ -1,5 +1,6 @@
package go.kr.project.carInspectionPenalty.registration.mapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtExcelVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import org.apache.ibatis.annotations.Mapper;
@ -45,7 +46,14 @@ public interface CarFfnlgTrgtMapper {
* @return
*/
int update(CarFfnlgTrgtVO vo);
/**
*
* @param vo (carFfnlgTrgtId, taskPrcsSttsCd, rmrk)
* @return
*/
int updateTaskPrcsSttsCdAndRmrk(CarFfnlgTrgtVO vo);
/**
* ()
* @param vo (carFfnlgTrgtId, dltr)
@ -66,4 +74,11 @@ public interface CarFfnlgTrgtMapper {
* @return
*/
String selectSggNmBySggCd(String sggCd);
/**
*
* @param vo
* @return
*/
List<CarFfnlgTrgtExcelVO> selectListForExcel(CarFfnlgTrgtVO vo);
}

@ -0,0 +1,132 @@
package go.kr.project.carInspectionPenalty.registration.model;
import egovframework.util.excel.ExcelColumn;
import egovframework.util.excel.ExcelSheet;
import lombok.*;
/**
* VO
*
* <p> VO @ExcelColumn </p>
* <p> , </p>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@ExcelSheet(name = "과태료대상목록")
public class CarFfnlgTrgtExcelVO {
/** 접수일자 */
@ExcelColumn(headerName = "접수일자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String rcptYmd;
/** 검사소코드 */
@ExcelColumn(headerName = "검사소코드", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String inspstnCd;
/** 검사일자 */
@ExcelColumn(headerName = "검사일자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String inspYmd;
/** 차량번호 */
@ExcelColumn(headerName = "차량번호", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String vhclno;
/** 소유자명 */
@ExcelColumn(headerName = "소유자명", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String ownrNm;
/** 주민등록번호 */
@ExcelColumn(headerName = "주민등록번호", headerWidth = 20, align = ExcelColumn.Align.CENTER)
private String rrno;
/** 자동차명 */
@ExcelColumn(headerName = "자동차명", headerWidth = 20, align = ExcelColumn.Align.LEFT)
private String carNm;
/** 자동차종류 */
@ExcelColumn(headerName = "자동차종류", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String carKnd;
/** 자동차용도 */
@ExcelColumn(headerName = "자동차용도", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String carUsg;
/** 검사종료일자 */
@ExcelColumn(headerName = "검사종료일자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String inspEndYmd;
/** 일수 */
@ExcelColumn(headerName = "일수", headerWidth = 10, align = ExcelColumn.Align.RIGHT)
private Long daycnt;
/** 재검여부 */
@ExcelColumn(headerName = "재검여부", headerWidth = 12, align = ExcelColumn.Align.CENTER)
private String reinspYn;
/** 과태료금액 */
@ExcelColumn(headerName = "과태료금액", headerWidth = 15, align = ExcelColumn.Align.RIGHT)
private Long ffnlgAmt;
/** 최종등록일자 */
@ExcelColumn(headerName = "최종등록일자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String lastRegYmd;
/** 주소 */
@ExcelColumn(headerName = "주소", headerWidth = 40, align = ExcelColumn.Align.LEFT)
private String addr;
/** 유효기간만료일자 */
@ExcelColumn(headerName = "유효기간만료일자", headerWidth = 18, align = ExcelColumn.Align.CENTER)
private String vldPrdExpryYmd;
/** 매매상품 */
@ExcelColumn(headerName = "매매상품", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String trdGds;
/** 처리상태 */
@ExcelColumn(headerName = "처리상태", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String taskPrcsSttsCdNm;
/** 처리일자 */
@ExcelColumn(headerName = "처리일자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String taskPrcsYmd;
/** 비고 */
@ExcelColumn(headerName = "비고", headerWidth = 30, align = ExcelColumn.Align.LEFT)
private String rmrk;
/** 비고 상세 */
@ExcelColumn(headerName = "비고 상세", headerWidth = 50, align = ExcelColumn.Align.LEFT)
private String rmrkDtl;
/** 기본사항조회성명 */
@ExcelColumn(headerName = "기본사항조회성명", headerWidth = 18, align = ExcelColumn.Align.CENTER)
private String carBscMttrInqFlnm;
/** 기본사항조회시군구명 */
@ExcelColumn(headerName = "기본사항조회시군구명", headerWidth = 20, align = ExcelColumn.Align.CENTER)
private String carBscMttrInqSggNm;
/** 등록원부변경업무명 */
@ExcelColumn(headerName = "등록원부변경업무명", headerWidth = 20, align = ExcelColumn.Align.CENTER)
private String carRegFrmbkChgTaskSeNm;
/** 등록원부변경일자 */
@ExcelColumn(headerName = "등록원부변경일자", headerWidth = 18, align = ExcelColumn.Align.CENTER)
private String carRegFrmbkChgYmd;
/** 등록원부상세 */
@ExcelColumn(headerName = "등록원부상세", headerWidth = 40, align = ExcelColumn.Align.LEFT)
private String carRegFrmbkDtl;
/** 등록일시 */
@ExcelColumn(headerName = "등록일시", headerWidth = 20, align = ExcelColumn.Align.CENTER)
private String regDt;
/** 등록자 */
@ExcelColumn(headerName = "등록자", headerWidth = 15, align = ExcelColumn.Align.CENTER)
private String rgtrNm;
}

@ -0,0 +1,31 @@
package go.kr.project.carInspectionPenalty.registration.model;
import go.kr.project.common.model.PagingVO;
import lombok.*;
import java.util.List;
/**
* packageName : go.kr.project.carInspectionPenalty.registration.model
* fileName : CarFfnlgTrgtModifiedDataVO
* author :
* date : 2025-11-28
* description : VO
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-11-28
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CarFfnlgTrgtModifiedDataVO extends PagingVO {
private List<CarFfnlgTrgtVO> createdRows;
private List<CarFfnlgTrgtVO> updatedRows;
private List<CarFfnlgTrgtVO> deletedRows;
}

@ -6,6 +6,7 @@ import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
/**
* VO
@ -34,6 +35,7 @@ public class CarFfnlgTrgtVO extends PagingVO {
private String carUsg; // 자동차 용도
private String inspEndYmd; // 검사 종료 일자
private String daycnt; // 일수
private String reinspYn; // 재검 여부 (Y/N)
private String ffnlgAmt; // 과태료 금액
private String lastRegYmd; // 최종 등록 일자
private String addr; // 주소
@ -42,6 +44,7 @@ public class CarFfnlgTrgtVO extends PagingVO {
private String taskPrcsSttsCd; // 업무 처리 상태 코드 (01=접수, 02=처리중, 03=완료)
private String taskPrcsYmd; // 업무 처리 일자
private String rmrk; // 비고
private String rmrkDtl; // 비고 상세
private String carBassMatterInqireId; // 자동차 기본 사항 조회 ID
private String carLedgerFrmbkId; // 자동차 등록 원부 갑 ID
private String carBscMttrInqFlnm; // 자동차 기본 사항 조회 성명 (상품용일 때 저장)
@ -72,7 +75,9 @@ public class CarFfnlgTrgtVO extends PagingVO {
private String schRcptYmdEnd; // 검색 종료 접수 일자
private String schVhclno; // 검색 차량번호
private String schOwnrNm; // 검색 소유자명
private String schTaskPrcsSttsCd; // 검색 업무 처리 상태 코드
private List<String> schTaskPrcsSttsCd; // 검색 업무 처리 상태 코드 (다중 선택 가능)
private List<String> schReinspYn; // 검색 재검 여부 (다중 선택 가능)
private String schInspYmdStart; // 검색 시작 검사 일자
private String schInspYmdEnd; // 검색 종료 검사 일자
private String schFfnlgTrgtSeCd; // 검색 과태료 대상 구분 코드
}

@ -1,5 +1,7 @@
package go.kr.project.carInspectionPenalty.registration.service;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtExcelVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtModifiedDataVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import org.springframework.web.multipart.MultipartFile;
@ -77,4 +79,20 @@ public interface CarFfnlgTrgtService {
* @return (compareResults, totalCount, successCount, failCount)
*/
Map<String, Object> compareWithApi(List<Map<String, String>> targetList);
/**
*
* , , .
*
* @param modifyData // VO
* @return
*/
int saveCarFfnlgTrgts(CarFfnlgTrgtModifiedDataVO modifyData);
/**
*
* @param vo
* @return
*/
List<CarFfnlgTrgtExcelVO> selectListForExcel(CarFfnlgTrgtVO vo);
}

@ -1,12 +1,11 @@
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>
* <p> API .</p>
*/
public interface ComparisonService {
@ -17,16 +16,14 @@ public interface ComparisonService {
* <ol>
* <li> </li>
* <li> </li>
* <li> ( )</li>
* <li> ...</li>
* <li> ...</li>
* </ol>
*
* <p> .</p>
*
* @param existingData
* @param apiResponse API
* @param userId ID
* @return (02=, 03=, 04=, null=)
* @param userInfo
* @return (02=, 03=, null=)
*/
String executeComparison(CarFfnlgTrgtVO existingData, VehicleApiResponseVO apiResponse, String userId);
String executeComparison(CarFfnlgTrgtVO existingData, go.kr.project.login.model.LoginUserVO userInfo);
}

@ -2,11 +2,12 @@ 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;
import go.kr.project.api.service.VehicleInfoService;
import egovframework.util.SessionUtil;
import go.kr.project.api.service.ExternalVehicleApiService;
import go.kr.project.carInspectionPenalty.registration.config.CarFfnlgTxtParseConfig;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtExcelVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtModifiedDataVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
import go.kr.project.carInspectionPenalty.registration.service.ComparisonService;
@ -15,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
@ -26,6 +28,11 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Service
@ -37,13 +44,14 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
private final CarFfnlgTrgtMapper mapper;
private final CarFfnlgTxtParseConfig parseConfig;
private final VehicleInfoService vehicleInfoService;
private final ExternalVehicleApiService service;
private final ComparisonService comparisonService;
private final TransactionTemplate transactionTemplate;
// 날짜 형식 (YYYYMMDD)
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter DATE_FORMATTER_DASH = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public int selectListTotalCount(CarFfnlgTrgtVO vo) {
return mapper.selectListTotalCount(vo);
@ -105,8 +113,8 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 파일 확장자 검증
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".txt")) {
throw new IllegalArgumentException("TXT 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename);
if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".txt") && !originalFilename.toLowerCase().endsWith(".prn"))) {
throw new IllegalArgumentException("TXT, PRN 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename);
}
// 파일 크기 검증 (50MB 제한)
@ -155,7 +163,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
String errorMsg = String.format("[라인 %d] 데이터가 불완전합니다. 2줄 1세트 형식이 필요합니다.", i + 1);
errorMessages.add(errorMsg);
// 한 건이라도 실패하면 전체 롤백
throw new RuntimeException(buildErrorMessage(errorMessages));
throw new MessageException(buildErrorMessage(errorMessages));
}
String secondLine = allLines.get(i + 1);
@ -166,7 +174,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
if (vo == null) {
// 파싱 실패 시 전체 롤백
throw new RuntimeException(buildErrorMessage(errorMessages));
throw new MessageException(buildErrorMessage(errorMessages));
}
// 필수 필드 검증
@ -175,7 +183,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
if (!validationErrors.isEmpty()) {
errorMessages.addAll(validationErrors);
// 검증 실패 시 전체 롤백
throw new RuntimeException(buildErrorMessage(errorMessages));
throw new MessageException(buildErrorMessage(errorMessages));
}
// 차량번호 중복 체크
@ -188,13 +196,13 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
dataLineNumber, vo.getVhclno(), vo.getInspYmd());
errorMessages.add(errorMsg);
// 중복 체크 실패 시 전체 롤백
throw new RuntimeException(buildErrorMessage(errorMessages));
throw new MessageException(buildErrorMessage(errorMessages));
}
// 업무 처리 상태 및 등록자 설정
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.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
vo.setRcptYmd(LocalDate.now().format(DATE_FORMATTER_DASH)); // 접수일자는 현재 날짜
vo.setRgtr(rgtr);
// DB 저장
@ -207,7 +215,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
String errorMsg = String.format("[데이터 %d] 데이터 저장 실패 - 차량번호: %s", dataLineNumber, vo.getVhclno());
errorMessages.add(errorMsg);
// DB 저장 실패 시 전체 롤백
throw new RuntimeException(buildErrorMessage(errorMessages));
throw new MessageException(buildErrorMessage(errorMessages));
}
// 2줄 1세트이므로 다음 줄 건너뛰기
@ -229,7 +237,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
result.put("successCount", 0);
result.put("failCount", 0);
result.put("errorMessages", errorMessages);
} catch (RuntimeException e) {
} catch (MessageException e) {
// 데이터 처리 중 오류 발생 시 전체 롤백
log.error("TXT 파일 업로드 중 오류 발생 - 전체 롤백 처리", e);
throw e; // 트랜잭션 롤백을 위해 예외 재발생
@ -272,17 +280,34 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 3) 데이터 라인 생성 (각 항목 2줄)
for (CarFfnlgTrgtVO row : list) {
// 차량번호 별표 처리
String vhclno = nvl(row.getVhclno());
String vhclnoAsterisk = " ";
if (vhclno.startsWith("*")) {
vhclnoAsterisk = "*";
vhclno = vhclno.substring(1); // 별표 제거
}
// 최종등록일 별표 처리
String lastRegYmd = nvl(row.getLastRegYmd());
String lastRegYmdAsterisk = " ";
if (lastRegYmd.startsWith("*")) {
lastRegYmdAsterisk = "*";
lastRegYmd = lastRegYmd.substring(1); // 별표 제거
}
// 첫째줄: 고정폭 필드들 연결
String firstLine =
padRightBytes(nvl(row.getInspstnCd()), parseConfig.getFirstLineLength("inspstn-cd"), encoding) +
padRightBytes(formatYmd(row.getInspYmd(), true), parseConfig.getFirstLineLength("insp-ymd"), encoding) +
padRightBytes(nvl(row.getVhclno()), parseConfig.getFirstLineLength("vhclno"), encoding) +
padRightBytes(row.getInspYmd(), parseConfig.getFirstLineLength("insp-ymd"), encoding) +
padRightBytes(vhclnoAsterisk, parseConfig.getFirstLineLength("vhclno-asterisk"), encoding) +
padRightBytes(vhclno, parseConfig.getFirstLineLength("vhclno"), encoding) +
padRightBytes(nvl(row.getOwnrNm()), parseConfig.getFirstLineLength("ownr-nm"), encoding) +
padRightBytes(nvl(row.getRrno()), parseConfig.getFirstLineLength("rrno"), encoding) +
padRightBytes(nvl(row.getCarNm()), parseConfig.getFirstLineLength("car-nm"), encoding) +
padRightBytes(nvl(row.getCarKnd()), parseConfig.getFirstLineLength("car-knd"), encoding) +
padRightBytes(nvl(row.getCarUsg()), parseConfig.getFirstLineLength("car-usg"), encoding) +
padRightBytes(formatYmd(row.getInspEndYmd(), true), parseConfig.getFirstLineLength("insp-end-ymd"), encoding) +
padRightBytes(row.getInspEndYmd(), parseConfig.getFirstLineLength("insp-end-ymd"), encoding) +
padLeftBytes(nvl(row.getDaycnt()), parseConfig.getFirstLineLength("daycnt"), encoding) +
padLeftBytes(formatAmtToManWon(row.getFfnlgAmt()), parseConfig.getFirstLineLength("ffnlg-amt"), encoding);
@ -291,9 +316,10 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 둘째줄: skip + 나머지 필드
String secondLine =
padRightBytes("", parseConfig.getSecondLineLength("skip"), encoding) +
padRightBytes(formatYmd(row.getLastRegYmd(), true), parseConfig.getSecondLineLength("last-reg-ymd"), encoding) +
padRightBytes(lastRegYmdAsterisk, parseConfig.getSecondLineLength("last-reg-ymd-asterisk"), encoding) +
padRightBytes(lastRegYmd, parseConfig.getSecondLineLength("last-reg-ymd"), encoding) +
padRightBytes(nvl(row.getAddr()), parseConfig.getSecondLineLength("addr"), encoding) +
padRightBytes(formatYmd(row.getVldPrdExpryYmd(), true), parseConfig.getSecondLineLength("vld-prd-expry-ymd"), encoding) +
padRightBytes(row.getVldPrdExpryYmd(), parseConfig.getSecondLineLength("vld-prd-expry-ymd"), encoding) +
padRightBytes(nvl(row.getTrdGds()), parseConfig.getSecondLineLength("trd-gds"), encoding);
sb.append(secondLine).append("\r\n");
@ -302,7 +328,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
return sb.toString().getBytes(encoding);
} catch (Exception e) {
throw new RuntimeException("다운로드 파일 생성 중 오류: " + e.getMessage(), e);
throw new MessageException("다운로드 파일 생성 중 오류: " + e.getMessage(), e);
}
}
@ -459,12 +485,24 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
String inspYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 검사일자(inspYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspYmd);
pos += len;
// 차량번호 (13바이트)
// 차량번호 별표 공간 (1바이트)
len = parseConfig.getFirstLineLength("vhclno-asterisk");
String vhclnoAsterisk = extractByteLength(firstBytes, pos, len, encoding);
log.info("[DEBUG_LOG] 차량번호 별표(vhclnoAsterisk) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vhclnoAsterisk);
pos += len;
// 차량번호 (12바이트)
len = parseConfig.getFirstLineLength("vhclno");
String vhclno = extractByteLength(firstBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 차량번호(vhclno) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vhclno);
pos += len;
// 별표가 있으면 차량번호 앞에 붙임
if ("*".equals(vhclnoAsterisk.trim())) {
vhclno = "*" + vhclno;
log.info("[DEBUG_LOG] 차량번호에 별표 추가: [{}]", vhclno);
}
// 소유자명 (31바이트)
len = parseConfig.getFirstLineLength("ownr-nm");
@ -501,8 +539,8 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
String inspEndYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 종료일(inspEndYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspEndYmd);
pos += len;
// 일수 (8바이트)
// 일수 (별표 포함)
len = parseConfig.getFirstLineLength("daycnt");
String daycnt = extractByteLength(firstBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 일수(daycnt) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, daycnt);
@ -523,12 +561,24 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
len = parseConfig.getSecondLineLength("skip");
log.info("[DEBUG_LOG] 공백 스킵 [{}바이트]", len);
pos += len;
// 최종등록일 (12바이트)
// 최종등록일 별표 공간 (1바이트)
len = parseConfig.getSecondLineLength("last-reg-ymd-asterisk");
String lastRegYmdAsterisk = extractByteLength(secondBytes, pos, len, encoding);
log.info("[DEBUG_LOG] 최종등록일 별표(lastRegYmdAsterisk) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, lastRegYmdAsterisk);
pos += len;
// 최종등록일 (11바이트)
len = parseConfig.getSecondLineLength("last-reg-ymd");
String lastRegYmd = extractByteLength(secondBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 최종등록일(lastRegYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, lastRegYmd);
pos += len;
// 별표가 있으면 최종등록일 앞에 붙임
if ("*".equals(lastRegYmdAsterisk.trim())) {
lastRegYmd = "*" + lastRegYmd;
log.info("[DEBUG_LOG] 최종등록일에 별표 추가: [{}]", lastRegYmd);
}
// 주소 (88바이트)
len = parseConfig.getSecondLineLength("addr");
@ -547,11 +597,12 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
String trdGds = extractByteLength(secondBytes, pos, len, encoding).trim();
log.info("[DEBUG_LOG] 매매상품(trdGds) [{}바이트, 위치 {}~끝] = [{}]", len, pos, trdGds);
// 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
inspYmd = convertDateFormat(inspYmd);
inspEndYmd = convertDateFormat(inspEndYmd);
lastRegYmd = convertDateFormat(lastRegYmd);
vldPrdExpryYmd = convertDateFormat(vldPrdExpryYmd);
// 날짜에 '*' 특수문자가 붙는 경우때문에
// 날짜 형식 변환 하지않고 원본 그대로 저장 (YYYY-MM-DD -> YYYYMMDD)
//inspYmd = convertDateFormat(inspYmd);
//inspEndYmd = convertDateFormat(inspEndYmd);
//lastRegYmd = convertDateFormat(lastRegYmd);
//vldPrdExpryYmd = convertDateFormat(vldPrdExpryYmd);
log.info("[DEBUG_LOG] 날짜 변환 후 - inspYmd: [{}], inspEndYmd: [{}], lastRegYmd: [{}], vldPrdExpryYmd: [{}]",
inspYmd, inspEndYmd, lastRegYmd, vldPrdExpryYmd);
@ -559,7 +610,14 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 과태료 금액 숫자만 추출 (예: "30만원" -> "300000")
ffnlgAmt = extractNumericAmount(ffnlgAmt);
log.info("[DEBUG_LOG] 과태료 변환 후: [{}]", ffnlgAmt);
// 재검여부 설정 (일수에 '*'가 있으면 Y, 없으면 N)
String reinspYn = "N";
if (daycnt != null && daycnt.contains("*")) {
reinspYn = "Y";
}
log.info("[DEBUG_LOG] 재검여부(reinspYn): [{}] (일수: {})", reinspYn, daycnt);
// VO 설정
vo.setInspstnCd(inspstnCd);
vo.setInspYmd(inspYmd);
@ -571,6 +629,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
vo.setCarUsg(carUsg);
vo.setInspEndYmd(inspEndYmd);
vo.setDaycnt(daycnt);
vo.setReinspYn(reinspYn);
vo.setFfnlgAmt(ffnlgAmt);
vo.setLastRegYmd(lastRegYmd);
vo.setAddr(addr);
@ -686,10 +745,11 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
// 2. 검사일자 검증
if (vo.getInspYmd() == null || vo.getInspYmd().isEmpty()) {
errors.add(String.format("[데이터 %d] 검사일자가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
} else if (!isValidDate(vo.getInspYmd())) {
errors.add(String.format("[데이터 %d] 검사일자 형식이 올바르지 않습니다. 검사일자: %s (YYYYMMDD 형식이어야 함), 차량번호: %s",
dataLineNumber, vo.getInspYmd(), vhclno));
}
/*else if (!isValidDate(vo.getInspYmd())) {
errors.add(String.format("[데이터 %d] 검사일자 형식이 올바르지 않습니다. 검사일자: %s (YYYY-MM-DD 형식이어야 함), 차량번호: %s",
dataLineNumber, vo.getInspYmd(), vhclno));
}*/
// 3. 차량번호 검증
if (vo.getVhclno() == null || vo.getVhclno().isEmpty()) {
@ -702,12 +762,9 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
dataLineNumber, vo.getVhclno()));
}
// 4. 소유자명 검증
if (vo.getOwnrNm() == null || vo.getOwnrNm().isEmpty()) {
errors.add(String.format("[데이터 %d] 소유자명이 누락되었습니다. 차량번호: %s",
dataLineNumber, vhclno));
} else if (vo.getOwnrNm().length() > 75) {
errors.add(String.format("[데이터 %d] 소유자명이 너무 깁니다. 소유자명: %s (최대 75자), 차량번호: %s",
// 4. 소유자명 검증 (소유자명에 null or 공백 들어올수 있음 - 필수 아님)
if (vo.getOwnrNm() != null && !vo.getOwnrNm().isEmpty() && vo.getOwnrNm().length() > 75) {
errors.add(String.format("[데이터 %d] 소유자명이 너무 깁니다. 소유자명: %s (최대 75자), 차량번호: %s",
dataLineNumber, vo.getOwnrNm(), vhclno));
}
@ -756,26 +813,32 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
dataLineNumber, vo.getAddr().length(), vhclno));
}
// 8. 선택 필드 날짜 검증 (값이 있는 경우에만)
if (vo.getInspEndYmd() != null && !vo.getInspEndYmd().isEmpty() && !isValidDate(vo.getInspEndYmd())) {
errors.add(String.format("[데이터 %d] 검사종료일자 형식이 올바르지 않습니다. 검사종료일자: %s (YYYYMMDD 형식), 차량번호: %s",
dataLineNumber, vo.getInspEndYmd(), vhclno));
}
if (vo.getLastRegYmd() != null && !vo.getLastRegYmd().isEmpty() && !isValidDate(vo.getLastRegYmd())) {
errors.add(String.format("[데이터 %d] 최종등록일자 형식이 올바르지 않습니다. 최종등록일자: %s (YYYYMMDD 형식), 차량번호: %s",
dataLineNumber, vo.getLastRegYmd(), vhclno));
}
if (vo.getVldPrdExpryYmd() != null && !vo.getVldPrdExpryYmd().isEmpty() && !isValidDate(vo.getVldPrdExpryYmd())) {
errors.add(String.format("[데이터 %d] 유효기간만료일자 형식이 올바르지 않습니다. 유효기간만료일자: %s (YYYYMMDD 형식), 차량번호: %s",
dataLineNumber, vo.getVldPrdExpryYmd(), vhclno));
}
// 8. 선택 필드 날짜 검증 (값이 있는 경우에만), '*' 특수문자 들어간 경우때문에 일자형식 틀어짐, 제거
//if (vo.getInspEndYmd() != null && !vo.getInspEndYmd().isEmpty() && !isValidDate(vo.getInspEndYmd())) {
// errors.add(String.format("[데이터 %d] 검사종료일자 형식이 올바르지 않습니다. 검사종료일자: %s (YYYYMMDD 형식), 차량번호: %s",
// dataLineNumber, vo.getInspEndYmd(), vhclno));
//}
//
//if (vo.getLastRegYmd() != null && !vo.getLastRegYmd().isEmpty() && !isValidDate(vo.getLastRegYmd())) {
// errors.add(String.format("[데이터 %d] 최종등록일자 형식이 올바르지 않습니다. 최종등록일자: %s (YYYYMMDD 형식), 차량번호: %s",
// dataLineNumber, vo.getLastRegYmd(), vhclno));
//}
//
//if (vo.getVldPrdExpryYmd() != null && !vo.getVldPrdExpryYmd().isEmpty() && !isValidDate(vo.getVldPrdExpryYmd())) {
// errors.add(String.format("[데이터 %d] 유효기간만료일자 형식이 올바르지 않습니다. 유효기간만료일자: %s (YYYYMMDD 형식), 차량번호: %s",
// dataLineNumber, vo.getVldPrdExpryYmd(), vhclno));
//}
// 9. 일수 검증 (값이 있는 경우에만)
if (vo.getDaycnt() != null && !vo.getDaycnt().isEmpty() && !isNumeric(vo.getDaycnt())) {
errors.add(String.format("[데이터 %d] 일수는 숫자여야 합니다. 일수: %s, 차량번호: %s",
dataLineNumber, vo.getDaycnt(), vhclno));
// 일수에 '*' 특수문자가 포함될 수 있음 (재검여부 표시용)
if (vo.getDaycnt() != null && !vo.getDaycnt().isEmpty()) {
String daycnt = vo.getDaycnt().trim();
// '*' 제거 후 숫자 검증
String daycntWithoutAsterisk = daycnt.replace("*", "").trim();
if (!daycntWithoutAsterisk.isEmpty() && !isNumeric(daycntWithoutAsterisk)) {
errors.add(String.format("[데이터 %d] 일수는 숫자 또는 '*숫자' 형식이어야 합니다. 일수: %s, 차량번호: %s",
dataLineNumber, vo.getDaycnt(), vhclno));
}
}
return errors;
@ -820,11 +883,12 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
/**
*
*
*
* :
* - : 123456, 1234567 (2~3 + + 4 )
* - : 112222 ( + + + )
*
* - '*' ( )
*
* @param vhclno
* @return
*/
@ -832,36 +896,44 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
if (vhclno == null || vhclno.isEmpty()) {
return false;
}
// 공백 제거
vhclno = vhclno.trim();
// 최소 길이 체크 (예: 12가3456 = 7자)
if (vhclno.length() < 7) {
// '*' 특수문자를 임시로 제거하여 검증 (전출차량 등 특정 상황 표시용)
String vhclnoWithoutAsterisk = vhclno.replace("*", "");
// '*'만 있는 경우는 제외
if (vhclnoWithoutAsterisk.isEmpty()) {
return false;
}
// 최소 길이 체크 (예: 12가3456 = 7자, *12가3456 = 8자)
if (vhclnoWithoutAsterisk.length() < 7) {
return false;
}
// 패턴1: 일반 차량번호 (2~3자리 숫자 + 한글 1자 + 4자리 숫자)
// 예: 12가3456, 123가4567
if (vhclno.matches("\\d{2,3}[가-힣]\\d{4}")) {
// 예: 12가3456, 123가4567, *12가3456
if (vhclnoWithoutAsterisk.matches("\\d{2,3}[가-힣]\\d{4}")) {
return true;
}
// 패턴2: 지역명이 포함된 차량번호 (한글 + 숫자 + 한글 + 숫자)
// 예: 경기11사2222, 서울12가3456
if (vhclno.matches("[가-힣]+\\d+[가-힣]\\d+")) {
// 예: 경기11사2222, 서울12가3456, *경기11사2222
if (vhclnoWithoutAsterisk.matches("[가-힣]+\\d+[가-힣]\\d+")) {
return true;
}
// 패턴3: 특수 차량번호 (숫자 + 한글 + 숫자)
// 예: 22고2222, 33마3333
if (vhclno.matches("\\d+[가-힣]+\\d+")) {
// 예: 22고2222, 33마3333, *22고2222
if (vhclnoWithoutAsterisk.matches("\\d+[가-힣]+\\d+")) {
return true;
}
// 그 외의 경우는 일단 허용 (실제 차량번호 형식이 다양할 수 있음)
// 최소한 한글과 숫자가 모두 포함되어 있는지 확인
return vhclno.matches(".*[가-힣].*") && vhclno.matches(".*\\d.*");
return vhclnoWithoutAsterisk.matches(".*[가-힣].*") && vhclnoWithoutAsterisk.matches(".*\\d.*");
}
/**
@ -899,140 +971,259 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
}
/**
* API /
* API / ()
* : , .
* , .
*
* @param targetList
* @return
*/
@Override
@Transactional
public Map<String, Object> compareWithApi(List<Map<String, String>> targetList) {
log.info("========== API 호출 및 비교 시작 ==========");
log.info("========== API 호출 및 비교 시작 (병렬처리) ==========");
log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);
if (targetList == null || targetList.isEmpty()) {
throw new IllegalArgumentException("선택된 데이터가 없습니다.");
}
List<Map<String, Object>> compareResults = new ArrayList<>();
int successCount = 0;
int failCount = 0;
int productUseCount = 0; // 상품용 건수
int transferCount = 0; // 이첩 건수
int normalCount = 0; // 정상 처리 건수
for (Map<String, String> target : targetList) {
String carFfnlgTrgtId = target.get("carFfnlgTrgtId");
String vhclno = target.get("vhclno");
String inspYmd = target.get("inspYmd");
String rgtr = target.get("rgtr"); // 등록자 (사용자 ID)
log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtId", carFfnlgTrgtId);
compareResult.put("vhclno", vhclno);
// 세션에서 사용자 정보 조회 (병렬처리 전에 메인 스레드에서 조회)
go.kr.project.login.model.LoginUserVO userInfo = null;
try {
userInfo = SessionUtil.getLoginUser();
} catch (Exception e) {
log.warn("세션에서 사용자 정보 조회 실패", e);
}
final go.kr.project.login.model.LoginUserVO finalUserInfo = userInfo;
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtVO existingData = new CarFfnlgTrgtVO();
existingData.setCarFfnlgTrgtId(carFfnlgTrgtId);
existingData = mapper.selectOne(existingData);
if (existingData == null) {
compareResult.put("success", false);
compareResult.put("message", "기존 데이터를 찾을 수 없습니다.");
failCount++;
compareResults.add(compareResult);
continue;
}
// I/O 작업이므로 CPU 코어 수의 2배로 스레드 풀 생성
int threadPoolSize = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
// 2. API 호출 (통합 조회)
BasicRequest apiRequest = new BasicRequest();
apiRequest.setVhrno(vhclno);
// TODO : 기본적으로 검사일 기준으로 api 호출
apiRequest.setLevyStdde(inspYmd != null ? inspYmd.replace("-", "") : "");
//apiRequest.setLevyStdde(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
log.info("병렬처리 스레드 풀 크기: {}", threadPoolSize);
VehicleApiResponseVO apiResponse = vehicleInfoService.getVehicleInfo(apiRequest);
try {
// 병렬로 각 건 처리
List<CompletableFuture<Map<String, Object>>> futures = targetList.stream()
.map(target -> CompletableFuture.supplyAsync(() -> processOneTarget(target, finalUserInfo), executor))
.collect(Collectors.toList());
if (!apiResponse.isSuccess()) {
compareResult.put("success", false);
compareResult.put("message", "API 호출 실패: " + apiResponse.getMessage());
failCount++;
compareResults.add(compareResult);
continue;
}
// 모든 작업 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 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());
}
// 결과 수집
List<Map<String, Object>> compareResults = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 3. 비교 로직 실행
String statusCode = comparisonService.executeComparison(existingData, apiResponse, rgtr);
// 통계 집계
int successCount = 0;
int failCount = 0;
int productUseCount = 0;
int transferCount = 0;
int normalCount = 0;
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
for (Map<String, Object> result : compareResults) {
Boolean success = (Boolean) result.get("success");
if (Boolean.TRUE.equals(success)) {
successCount++;
String processStatus = (String) result.get("processStatus");
if ("상품용".equals(processStatus)) {
productUseCount++;
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
} else if ("이첩".equals(processStatus)) {
transferCount++;
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
} else if ("내사종결".equals(processStatus) || "정상".equals(processStatus)) {
normalCount++;
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
} else {
normalCount++;
compareResult.put("processStatus", "기타");
compareResult.put("message", "기타 상태로 처리되었습니다.");
}
compareResult.put("success", true);
successCount++;
} else {
// 정상 처리 (비교 로직에 해당 안됨)
normalCount++;
compareResult.put("success", true);
compareResult.put("message", "정상 처리되었습니다.");
compareResult.put("processStatus", "정상");
successCount++;
failCount++;
}
}
Map<String, Object> resultData = new HashMap<>();
resultData.put("compareResults", compareResults);
resultData.put("totalCount", targetList.size());
resultData.put("successCount", successCount);
resultData.put("failCount", failCount);
resultData.put("productUseCount", productUseCount);
resultData.put("transferCount", transferCount);
resultData.put("normalCount", normalCount);
log.info("========== API 호출 및 비교 완료 (병렬처리) ==========");
log.info("전체: {}건, 성공: {}건, 실패: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
targetList.size(), successCount, failCount, productUseCount, transferCount, normalCount);
return resultData;
} catch (Exception e) {
log.error("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno, e);
throw new MessageException(String.format("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno), e);
/*
compareResult.put("success", false);
compareResult.put("message", "비교 중 오류: " + e.getMessage());
failCount++;
*/
} finally {
// ExecutorService 종료
executor.shutdown();
try {
if (!executor.awaitTermination(120, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
compareResults.add(compareResult);
/**
* ( )
*
* @param target
* @return
*/
private Map<String, Object> processOneTarget(Map<String, String> target, go.kr.project.login.model.LoginUserVO userInfo) {
String carFfnlgTrgtId = target.get("carFfnlgTrgtId");
String vhclno = target.get("vhclno");
String inspYmd = target.get("inspYmd");
log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtId", carFfnlgTrgtId);
compareResult.put("vhclno", vhclno);
try {
// 개별 트랜잭션으로 실행
String statusCode = transactionTemplate.execute(status -> {
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtVO existingData = new CarFfnlgTrgtVO();
existingData.setCarFfnlgTrgtId(carFfnlgTrgtId);
existingData = mapper.selectOne(existingData);
if (existingData == null) {
String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno);
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
if (!TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT.equals(existingData.getTaskPrcsSttsCd())) {
String errorMsg = String.format("접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s",
vhclno, existingData.getTaskPrcsSttsCd());
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 3. 비교 로직 실행
return comparisonService.executeComparison(existingData, userInfo);
} catch (Exception e) {
// 트랜잭션 롤백
status.setRollbackOnly();
throw e;
}
});
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
} else {
compareResult.put("processStatus", "기타");
compareResult.put("message", "기타 상태로 처리되었습니다.");
}
compareResult.put("success", true);
} else {
// 정상 처리 (비교 로직에 해당 안됨)
compareResult.put("success", true);
compareResult.put("message", "정상 처리되었습니다.");
compareResult.put("processStatus", "정상");
}
} catch (Exception e) {
log.error("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno, e);
compareResult.put("success", false);
compareResult.put("message", "처리 실패: " + e.getMessage());
compareResult.put("processStatus", "실패");
}
return compareResult;
}
/**
*
* , , .
*
* @param modifyData // VO
* @return
*/
@Override
@Transactional
public int saveCarFfnlgTrgts(CarFfnlgTrgtModifiedDataVO modifyData) {
int result = 0;
// 1. 삭제된 행 처리 (논리 삭제)
List<CarFfnlgTrgtVO> deletedRows = modifyData.getDeletedRows();
if (deletedRows != null && !deletedRows.isEmpty()) {
for (CarFfnlgTrgtVO vo : deletedRows) {
// 삭제자 정보 설정
String dltr = SessionUtil.getUserId();
if (dltr == null || dltr.isEmpty()) {
throw new MessageException("로그인 정보가 없습니다.");
}
vo.setDltr(dltr);
// 논리 삭제 처리
result += mapper.delete(vo);
}
}
Map<String, Object> resultData = new HashMap<>();
resultData.put("compareResults", compareResults);
resultData.put("totalCount", targetList.size());
resultData.put("successCount", successCount);
resultData.put("failCount", failCount);
resultData.put("productUseCount", productUseCount);
resultData.put("transferCount", transferCount);
resultData.put("normalCount", normalCount);
// 2. 추가된 행 처리
List<CarFfnlgTrgtVO> createdRows = modifyData.getCreatedRows();
if (createdRows != null && !createdRows.isEmpty()) {
for (CarFfnlgTrgtVO vo : createdRows) {
// 등록자 정보 설정
String rgtr = SessionUtil.getUserId();
if (rgtr == null || rgtr.isEmpty()) {
throw new MessageException("로그인 정보가 없습니다.");
}
vo.setRgtr(rgtr);
// 기본값 설정
if (vo.getTaskPrcsSttsCd() == null || vo.getTaskPrcsSttsCd().isEmpty()) {
vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT);
}
result += mapper.insert(vo);
}
}
log.info("========== API 호출 및 비교 완료 ==========");
log.info("성공: {}건, 실패: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
successCount, failCount, productUseCount, transferCount, normalCount);
// 3. 수정된 행 처리 (처리상태, 비고만 수정)
List<CarFfnlgTrgtVO> updatedRows = modifyData.getUpdatedRows();
if (updatedRows != null && !updatedRows.isEmpty()) {
for (CarFfnlgTrgtVO vo : updatedRows) {
vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
result += mapper.updateTaskPrcsSttsCdAndRmrk(vo);
}
}
return resultData;
log.info("과태료 대상 일괄 저장 완료 - 처리 건수: {}", result);
return result;
}
/**
*
* @param vo
* @return
*/
@Override
public List<CarFfnlgTrgtExcelVO> selectListForExcel(CarFfnlgTrgtVO vo) {
log.debug("과태료 대상 목록 엑셀 다운로드용 조회 - 검색조건: {}", vo);
return mapper.selectListForExcel(vo);
}
}

@ -0,0 +1,586 @@
package go.kr.project.carInspectionPenalty.registration.service.impl;
import egovframework.util.DateUtil;
import egovframework.util.StringUtil;
import go.kr.project.api.model.response.NewLedgerResponse;
/**
* (Remark)
*
* <p> .</p>
*
* <p> :</p>
* <ul>
* <li>1. : </li>
* <li>2. : (25.9.5.) , / </li>
* <li>3. -1: / , </li>
* <li>4. -2: / , 115 </li>
* </ul>
*/
public class ComparisonRemarkBuilder {
/**
*
* :
*
* @return
*/
public static String buildProductUseRemark() {
return "상품용";
}
/**
* - (31 )
* : (25.9.11.)
*
* @param ledgerRecord ( )
* @return
*/
public static String buildProductCloseRemark(NewLedgerResponse.Record ledgerRecord) {
String chgYmdFormatted = DateUtil.formatToShortDate(ledgerRecord.getChgYmd());
return String.format("명의이전(%s) 이전소유자 상품용", chgYmdFormatted);
}
/**
* - (31 )
* : / , (25.8.19.)
*
* @param ledgerRecord ( )
* @param sggNm
* @param ownerNm
* @return
*/
public static String buildProductLevyRemark(NewLedgerResponse.Record ledgerRecord,
String sggNm, String ownerNm) {
String chgYmdFormatted = DateUtil.formatToShortDate(ledgerRecord.getChgYmd());
return String.format("%s/ %s, 미수검명의이전(%s)",
StringUtil.nvl(sggNm), StringUtil.nvl(ownerNm), chgYmdFormatted);
}
/**
*
* : (25.9.5.), /
*
* @param ledgerRecord ( )
* @param sggNm
* @param ownerNm
* @return
*/
public static String buildOwnerChangeRemark(NewLedgerResponse.Record ledgerRecord,
String sggNm, String ownerNm) {
String chgYmdFormatted = DateUtil.formatToShortDate(ledgerRecord.getChgYmd());
return String.format("명의이전(%s), %s/ %s",
chgYmdFormatted, StringUtil.nvl(sggNm), StringUtil.nvl(ownerNm));
}
/**
* - Case -1 ( )
* : / ,
*
* @param sggNm
* @param ownerNm
* @return
*/
public static String buildTransferCase1Remark(String sggNm, String ownerNm) {
return String.format("%s/ %s, 검사일사용본거지", StringUtil.nvl(sggNm), StringUtil.nvl(ownerNm));
}
/**
* - Case -2 (115 )
* : / , 115
*
* @param sggNm
* @param ownerNm
* @return
*/
public static String buildTransferCase2Remark(String sggNm, String ownerNm) {
return String.format("%s/ %s, 115일 도래지", StringUtil.nvl(sggNm), StringUtil.nvl(ownerNm));
}
// ========================== 비고 상세 (RMRK_DTL) 생성 메서드 ==========================
/**
*
*
* @param vhclno
* @param inspYmd
* @param step1OwnerNm Step1
* @param targetChgYmd
* @param step4OwnerNm Step4 ( )
* @return
*/
public static String buildProductUseRemarkDetail(String vhclno, String inspYmd,
String step1OwnerNm, String targetChgYmd, String step4OwnerNm) {
StringBuilder sb = new StringBuilder();
sb.append("[상품용 판정 - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("Step1) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 검사일자(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(step1OwnerNm)).append("\n");
sb.append(" - 조건 확인: 소유자명에 '상품용' 포함 여부 → O\n\n");
sb.append("Step2) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 오늘일자\n");
sb.append(" - 목적: 차량번호, 성명, 주민번호, 법정동코드 조회\n\n");
sb.append("Step3) 자동차등록원부(갑) 조회\n");
sb.append(" - 조회 기준: 차량번호 + 성명 + 주민번호 + 법정동코드\n");
sb.append(" - 검색 조건:\n");
sb.append(" · 변경업무구분코드(CHG_TASK_SE_CD) = '11' (명의이전)\n");
sb.append(" · 변경일자(CHG_YMD) ≤ 검사종료일자\n");
sb.append(" · 조건 충족하는 레코드 중 가장 마지막 일자 선택\n");
sb.append(" - 선택된 갑부 변경일자: ").append(DateUtil.formatDateString(targetChgYmd)).append("\n\n");
sb.append("Step4) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 갑부 변경일자(").append(DateUtil.formatDateString(targetChgYmd)).append(")\n");
sb.append(" - 변경일 기준 소유자명: ").append(StringUtil.nvl(step4OwnerNm)).append("\n");
sb.append(" - 조건 확인: Step1 대표소유자 회원번호 = Step4 대표소유자 회원번호 → 일치\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. 검사일자 기준 소유자명에 '상품용' 포함\n");
sb.append("2. 갑부 상세에서 변경업무구분코드 '11'(명의이전) 레코드가 검사종료일자 이내에 존재\n");
sb.append("3. 갑부 변경일자 시점의 대표소유자 회원번호와 검사일자 기준 대표소유자 회원번호가 동일\n");
sb.append("→ 상품용 판정 (처리상태: 02_상품용)\n");
return sb.toString();
}
/**
* -
*
* @param vhclno
* @param inspYmd
* @param step1OwnerNm Step1
* @param targetChgYmd
* @param step4OwnerNm Step4 ( )
* @param daysBetween
* @param vldPrdExpryYmd
* @param inspEndYmd
* @return
*/
public static String buildProductCloseLevyRemarkDetail(String vhclno, String inspYmd,
String step1OwnerNm, String targetChgYmd,
String step4OwnerNm, long daysBetween,
String vldPrdExpryYmd, String inspEndYmd) {
StringBuilder sb = new StringBuilder();
sb.append("[내사종결 - 명의이전 이전소유자 상품용 판정 - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n");
sb.append("31일 기준일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(inspYmd).plusDays(31).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("Step1) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 검사일자(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(step1OwnerNm)).append("\n");
sb.append(" - 조건 확인: 소유자명에 '상품용' 포함 여부 → X\n\n");
sb.append("Step2) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 오늘일자\n");
sb.append(" - 목적: 차량번호, 성명, 주민번호, 법정동코드 조회\n\n");
sb.append("Step3) 자동차등록원부(갑) 조회\n");
sb.append(" - 조회 기준: 차량번호 + 성명 + 주민번호 + 법정동코드\n");
sb.append(" - 검색 조건:\n");
sb.append(" · 변경업무구분코드(CHG_TASK_SE_CD) = '11' (명의이전)\n");
sb.append(" · 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자\n");
sb.append(" - 유효기간만료일: ").append(DateUtil.formatDateString(vldPrdExpryYmd)).append("\n");
sb.append(" - 유효기간만료일-90일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n");
sb.append(" - 검사종료일자: ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append(" - 검색 범위: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(" ~ ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append(" · 조건 충족하는 레코드 중 가장 마지막 일자 선택\n");
sb.append(" - 선택된 갑부 명의이전 일자: ").append(DateUtil.formatDateString(targetChgYmd)).append("\n\n");
sb.append("Step4) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 명의이전일자-1일(").append(DateUtil.formatDateString(
DateUtil.parseDate(targetChgYmd).minusDays(1).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(")\n");
sb.append(" - 명의이전 전 소유자명: ").append(StringUtil.nvl(step4OwnerNm)).append("\n");
sb.append(" - 조건 확인: 명의이전 전 소유자명에 '상품용' 포함 → O\n\n");
sb.append("=== 일수 계산 ===\n");
sb.append("검사일~명의이전일 일수: ").append(daysBetween).append("일\n");
sb.append("기준일수: 31일\n");
sb.append("조건 확인: ").append(daysBetween).append("일 ≤ 31일 → 이내\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. 검사일자 기준 소유자명에 '상품용' 미포함\n");
sb.append("2. 갑부 상세에서 변경업무구분코드 '11'(명의이전) 레코드가 검사일자 이내에 존재\n");
sb.append("3. 명의이전일자-1일 시점의 소유자명에 '상품용' 포함\n");
sb.append("4. 명의이전일이 검사일로부터 31일 이내 (").append(daysBetween).append("일)\n");
sb.append("→ 내사종결 - 명의이전 이전소유자 상품용 판정 (처리상태: 04_내사종결)\n");
return sb.toString();
}
/**
* -
*
* @param vhclno
* @param inspYmd
* @param step1OwnerNm Step1
* @param targetChgYmd
* @param step4OwnerNm Step4 ( )
* @param daysBetween
* @return
*/
public static String buildOwnerChangeRemarkDetail(String vhclno, String inspYmd,
String step1OwnerNm, String targetChgYmd,
String step4OwnerNm, long daysBetween) {
StringBuilder sb = new StringBuilder();
sb.append("[내사종결 - 명의이전 판정 - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n");
sb.append("31일 기준일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(inspYmd).plusDays(31).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("Step1) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 검사일자(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(step1OwnerNm)).append("\n");
sb.append(" - 조건 확인: 소유자명에 '상품용' 포함 여부 → X\n\n");
sb.append("Step2) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 오늘일자\n");
sb.append(" - 목적: 차량번호, 성명, 주민번호, 법정동코드 조회\n\n");
sb.append("Step3) 자동차등록원부(갑) 조회\n");
sb.append(" - 조회 기준: 차량번호 + 성명 + 주민번호 + 법정동코드\n");
sb.append(" - 검색 조건:\n");
sb.append(" · 변경업무구분코드(CHG_TASK_SE_CD) = '11' (명의이전)\n");
sb.append(" · 변경일자(CHG_YMD) ≤ 검사일자\n");
sb.append(" · 조건 충족하는 레코드 중 가장 마지막 일자 선택\n");
sb.append(" - 선택된 갑부 명의이전 일자: ").append(DateUtil.formatDateString(targetChgYmd)).append("\n\n");
sb.append("Step4) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 명의이전일자(").append(DateUtil.formatDateString(targetChgYmd)).append(")\n");
sb.append(" - 명의이전 시점 소유자명: ").append(StringUtil.nvl(step4OwnerNm)).append("\n");
sb.append(" - 조건 확인: Step1 대표소유자 회원번호 = Step4 대표소유자 회원번호 → 일치\n");
sb.append(" - 명의이전 시점 소유자명에 '상품용' 포함 → X\n\n");
sb.append("=== 일수 계산 ===\n");
sb.append("검사일~명의이전일 일수: ").append(daysBetween).append("일\n");
sb.append("기준일수: 31일\n");
sb.append("조건 확인: ").append(daysBetween).append("일 ≤ 31일 → 이내\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. 검사일자 기준 소유자명에 '상품용' 미포함\n");
sb.append("2. 갑부 상세에서 변경업무구분코드 '11'(명의이전) 레코드가 검사일자 이내에 존재\n");
sb.append("3. 명의이전 시점의 대표소유자 회원번호와 검사일자 기준 대표소유자 회원번호가 동일 (소유자 변동 없음)\n");
sb.append("4. 명의이전 시점 소유자명에 '상품용' 미포함\n");
sb.append("5. 명의이전일이 검사일로부터 31일 이내 (").append(daysBetween).append("일)\n");
sb.append("→ 내사종결 - 명의이전 판정 (처리상태: 04_내사종결)\n");
return sb.toString();
}
/**
* -
*
* @param vhclno
* @param inspYmd
* @param step1OwnerNm Step1
* @param targetChgYmd
* @param step4OwnerNm Step4 ( )
* @param daysBetween
* @param vldPrdExpryYmd
* @param inspEndYmd
* @return
*/
public static String buildProductLevyOver31RemarkDetail(String vhclno, String inspYmd,
String step1OwnerNm, String targetChgYmd,
String step4OwnerNm, long daysBetween,
String vldPrdExpryYmd, String inspEndYmd) {
StringBuilder sb = new StringBuilder();
sb.append("[날짜수정후부과 - 명의이전 이전소유자 상품용 판정 - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n");
sb.append("31일 기준일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(inspYmd).plusDays(31).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("Step1) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 검사일자(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(step1OwnerNm)).append("\n");
sb.append(" - 조건 확인: 소유자명에 '상품용' 포함 여부 → X\n\n");
sb.append("Step2) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 오늘일자\n");
sb.append(" - 목적: 차량번호, 성명, 주민번호, 법정동코드 조회\n\n");
sb.append("Step3) 자동차등록원부(갑) 조회\n");
sb.append(" - 조회 기준: 차량번호 + 성명 + 주민번호 + 법정동코드\n");
sb.append(" - 검색 조건:\n");
sb.append(" · 변경업무구분코드(CHG_TASK_SE_CD) = '11' (명의이전)\n");
sb.append(" · 유효기간만료일-90일 <= CHG_YMD <= 검사종료일자\n");
sb.append(" - 유효기간만료일: ").append(DateUtil.formatDateString(vldPrdExpryYmd)).append("\n");
sb.append(" - 유효기간만료일-90일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n");
sb.append(" - 검사종료일자: ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append(" - 검색 범위: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(" ~ ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append(" · 조건 충족하는 레코드 중 가장 마지막 일자 선택\n");
sb.append(" - 선택된 갑부 명의이전 일자: ").append(DateUtil.formatDateString(targetChgYmd)).append("\n\n");
sb.append("Step4) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 명의이전일자-1일(").append(DateUtil.formatDateString(
DateUtil.parseDate(targetChgYmd).minusDays(1).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(")\n");
sb.append(" - 명의이전 전 소유자명: ").append(StringUtil.nvl(step4OwnerNm)).append("\n");
sb.append(" - 조건 확인: 명의이전 전 소유자명에 '상품용' 포함 → O\n\n");
sb.append("=== 일수 계산 ===\n");
sb.append("검사일~명의이전일 일수: ").append(daysBetween).append("일\n");
sb.append("기준일수: 31일\n");
sb.append("조건 확인: ").append(daysBetween).append("일 > 31일 → 초과\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. 검사일자 기준 소유자명에 '상품용' 미포함\n");
sb.append("2. 갑부 상세에서 변경업무구분코드 '11'(명의이전) 레코드가 검사일자 이내에 존재\n");
sb.append("3. 명의이전일자-1일 시점의 소유자명에 '상품용' 포함\n");
sb.append("4. 명의이전일이 검사일로부터 31일 초과 (").append(daysBetween).append("일)\n");
sb.append("→ 날짜수정후부과 - 명의이전 이전소유자 상품용 판정 (처리상태: 05_날짜수정후부과)\n");
sb.append("※ 부과일자를 명의이전일자로 수정하여 재부과 필요\n");
return sb.toString();
}
/**
* -
*
* @param vhclno
* @param inspYmd
* @param vldPrdExpryYmd
* @param inspEndYmd
* @param step1OwnerNm Step1
* @param targetChgYmd
* @param step4OwnerNm Step4 ( )
* @param daysBetween
* @return
*/
public static String buildOwnerLevyOver31RemarkDetail(String vhclno, String inspYmd, String vldPrdExpryYmd,
String inspEndYmd, String step1OwnerNm, String targetChgYmd,
String step4OwnerNm, long daysBetween) {
StringBuilder sb = new StringBuilder();
sb.append("[날짜수정후부과 - 명의이전 판정 - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n");
sb.append("유효기간만료일: ").append(DateUtil.formatDateString(vldPrdExpryYmd)).append("\n");
sb.append("유효기간만료일 - 90일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n");
sb.append("검사종료일자: ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append("31일 기준일: ").append(DateUtil.formatDateString(
DateUtil.parseDate(inspYmd).plusDays(31).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("Step1) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 검사일자(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(step1OwnerNm)).append("\n");
sb.append(" - 조건 확인: 소유자명에 '상품용' 포함 여부 → X\n\n");
sb.append("Step2) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 오늘일자\n");
sb.append(" - 목적: 차량번호, 성명, 주민번호, 법정동코드 조회\n\n");
sb.append("Step3) 자동차등록원부(갑) 조회\n");
sb.append(" - 조회 기준: 차량번호 + 성명 + 주민번호 + 법정동코드\n");
sb.append(" - 검색 조건:\n");
sb.append(" · 변경업무구분코드(CHG_TASK_SE_CD) = '11' (명의이전)\n");
sb.append(" · 유효기간만료일-90일 ≤ 변경일자(CHG_YMD) ≤ 검사종료일자\n");
sb.append(" · 범위: ").append(DateUtil.formatDateString(
DateUtil.parseDate(vldPrdExpryYmd).minusDays(90).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(" ~ ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append(" · 조건 충족하는 레코드 중 가장 마지막 일자 선택\n");
sb.append(" - 선택된 갑부 명의이전 일자: ").append(DateUtil.formatDateString(targetChgYmd)).append("\n\n");
sb.append("Step4) 자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차대번호 + 명의이전일자(").append(DateUtil.formatDateString(targetChgYmd)).append(")\n");
sb.append(" - 명의이전 시점 소유자명: ").append(StringUtil.nvl(step4OwnerNm)).append("\n");
sb.append(" - 조건 확인: Step1 대표소유자 회원번호 = Step4 대표소유자 회원번호 → 일치\n");
sb.append(" - 명의이전 시점 소유자명에 '상품용' 포함 → X\n\n");
sb.append("=== 일수 계산 ===\n");
sb.append("검사일~명의이전일 일수: ").append(daysBetween).append("일\n");
sb.append("기준일수: 31일\n");
sb.append("조건 확인: ").append(daysBetween).append("일 > 31일 → 초과\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. 검사일자 기준 소유자명에 '상품용' 미포함\n");
sb.append("2. 갑부 상세에서 변경업무구분코드 '11'(명의이전) 레코드가 검사기간(유효기간만료일-90일 ~ 검사종료일자) 내에 존재\n");
sb.append("3. 명의이전 시점의 대표소유자 회원번호와 검사일자 기준 대표소유자 회원번호가 동일 (소유자 변동 없음)\n");
sb.append("4. 명의이전 시점 소유자명에 '상품용' 미포함\n");
sb.append("5. 명의이전일이 검사일로부터 31일 초과 (").append(daysBetween).append("일)\n");
sb.append("→ 날짜수정후부과 - 명의이전 판정 (처리상태: 05_날짜수정후부과)\n");
sb.append("※ 부과일자를 명의이전일자로 수정하여 재부과 필요\n");
return sb.toString();
}
/**
* - Case -1 ( )
*
* @param vhclno
* @param inspYmd
* @param ownerNm
* @param usgsrhldStdgCd
* @param sggNm
* @param userOrgCd
* @return
*/
public static String buildTransferCase1RemarkDetail(String vhclno, String inspYmd,
String ownerNm, String usgsrhldStdgCd,
String sggNm, String userOrgCd) {
StringBuilder sb = new StringBuilder();
sb.append("[이첩 판정 - 검사일 사용본거지 (이첩-1) - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사일자: ").append(DateUtil.formatDateString(inspYmd)).append("\n");
sb.append("DAYCNT: 115일 이하\n");
sb.append("부과기준일: 검사일자 (DAYCNT ≤ 115이므로)\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 부과기준일(검사일자)(").append(DateUtil.formatDateString(inspYmd)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(ownerNm)).append("\n");
sb.append(" - 사용본거지법정동코드: ").append(StringUtil.nvl(usgsrhldStdgCd)).append("\n\n");
sb.append("=== 법정동코드 비교 ===\n");
String legalDong4 = usgsrhldStdgCd != null && usgsrhldStdgCd.length() >= 4 ? usgsrhldStdgCd.substring(0, 4) : "";
String userOrg4 = userOrgCd != null && userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : "";
sb.append("사용본거지법정동코드 앞 4자리: ").append(legalDong4).append("\n");
sb.append("현재 사용자 조직코드 앞 4자리: ").append(userOrg4).append("\n");
sb.append("코드 불일치 → 이첩 대상\n");
sb.append("시군구코드: ").append(usgsrhldStdgCd != null && usgsrhldStdgCd.length() >= 5 ? usgsrhldStdgCd.substring(0, 5) : "").append("\n");
sb.append("시군구명: ").append(StringUtil.nvl(sggNm)).append("\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. DAYCNT ≤ 115일이므로 부과기준일 = 검사일자\n");
sb.append("2. 검사일자 기준 사용본거지법정동코드 앞 4자리와 현재 사용자 조직코드 앞 4자리가 불일치\n");
sb.append("→ 이첩 판정 - 검사일 사용본거지 (처리상태: 03_이첩)\n");
return sb.toString();
}
/**
* - Case -2 (115 )
*
* @param vhclno
* @param inspEndYmd
* @param ownerNm
* @param usgsrhldStdgCd
* @param sggNm
* @param userOrgCd
* @param daycnt
* @return
*/
public static String buildTransferCase2RemarkDetail(String vhclno, String inspEndYmd,
String ownerNm, String usgsrhldStdgCd,
String sggNm, String userOrgCd, String daycnt) {
StringBuilder sb = new StringBuilder();
sb.append("[이첩 판정 - 115일 도래지 (이첩-2) - 지연]\n\n");
sb.append("=== 기본 정보 ===\n");
sb.append("차량번호: ").append(StringUtil.nvl(vhclno)).append("\n");
sb.append("검사종료일자: ").append(DateUtil.formatDateString(inspEndYmd)).append("\n");
sb.append("DAYCNT: ").append(StringUtil.nvl(daycnt)).append("일 (115일 초과)\n");
sb.append("부과기준일 계산: 검사종료일자 + 115일 = ").append(DateUtil.formatDateString(
DateUtil.parseDate(inspEndYmd).plusDays(115).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append("\n\n");
sb.append("=== API 호출 및 비교 과정 ===\n");
sb.append("자동차기본정보 조회\n");
sb.append(" - 조회 기준: 차량번호 + 부과기준일(검사종료일자+115일)(").append(DateUtil.formatDateString(
DateUtil.parseDate(inspEndYmd).plusDays(115).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)).append(")\n");
sb.append(" - 소유자명: ").append(StringUtil.nvl(ownerNm)).append("\n");
sb.append(" - 사용본거지법정동코드: ").append(StringUtil.nvl(usgsrhldStdgCd)).append("\n\n");
sb.append("=== 법정동코드 비교 ===\n");
String legalDong4 = usgsrhldStdgCd != null && usgsrhldStdgCd.length() >= 4 ? usgsrhldStdgCd.substring(0, 4) : "";
String userOrg4 = userOrgCd != null && userOrgCd.length() >= 4 ? userOrgCd.substring(0, 4) : "";
sb.append("사용본거지법정동코드 앞 4자리: ").append(legalDong4).append("\n");
sb.append("현재 사용자 조직코드 앞 4자리: ").append(userOrg4).append("\n");
sb.append("코드 불일치 → 이첩 대상\n");
sb.append("시군구코드: ").append(usgsrhldStdgCd != null && usgsrhldStdgCd.length() >= 5 ? usgsrhldStdgCd.substring(0, 5) : "").append("\n");
sb.append("시군구명: ").append(StringUtil.nvl(sggNm)).append("\n\n");
sb.append("=== 판정 근거 ===\n");
sb.append("1. DAYCNT > 115일(").append(StringUtil.nvl(daycnt)).append("일)이므로 부과기준일 = 검사종료일자 + 115일\n");
sb.append("2. 부과기준일 기준 사용본거지법정동코드 앞 4자리와 현재 사용자 조직코드 앞 4자리가 불일치\n");
sb.append("→ 이첩 판정 - 115일 도래지 (처리상태: 03_이첩)\n");
return sb.toString();
}
/**
* ( 3 1300)
*
* @param str
* @return
*/
private static String truncateToMaxLength(String str) {
if (str == null) {
return "";
}
// 4000바이트 / 3(한글) = 약 1333자, 여유있게 1300자로 제한
if (str.length() > 1300) {
return str.substring(0, 1297) + "...";
}
return str;
}
/**
*
*
* @param record
* @return
*/
public static String buildLedgerRecordDetail(NewLedgerResponse.Record record) {
if (record == null) {
return "";
}
StringBuilder detail = new StringBuilder();
StringUtil.appendIfNotEmpty(detail, "변경업무구분코드", record.getChgTaskSeCd());
StringUtil.appendIfNotEmpty(detail, "변경업무구분명", record.getChgTaskSeNm());
StringUtil.appendIfNotEmpty(detail, "변경일자", DateUtil.formatDateString(record.getChgYmd()));
StringUtil.appendIfNotEmpty(detail, "주요번호", record.getMainNo());
StringUtil.appendIfNotEmpty(detail, "일련번호", record.getSno());
StringUtil.appendIfNotEmpty(detail, "특별사항", record.getSpcablMttr());
StringUtil.appendIfNotEmpty(detail, "명의자명", record.getHshldrNm());
StringUtil.appendIfNotEmpty(detail, "명의자식별번호", StringUtil.maskIdecno(record.getHshldrIdecno()));
StringUtil.appendIfNotEmpty(detail, "신청접수번호", record.getAplyRcptNo());
StringUtil.appendIfNotEmpty(detail, "차량관리번호", record.getVhmno());
StringUtil.appendIfNotEmpty(detail, "원부그룹번호", record.getLedgerGroupNo());
StringUtil.appendIfNotEmpty(detail, "원부개별번호", record.getLedgerIndivNo());
StringUtil.appendIfNotEmpty(detail, "상세일련번호", record.getDtlSn());
return detail.toString();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save