From ebcd17a6f06aa40f832efc451dc6867e2c3943fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?=
Date: Thu, 6 Nov 2025 20:34:59 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20VMIS-interface=20=ED=86=B5=ED=95=A9=20(?=
=?UTF-8?q?Spring=20Boot=202.7=20=ED=98=B8=ED=99=98)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- VMIS-interface 전체 코드 이식 (33개 Java 파일, 2개 XML)
- 패키지 변경: com.vmis.interfaceapp → go.kr.project.vmis
- Spring Boot 3 → 2 호환: jakarta → javax
- Java 17 → 8 호환: Text Blocks, List.of() 제거
- HttpClient 5 → 4 변환
- GPKI 라이브러리 추가
- application.yml에 VMIS 설정 통합
- MyBatis 매퍼 경로 추가
빌드 성공 확인 ✅
Co-Authored-By: Claude
---
build.gradle | 4 +
.../kr/project/vmis/client/GovernmentApi.java | 21 +
.../vmis/client/GovernmentApiClient.java | 627 +++++++++++++++++
.../kr/project/vmis/config/ApiConstant.java | 50 ++
.../project/vmis/config/DatabaseConfig.java | 57 ++
.../go/kr/project/vmis/config/Globals.java | 39 ++
.../go/kr/project/vmis/config/GpkiConfig.java | 20 +
.../project/vmis/config/HttpClientConfig.java | 52 ++
.../kr/project/vmis/config/OpenApiConfig.java | 27 +
.../project/vmis/config/PropertiesConfig.java | 10 +
.../config/properties/VmisProperties.java | 180 +++++
.../VehicleInterfaceController.java | 203 ++++++
.../go/kr/project/vmis/gpki/GpkiService.java | 7 +
.../kr/project/vmis/gpki/NoopGpkiService.java | 18 +
.../kr/project/vmis/gpki/RealGpkiService.java | 41 ++
.../mapper/CarBassMatterInqireMapper.java | 54 ++
.../vmis/mapper/CarLedgerFrmbkMapper.java | 28 +
.../vmis/model/basic/BasicRequest.java | 90 +++
.../vmis/model/basic/BasicResponse.java | 121 ++++
.../model/basic/CarBassMatterInqireVO.java | 646 ++++++++++++++++++
.../project/vmis/model/common/Envelope.java | 32 +
.../model/ledger/CarLedgerFrmbkDtlVO.java | 73 ++
.../vmis/model/ledger/CarLedgerFrmbkVO.java | 192 ++++++
.../vmis/model/ledger/LedgerRequest.java | 82 +++
.../vmis/model/ledger/LedgerResponse.java | 250 +++++++
.../CarBassMatterInqireLogService.java | 56 ++
.../service/CarBassMatterInqireService.java | 87 +++
.../service/CarLedgerFrmbkLogService.java | 61 ++
.../vmis/service/CarLedgerFrmbkService.java | 85 +++
.../project/vmis/service/RequestEnricher.java | 63 ++
.../vmis/util/ExceptionDetailUtil.java | 29 +
.../kr/project/vmis/util/GpkiCryptoUtil.java | 98 +++
.../go/kr/project/vmis/util/NewGpkiUtil.java | 383 +++++++++++
.../go/kr/project/vmis/util/TxIdUtil.java | 18 +
src/main/resources/application.yml | 63 +-
.../vmis/CarBassMatterInqireMapper_maria.xml | 148 ++++
.../vmis/CarLedgerFrmbkMapper_maria.xml | 178 +++++
37 files changed, 4192 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/go/kr/project/vmis/client/GovernmentApi.java
create mode 100644 src/main/java/go/kr/project/vmis/client/GovernmentApiClient.java
create mode 100644 src/main/java/go/kr/project/vmis/config/ApiConstant.java
create mode 100644 src/main/java/go/kr/project/vmis/config/DatabaseConfig.java
create mode 100644 src/main/java/go/kr/project/vmis/config/Globals.java
create mode 100644 src/main/java/go/kr/project/vmis/config/GpkiConfig.java
create mode 100644 src/main/java/go/kr/project/vmis/config/HttpClientConfig.java
create mode 100644 src/main/java/go/kr/project/vmis/config/OpenApiConfig.java
create mode 100644 src/main/java/go/kr/project/vmis/config/PropertiesConfig.java
create mode 100644 src/main/java/go/kr/project/vmis/config/properties/VmisProperties.java
create mode 100644 src/main/java/go/kr/project/vmis/controller/VehicleInterfaceController.java
create mode 100644 src/main/java/go/kr/project/vmis/gpki/GpkiService.java
create mode 100644 src/main/java/go/kr/project/vmis/gpki/NoopGpkiService.java
create mode 100644 src/main/java/go/kr/project/vmis/gpki/RealGpkiService.java
create mode 100644 src/main/java/go/kr/project/vmis/mapper/CarBassMatterInqireMapper.java
create mode 100644 src/main/java/go/kr/project/vmis/mapper/CarLedgerFrmbkMapper.java
create mode 100644 src/main/java/go/kr/project/vmis/model/basic/BasicRequest.java
create mode 100644 src/main/java/go/kr/project/vmis/model/basic/BasicResponse.java
create mode 100644 src/main/java/go/kr/project/vmis/model/basic/CarBassMatterInqireVO.java
create mode 100644 src/main/java/go/kr/project/vmis/model/common/Envelope.java
create mode 100644 src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkDtlVO.java
create mode 100644 src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkVO.java
create mode 100644 src/main/java/go/kr/project/vmis/model/ledger/LedgerRequest.java
create mode 100644 src/main/java/go/kr/project/vmis/model/ledger/LedgerResponse.java
create mode 100644 src/main/java/go/kr/project/vmis/service/CarBassMatterInqireLogService.java
create mode 100644 src/main/java/go/kr/project/vmis/service/CarBassMatterInqireService.java
create mode 100644 src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkLogService.java
create mode 100644 src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkService.java
create mode 100644 src/main/java/go/kr/project/vmis/service/RequestEnricher.java
create mode 100644 src/main/java/go/kr/project/vmis/util/ExceptionDetailUtil.java
create mode 100644 src/main/java/go/kr/project/vmis/util/GpkiCryptoUtil.java
create mode 100644 src/main/java/go/kr/project/vmis/util/NewGpkiUtil.java
create mode 100644 src/main/java/go/kr/project/vmis/util/TxIdUtil.java
create mode 100644 src/main/resources/mybatis/mapper/vmis/CarBassMatterInqireMapper_maria.xml
create mode 100644 src/main/resources/mybatis/mapper/vmis/CarLedgerFrmbkMapper_maria.xml
diff --git a/build.gradle b/build.gradle
index 46ddf16..c831be6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -140,6 +140,10 @@ 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'
diff --git a/src/main/java/go/kr/project/vmis/client/GovernmentApi.java b/src/main/java/go/kr/project/vmis/client/GovernmentApi.java
new file mode 100644
index 0000000..159c6d2
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/client/GovernmentApi.java
@@ -0,0 +1,21 @@
+package go.kr.project.vmis.client;
+
+import go.kr.project.vmis.model.basic.BasicRequest;
+import go.kr.project.vmis.model.basic.BasicResponse;
+import go.kr.project.vmis.model.common.Envelope;
+import go.kr.project.vmis.model.ledger.LedgerRequest;
+import go.kr.project.vmis.model.ledger.LedgerResponse;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * 정부 시스템 연계 API 추상화 인터페이스.
+ *
+ * 외부 정부 시스템과의 통신 계약을 명확히 하여 테스트 용이성과
+ * 추후 교체 가능성을 높입니다.
+ */
+public interface GovernmentApi {
+
+ ResponseEntity> callBasic(Envelope envelope);
+
+ ResponseEntity> callLedger(Envelope envelope);
+}
diff --git a/src/main/java/go/kr/project/vmis/client/GovernmentApiClient.java b/src/main/java/go/kr/project/vmis/client/GovernmentApiClient.java
new file mode 100644
index 0000000..60d815e
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/client/GovernmentApiClient.java
@@ -0,0 +1,627 @@
+package go.kr.project.vmis.client;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import go.kr.project.vmis.config.properties.VmisProperties;
+import go.kr.project.vmis.gpki.GpkiService;
+import go.kr.project.vmis.model.basic.BasicRequest;
+import go.kr.project.vmis.model.basic.BasicResponse;
+import go.kr.project.vmis.model.common.Envelope;
+import go.kr.project.vmis.model.ledger.LedgerRequest;
+import go.kr.project.vmis.model.ledger.LedgerResponse;
+import go.kr.project.vmis.util.TxIdUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestTemplate;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 정부 시스템 API 클라이언트
+ *
+ * 이 클래스는 시군구연계 자동차 정보 조회를 위해 정부 시스템의 API를 호출하는
+ * 클라이언트 역할을 수행합니다. HTTP 통신, 암호화, 에러 처리 등 정부 API와의
+ * 모든 상호작용을 캡슐화합니다.
+ *
+ * 주요 책임:
+ *
+ * 정부 API 엔드포인트로 HTTP 요청 전송
+ * GPKI(행정전자서명) 암호화/복호화 처리
+ * 필수 HTTP 헤더 구성 및 관리
+ * 요청/응답 데이터의 JSON 직렬화/역직렬화
+ * 트랜잭션 ID(tx_id) 생성 및 추적
+ * 네트워크 오류 및 HTTP 에러 처리
+ * 상세한 로깅을 통한 디버깅 지원
+ *
+ *
+ * 아키텍처 패턴:
+ *
+ * Adapter 패턴: 외부 정부 시스템 API를 내부 인터페이스로 변환
+ * Template Method 패턴: callModel 메서드가 공통 흐름을 정의
+ * Dependency Injection: 생성자를 통한 의존성 주입
+ *
+ *
+ * 보안 특성:
+ *
+ * GPKI 암호화를 통한 데이터 보안 (선택적 활성화)
+ * API 키 기반 인증
+ * 기관 식별 정보(INFO_SYS_ID, REGION_CODE 등)를 헤더에 포함
+ *
+ *
+ * @see RestTemplate
+ * @see GpkiService
+ * @see VmisProperties
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class GovernmentApiClient implements GovernmentApi {
+
+ /**
+ * Spring RestTemplate
+ *
+ * HTTP 클라이언트로서 실제 네트워크 통신을 수행합니다.
+ * 이 객체는 Spring Bean으로 주입되며, 설정에 따라 다음을 포함할 수 있습니다:
+ *
+ * Connection Timeout 설정
+ * Read Timeout 설정
+ * Connection Pool 관리
+ * 메시지 컨버터 (Jackson for JSON)
+ * 인터셉터 (로깅, 헤더 추가 등)
+ *
+ */
+ private final RestTemplate restTemplate;
+
+ /**
+ * VMIS 설정 속성
+ *
+ * application.yml 또는 application.properties에서 로드된 설정값들입니다.
+ * 포함되는 주요 설정:
+ *
+ * 정부 API URL (호스트, 포트, 경로)
+ * API 키 및 인증 정보
+ * 시스템 식별 정보 (INFO_SYS_ID, REGION_CODE 등)
+ * GPKI 설정 (인증서 서버 ID 등)
+ *
+ */
+ private final VmisProperties props;
+
+ /**
+ * GPKI(행정전자서명) 서비스
+ *
+ * 정부24 등 공공기관 간 통신에 사용되는 암호화 서비스입니다.
+ * 주요 기능:
+ *
+ * 요청 데이터 암호화 (공개키 암호화)
+ * 응답 데이터 복호화 (개인키 복호화)
+ * 전자서명 생성 및 검증
+ * 암호화 활성화 여부 확인
+ *
+ *
+ * 암호화가 비활성화된 경우 평문(Plain Text)으로 통신합니다.
+ */
+ private final GpkiService gpkiService;
+
+ /**
+ * Jackson ObjectMapper
+ *
+ * Java 객체와 JSON 문자열 간의 변환을 담당합니다.
+ * 주요 역할:
+ *
+ * 요청 객체를 JSON 문자열로 직렬화 (Serialization)
+ * 응답 JSON을 Java 객체로 역직렬화 (Deserialization)
+ * 제네릭 타입 처리 (TypeReference 사용)
+ * 날짜/시간 포맷 변환
+ * null 값 처리
+ *
+ *
+ * Spring Boot가 자동 구성한 ObjectMapper를 주입받아 사용하므로
+ * 전역 설정(날짜 포맷, 네이밍 전략 등)이 일관되게 적용됩니다.
+ */
+ private final ObjectMapper objectMapper;
+
+
+ /**
+ * 서비스 타입 열거형
+ *
+ * 정부 API 서비스의 종류를 구분하는 열거형입니다.
+ * 각 서비스 타입은 서로 다른 엔드포인트와 API 키를 가집니다.
+ *
+ * 서비스 타입:
+ *
+ * BASIC: 자동차 기본사항 조회 서비스
+ *
+ * 차량번호로 기본 정보(소유자, 차종, 용도 등) 조회
+ * 비교적 간단한 정보 제공
+ * 응답 속도가 빠름
+ *
+ *
+ * LEDGER: 자동차 등록원부(갑) 조회 서비스
+ *
+ * 상세한 등록 정보 및 법적 권리관계 조회
+ * 저당권, 압류, 소유권 이전 이력 등 포함
+ * 민감 정보를 포함하여 권한 검증이 엄격함
+ *
+ *
+ *
+ */
+ public enum ServiceType {
+ /**
+ * Basic service type.
+ */
+ BASIC,
+ /**
+ * Ledger service type.
+ */
+ LEDGER }
+
+ /**
+ * HTTP 헤더 구성
+ *
+ * 정부 API 호출에 필요한 모든 HTTP 헤더를 구성하는 private 메서드입니다.
+ * 정부 시스템은 엄격한 헤더 검증을 수행하므로 모든 필수 헤더가 정확히 포함되어야 합니다.
+ *
+ * 헤더 구성 항목:
+ *
+ *
+ * 헤더명
+ * 설명
+ * 예시값
+ * 필수여부
+ *
+ *
+ * Content-Type
+ * 요청 바디의 미디어 타입 및 문자 인코딩
+ * application/json; charset=UTF-8
+ * 필수
+ *
+ *
+ * Accept
+ * 클라이언트가 수용 가능한 응답 형식
+ * application/json
+ * 필수
+ *
+ *
+ * gpki_yn
+ * GPKI 암호화 사용 여부 (Y/N)
+ * Y
+ * 필수
+ *
+ *
+ * tx_id
+ * 트랜잭션 고유 ID (요청 추적용)
+ * 20250104123045_abc123
+ * 필수
+ *
+ *
+ * cert_server_id
+ * 인증서 서버 식별자
+ * VMIS_SERVER_01
+ * 필수
+ *
+ *
+ * api_key
+ * 서비스별 API 인증 키
+ * abc123def456...
+ * 필수
+ *
+ *
+ * cvmis_apikey
+ * CVMIS 시스템 API 키
+ * xyz789uvw012...
+ * 필수
+ *
+ *
+ * INFO_SYS_ID
+ * 정보시스템 식별자
+ * VMIS_SEOUL
+ * 필수
+ *
+ *
+ *
+ * 문자 인코딩:
+ *
+ * Content-Type에 UTF-8 인코딩을 명시적으로 지정
+ * 한글 데이터 처리를 위해 필수
+ * 정부 시스템이 다양한 클라이언트와 호환되도록 표준 인코딩 사용
+ *
+ *
+ * 보안 고려사항:
+ *
+ * API 키는 설정 파일에서 안전하게 관리
+ * 로그에 API 키가 노출되지 않도록 주의
+ * 각 서비스(BASIC, LEDGER)마다 별도의 API 키 사용 가능
+ *
+ *
+ * @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 호출
+ *
+ * 타입 안전성이 보장되는 자동차 기본사항 조회 메서드입니다.
+ * 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.
+ *
+ * 특징:
+ *
+ * 제네릭 타입으로 컴파일 타입 타입 체크
+ * 요청/응답 객체가 Envelope로 감싸져 있음
+ * Jackson TypeReference를 사용한 제네릭 역직렬화
+ * API 호출 전후로 DB에 로그성 데이터 저장
+ *
+ *
+ * 처리 흐름:
+ *
+ * 요청 정보를 DB에 INSERT (로그 저장)
+ * 정부 API 호출
+ * 응답 정보를 DB에 UPDATE
+ * 에러 발생 시 에러 정보도 DB에 UPDATE
+ *
+ *
+ * 사용 예시:
+ *
+ * BasicRequest request = new BasicRequest();
+ * request.setVehicleNo("12가3456");
+ *
+ * Envelope<BasicRequest> envelope = new Envelope<>();
+ * envelope.setData(request);
+ *
+ * ResponseEntity<Envelope<BasicResponse>> response = govClient.callBasic(envelope);
+ * BasicResponse data = response.getBody().getData();
+ *
+ *
+ * @param envelope 자동차 기본사항 조회 요청을 담은 Envelope
+ * @return ResponseEntity<Envelope<BasicResponse>> 조회 결과를 담은 응답
+ */
+ public ResponseEntity> callBasic(Envelope envelope) {
+ // 순수한 전송 책임만 수행: DB 로깅은 서비스 레이어에서 처리
+ return callModel(ServiceType.BASIC, envelope, new TypeReference>(){});
+ }
+
+ /**
+ * 자동차 등록원부(갑) 조회 API 호출
+ *
+ * 타입 안전성이 보장되는 자동차 등록원부 조회 메서드입니다.
+ * 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.
+ *
+ * 특징:
+ *
+ * 제네릭 타입으로 컴파일 타임 타입 체크
+ * 요청/응답 객체가 Envelope로 감싸져 있음
+ * Jackson TypeReference를 사용한 제네릭 역직렬화
+ *
+ *
+ * 사용 예시:
+ *
+ * LedgerRequest request = new LedgerRequest();
+ * request.setVehicleNo("12가3456");
+ * request.setOwnerName("홍길동");
+ *
+ * Envelope<LedgerRequest> envelope = new Envelope<>();
+ * envelope.setData(request);
+ *
+ * ResponseEntity<Envelope<LedgerResponse>> response = govClient.callLedger(envelope);
+ * LedgerResponse data = response.getBody().getData();
+ *
+ *
+ * @param envelope 자동차 등록원부 조회 요청을 담은 Envelope
+ * @return ResponseEntity<Envelope<LedgerResponse>> 조회 결과를 담은 응답
+ */
+ public ResponseEntity> callLedger(Envelope envelope) {
+ // TypeReference를 사용하여 제네릭 타입 정보 전달
+ // 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결
+ return callModel(ServiceType.LEDGER, envelope, new TypeReference>(){});
+ }
+
+ /**
+ * 정부 API 호출 (타입 안전 모델 기반)
+ *
+ * 이 메서드는 정부 API 호출의 핵심 로직을 담고 있는 제네릭 메서드입니다.
+ * Java 객체를 받아 JSON으로 변환하고, 암호화하여 전송한 후, 응답을 복호화하여
+ * 다시 Java 객체로 변환하는 전체 파이프라인을 처리합니다.
+ *
+ * Template Method 패턴:
+ *
+ * 이 메서드는 Template Method 패턴의 템플릿 역할
+ * 공통 처리 흐름을 정의하고 서비스별 차이는 파라미터로 처리
+ * 코드 중복을 제거하고 일관성을 보장
+ *
+ *
+ * 제네릭 타입 파라미터:
+ *
+ * <TReq>: 요청 데이터 타입 (BasicRequest 또는 LedgerRequest)
+ * <TResp>: 응답 데이터 타입 (BasicResponse 또는 LedgerResponse)
+ * 타입 안전성을 보장하여 런타임 에러 방지
+ *
+ *
+ * 처리 흐름 (상세):
+ *
+ * 설정 로드:
+ * 서비스 타입에 따라 BASIC 또는 LEDGER 설정 선택
+ *
+ * URL 및 트랜잭션 ID 구성:
+ * 완전한 API URL 생성
+ * 고유 트랜잭션 ID 생성
+ *
+ * 직렬화 (Serialization):
+ * Java 객체(Envelope<TReq>)를 JSON 문자열로 변환
+ * ObjectMapper.writeValueAsString() 사용
+ *
+ * 헤더 구성:
+ * buildHeaders() 메서드 호출
+ * 모든 필수 헤더 추가
+ *
+ * GPKI 암호화 (선택적):
+ * GPKI가 활성화된 경우 JSON을 암호화
+ * gpkiEncrypt() 메서드 호출
+ *
+ * HTTP 요청 전송:
+ * RestTemplate.exchange()로 POST 요청
+ * 요청 로그 기록
+ *
+ * GPKI 복호화 (선택적):
+ * 성공 응답(2xx)이고 GPKI가 활성화된 경우
+ * gpkiDecrypt() 메서드 호출
+ *
+ * 역직렬화 (Deserialization):
+ * JSON 문자열을 Java 객체(Envelope<TResp>)로 변환
+ * TypeReference를 사용하여 제네릭 타입 정보 보존
+ *
+ * 응답 반환:
+ * ResponseEntity로 감싸서 HTTP 정보 포함
+ *
+ *
+ *
+ * 에러 처리 전략 (3단계):
+ *
+ * HttpStatusCodeException (HTTP 에러):
+ *
+ * 정부 API가 4xx 또는 5xx 상태 코드를 반환한 경우
+ * 에러 응답 바디를 파싱하여 Envelope 객체로 변환 시도
+ * 파싱 실패 시 빈 Envelope 객체 반환
+ * 에러 로그 기록 (WARN 레벨)
+ *
+ *
+ * JSON 파싱 에러:
+ *
+ * 응답 JSON이 예상한 형식과 다른 경우
+ * RuntimeException으로 래핑하여 상위로 전파
+ *
+ *
+ * 기타 예외:
+ *
+ * 네트워크 타임아웃, 연결 실패 등
+ * RuntimeException으로 래핑하여 상위로 전파
+ *
+ *
+ *
+ *
+ * TypeReference의 필요성:
+ * Java의 제네릭은 런타임에 타입 소거(Type Erasure)가 발생하여
+ * {@code objectMapper.readValue(json, Envelope.class)}와 같은 코드는
+ * 컴파일되지 않습니다. TypeReference는 익명 클래스를 사용하여 컴파일 타임의
+ * 제네릭 타입 정보를 런타임에 전달하는 Jackson의 메커니즘입니다.
+ *
+ * 로깅 정보:
+ *
+ * 요청 로그: [GOV-REQ] url, tx_id, gpki, length
+ * 에러 로그: [GOV-ERR] status, body
+ *
+ *
+ * @param 요청 데이터의 제네릭 타입
+ * @param 응답 데이터의 제네릭 타입
+ * @param type 서비스 타입 (BASIC 또는 LEDGER)
+ * @param envelope 요청 데이터를 담은 Envelope 객체
+ * @param respType 응답 타입에 대한 TypeReference (제네릭 타입 정보 보존용)
+ * @return ResponseEntity<Envelope<TResp>> 응답 데이터를 담은 ResponseEntity
+ * @throws RuntimeException JSON 직렬화/역직렬화 실패, 네트워크 오류, GPKI 암복호화 실패 등
+ */
+ private ResponseEntity> callModel(ServiceType type,
+ Envelope envelope,
+ TypeReference> 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 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 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 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 empty = new Envelope<>();
+ try {
+ // 에러 응답을 Envelope 객체로 파싱
+ Envelope 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 암호화 처리
+ *
+ * 평문 JSON 문자열을 GPKI(행정전자서명) 알고리즘으로 암호화하는 헬퍼 메서드입니다.
+ * 암호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.
+ *
+ * 암호화 과정:
+ *
+ * 평문 JSON 문자열을 바이트 배열로 변환
+ * 정부 시스템의 공개키를 사용하여 암호화
+ * 암호화된 바이트 배열을 Base64로 인코딩
+ * Base64 문자열 반환
+ *
+ *
+ * 에러 처리:
+ *
+ * 인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우
+ * 암호화 오류: 암호화 알고리즘 실행 중 오류 발생
+ * 인코딩 오류: Base64 인코딩 실패
+ * 모든 예외는 RuntimeException으로 래핑하여 즉시 중단
+ *
+ *
+ * 보안 고려사항:
+ *
+ * 공개키 암호화 방식 사용 (비대칭키)
+ * 정부 시스템만 개인키로 복호화 가능
+ * 민감한 개인정보 보호
+ *
+ *
+ * @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 복호화 처리
+ *
+ * 암호화된 응답 문자열을 GPKI(행정전자서명) 알고리즘으로 복호화하는 헬퍼 메서드입니다.
+ * 복호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.
+ *
+ * 복호화 과정:
+ *
+ * Base64로 인코딩된 암호문을 바이트 배열로 디코딩
+ * 우리 시스템의 개인키를 사용하여 복호화
+ * 복호화된 바이트 배열을 UTF-8 문자열로 변환
+ * 평문 JSON 문자열 반환
+ *
+ *
+ * 에러 처리:
+ *
+ * 인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우
+ * 복호화 오류: 암호문이 손상되었거나 올바르지 않은 경우
+ * 디코딩 오류: Base64 디코딩 실패
+ * 문자 인코딩 오류: UTF-8 변환 실패
+ * 모든 예외는 RuntimeException으로 래핑하여 즉시 중단
+ *
+ *
+ * 보안 고려사항:
+ *
+ * 개인키 암호화 방식 사용 (비대칭키)
+ * 개인키는 안전하게 저장 및 관리 필요
+ * 복호화 실패 시 상세 에러 정보 로깅 주의 (정보 유출 방지)
+ *
+ *
+ * @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);
+ }
+ }
+
+}
diff --git a/src/main/java/go/kr/project/vmis/config/ApiConstant.java b/src/main/java/go/kr/project/vmis/config/ApiConstant.java
new file mode 100644
index 0000000..ef22b15
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/ApiConstant.java
@@ -0,0 +1,50 @@
+package go.kr.project.vmis.config;
+
+/**
+ * 애플리케이션 전역에서 사용되는 상수를 관리하는 클래스
+ *
+ * 이 클래스는 하드코딩된 값들을 상수로 관리하여 유지보수성을 향상시킵니다.
+ *
+ * 연계 결과 코드
+ * 시스템 상수
+ *
+ */
+public class ApiConstant {
+
+ // ===== 연계 결과 코드 (CNTC_RESULT_CODE) =====
+
+ /**
+ * 연계 결과 코드: 정상
+ * 외부 API 호출이 정상적으로 처리되었음을 나타냅니다.
+ */
+ public static final String CNTC_RESULT_CODE_SUCCESS = "00";
+
+ /**
+ * 연계 결과 코드: 정보 없음
+ * 외부 API 호출이 정상적으로 처리되었으나 정보없음을 나타냅니다.
+ */
+ public static final String CNTC_RESULT_CODE_NO_DATA = "99";
+
+ /**
+ * 연계 결과 코드: 에러
+ * 외부 API 호출 중 에러가 발생했음을 나타냅니다.
+ */
+ public static final String CNTC_RESULT_CODE_ERROR = "ERROR";
+
+ // ===== 시스템 상수 =====
+
+ /**
+ * 기본 등록자: SYSTEM
+ * 시스템에서 자동으로 등록한 레코드의 등록자명입니다.
+ */
+ public static final String DEFAULT_REGISTRANT = "SYSTEM";
+
+ // ===== Private Constructor =====
+
+ /**
+ * 유틸리티 클래스이므로 인스턴스 생성을 방지합니다.
+ */
+ private ApiConstant() {
+ throw new AssertionError("Constraint 클래스는 인스턴스화할 수 없습니다.");
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/DatabaseConfig.java b/src/main/java/go/kr/project/vmis/config/DatabaseConfig.java
new file mode 100644
index 0000000..38e92d8
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/DatabaseConfig.java
@@ -0,0 +1,57 @@
+package go.kr.project.vmis.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+
+/**
+ * 데이터베이스 및 트랜잭션 설정
+ *
+ * 이 클래스는 데이터베이스 연결과 트랜잭션 관리를 위한 설정을 제공합니다.
+ * Spring Boot의 자동 설정을 활용하되, 명시적인 트랜잭션 관리를 위해
+ * TransactionManager를 직접 설정합니다.
+ *
+ *
+ * DataSource: application.yml에서 자동 설정
+ * SqlSessionFactory: MyBatis Spring Boot Starter가 자동 생성
+ * TransactionManager: 명시적으로 설정하여 트랜잭션 관리
+ * MapperScan: go.kr.project.vmis.mapper 패키지의 Mapper 인터페이스 자동 스캔
+ *
+ */
+@Configuration
+@EnableTransactionManagement
+@MapperScan("go.kr.project.vmis.mapper")
+public class DatabaseConfig {
+
+ /**
+ * 트랜잭션 관리자를 설정합니다.
+ *
+ * DataSourceTransactionManager는 JDBC 기반의 트랜잭션을 관리합니다.
+ * @Transactional 어노테이션을 사용하여 선언적 트랜잭션 관리가 가능합니다.
+ *
+ * 트랜잭션 전파(Propagation), 격리 수준(Isolation), 타임아웃 등의
+ * 세부 설정은 @Transactional 어노테이션의 속성으로 지정할 수 있습니다.
+ *
+ * 예제:
+ *
+ * {@code
+ * @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
+ * public void saveData() {
+ * // 트랜잭션 처리가 필요한 로직
+ * }
+ * }
+ *
+ *
+ * @param dataSource Spring Boot가 자동 생성한 DataSource 빈
+ * @return PlatformTransactionManager 트랜잭션 관리자 인스턴스
+ */
+ @Bean
+ public PlatformTransactionManager transactionManager(DataSource dataSource) {
+ return new DataSourceTransactionManager(dataSource);
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/Globals.java b/src/main/java/go/kr/project/vmis/config/Globals.java
new file mode 100644
index 0000000..b014999
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/Globals.java
@@ -0,0 +1,39 @@
+package go.kr.project.vmis.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 전역 상수 및 설정값을 관리하는 클래스
+ *
+ * 이 클래스는 애플리케이션 전반에서 사용되는 전역 설정값을 제공합니다.
+ * Spring의 @Value를 통해 application.yml의 설정값을 주입받아 사용합니다.
+ */
+@Component
+public class Globals {
+
+ /**
+ * 데이터베이스 타입 (예: mariadb, oracle, mysql 등)
+ *
+ * MyBatis Mapper XML 파일 선택 시 사용됩니다.
+ * mapper-locations: classpath:mybatis/mapper/**\/*_${Globals.DbType}.xml
+ *
+ * 예시:
+ *
+ * DbType = "mariadb" → user_mariadb.xml 매핑
+ * DbType = "oracle" → user_oracle.xml 매핑
+ *
+ *
+ */
+ public static String DbType;
+
+ /**
+ * application.yml에서 Globals.DbType 값을 주입합니다.
+ *
+ * @param dbType 데이터베이스 타입 (기본값: mariadb)
+ */
+ @Value("${Globals.DbType:mariadb}")
+ public void setDbType(String dbType) {
+ Globals.DbType = dbType;
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/GpkiConfig.java b/src/main/java/go/kr/project/vmis/config/GpkiConfig.java
new file mode 100644
index 0000000..8109e72
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/GpkiConfig.java
@@ -0,0 +1,20 @@
+package go.kr.project.vmis.config;
+
+import go.kr.project.vmis.config.properties.VmisProperties;
+import go.kr.project.vmis.gpki.GpkiService;
+import go.kr.project.vmis.gpki.NoopGpkiService;
+import go.kr.project.vmis.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();
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/HttpClientConfig.java b/src/main/java/go/kr/project/vmis/config/HttpClientConfig.java
new file mode 100644
index 0000000..4ff063d
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/HttpClientConfig.java
@@ -0,0 +1,52 @@
+package go.kr.project.vmis.config;
+
+import go.kr.project.vmis.config.properties.VmisProperties;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * HttpClient 설정
+ * Apache HttpClient 4를 사용한 RestTemplate 구성
+ */
+@Configuration
+public class HttpClientConfig {
+
+ @Bean
+ public RestTemplate restTemplate(VmisProperties props, RestTemplateBuilder builder) {
+ VmisProperties.GovProps gov = props.getGov();
+ int connectTimeout = gov.getConnectTimeoutMillis();
+ int readTimeout = gov.getReadTimeoutMillis();
+
+ // HttpClient 4 방식의 RequestConfig
+ RequestConfig requestConfig = RequestConfig.custom()
+ .setConnectTimeout(connectTimeout)
+ .setSocketTimeout(readTimeout)
+ .setConnectionRequestTimeout(connectTimeout)
+ .build();
+
+ // 커넥션 풀 설정
+ PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+ cm.setMaxTotal(100);
+ cm.setDefaultMaxPerRoute(20);
+
+ // HttpClient 생성
+ CloseableHttpClient httpClient = HttpClientBuilder.create()
+ .setDefaultRequestConfig(requestConfig)
+ .setConnectionManager(cm)
+ .build();
+
+ // RestTemplate에 HttpClient 적용
+ HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
+
+ return builder
+ .requestFactory(() -> requestFactory)
+ .build();
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/OpenApiConfig.java b/src/main/java/go/kr/project/vmis/config/OpenApiConfig.java
new file mode 100644
index 0000000..4facc9f
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/OpenApiConfig.java
@@ -0,0 +1,27 @@
+package go.kr.project.vmis.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(""));
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/config/PropertiesConfig.java b/src/main/java/go/kr/project/vmis/config/PropertiesConfig.java
new file mode 100644
index 0000000..648ad1b
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/PropertiesConfig.java
@@ -0,0 +1,10 @@
+package go.kr.project.vmis.config;
+
+import go.kr.project.vmis.config.properties.VmisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties(VmisProperties.class)
+public class PropertiesConfig {
+}
diff --git a/src/main/java/go/kr/project/vmis/config/properties/VmisProperties.java b/src/main/java/go/kr/project/vmis/config/properties/VmisProperties.java
new file mode 100644
index 0000000..74b5961
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/config/properties/VmisProperties.java
@@ -0,0 +1,180 @@
+package go.kr.project.vmis.config.properties;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+@ConfigurationProperties(prefix = "vmis")
+@Validated
+public class VmisProperties {
+
+ @NotNull
+ private SystemProps system = new SystemProps();
+ @NotNull
+ private GpkiProps gpki = new GpkiProps();
+ @NotNull
+ private GovProps gov = new GovProps();
+
+ public SystemProps getSystem() { return system; }
+ public void setSystem(SystemProps system) { this.system = system; }
+ public GpkiProps getGpki() { return gpki; }
+ public void setGpki(GpkiProps gpki) { this.gpki = gpki; }
+ public GovProps getGov() { return gov; }
+ public void setGov(GovProps gov) { this.gov = gov; }
+
+ public static class SystemProps {
+ @NotBlank
+ private String infoSysId;
+ /** INFO_SYS_IP */
+ private String infoSysIp;
+ /** 시군구코드 (SIGUNGU_CODE) */
+ private String regionCode;
+ private String departmentCode;
+ // 담당자 정보
+ private String chargerId;
+ private String chargerIp;
+ private String chargerNm;
+
+ public String getInfoSysId() { return infoSysId; }
+ public void setInfoSysId(String infoSysId) { this.infoSysId = infoSysId; }
+ public String getInfoSysIp() { return infoSysIp; }
+ public void setInfoSysIp(String infoSysIp) { this.infoSysIp = infoSysIp; }
+ public String getRegionCode() { return regionCode; }
+ public void setRegionCode(String regionCode) { this.regionCode = regionCode; }
+ public String getDepartmentCode() { return departmentCode; }
+ public void setDepartmentCode(String departmentCode) { this.departmentCode = departmentCode; }
+ public String getChargerId() { return chargerId; }
+ public void setChargerId(String chargerId) { this.chargerId = chargerId; }
+ public String getChargerIp() { return chargerIp; }
+ public void setChargerIp(String chargerIp) { this.chargerIp = chargerIp; }
+ public String getChargerNm() { return chargerNm; }
+ public void setChargerNm(String chargerNm) { this.chargerNm = chargerNm; }
+ }
+
+ 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 String getEnabled() { return enabled; }
+ public void setEnabled(String enabled) { this.enabled = enabled; }
+ public boolean isUseSign() { return useSign; }
+ public void setUseSign(boolean useSign) { this.useSign = useSign; }
+ public String getCharset() { return charset; }
+ public void setCharset(String charset) { this.charset = charset; }
+ public String getCertServerId() { return certServerId; }
+ public void setCertServerId(String certServerId) { this.certServerId = certServerId; }
+ public String getTargetServerId() { return targetServerId; }
+ public void setTargetServerId(String targetServerId) { this.targetServerId = targetServerId; }
+ public Boolean getLdap() { return ldap; }
+ public void setLdap(Boolean ldap) { this.ldap = ldap; }
+ public String getGpkiLicPath() { return gpkiLicPath; }
+ public void setGpkiLicPath(String gpkiLicPath) { this.gpkiLicPath = gpkiLicPath; }
+ public String getCertFilePath() { return certFilePath; }
+ public void setCertFilePath(String certFilePath) { this.certFilePath = certFilePath; }
+ public String getEnvCertFilePathName() { return envCertFilePathName; }
+ public void setEnvCertFilePathName(String envCertFilePathName) { this.envCertFilePathName = envCertFilePathName; }
+ public String getEnvPrivateKeyFilePathName() { return envPrivateKeyFilePathName; }
+ public void setEnvPrivateKeyFilePathName(String envPrivateKeyFilePathName) { this.envPrivateKeyFilePathName = envPrivateKeyFilePathName; }
+ public String getEnvPrivateKeyPasswd() { return envPrivateKeyPasswd; }
+ public void setEnvPrivateKeyPasswd(String envPrivateKeyPasswd) { this.envPrivateKeyPasswd = envPrivateKeyPasswd; }
+ public String getSigCertFilePathName() { return sigCertFilePathName; }
+ public void setSigCertFilePathName(String sigCertFilePathName) { this.sigCertFilePathName = sigCertFilePathName; }
+ public String getSigPrivateKeyFilePathName() { return sigPrivateKeyFilePathName; }
+ public void setSigPrivateKeyFilePathName(String sigPrivateKeyFilePathName) { this.sigPrivateKeyFilePathName = sigPrivateKeyFilePathName; }
+ public String getSigPrivateKeyPasswd() { return sigPrivateKeyPasswd; }
+ public void setSigPrivateKeyPasswd(String sigPrivateKeyPasswd) { this.sigPrivateKeyPasswd = sigPrivateKeyPasswd; }
+
+ public boolean isEnabledFlag() { return "Y".equalsIgnoreCase(enabled); }
+ }
+
+ public static class GovProps {
+ @NotBlank
+ private String scheme = "http";
+ @NotBlank
+ private String host;
+ @NotBlank
+ private String basePath;
+ private int connectTimeoutMillis = 5000;
+ private int readTimeoutMillis = 10000;
+ @NotNull
+ private Services services = new Services();
+
+ public String getScheme() { return scheme; }
+ public void setScheme(String scheme) { this.scheme = scheme; }
+ public String getHost() { return host; }
+ public void setHost(String host) { this.host = host; }
+ public String getBasePath() { return basePath; }
+ public void setBasePath(String basePath) { this.basePath = basePath; }
+ public int getConnectTimeoutMillis() { return connectTimeoutMillis; }
+ public void setConnectTimeoutMillis(int connectTimeoutMillis) { this.connectTimeoutMillis = connectTimeoutMillis; }
+ public int getReadTimeoutMillis() { return readTimeoutMillis; }
+ public void setReadTimeoutMillis(int readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; }
+ public Services getServices() { return services; }
+ public void setServices(Services services) { this.services = 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 Services {
+ @NotNull
+ private Service basic = new Service();
+ @NotNull
+ private Service ledger = new Service();
+
+ public Service getBasic() { return basic; }
+ public void setBasic(Service basic) { this.basic = basic; }
+ public Service getLedger() { return ledger; }
+ public void setLedger(Service ledger) { this.ledger = ledger; }
+ }
+
+ public static class Service {
+ @NotBlank
+ private String path;
+ @NotBlank
+ private String cntcInfoCode;
+ @NotBlank
+ private String apiKey;
+ @NotBlank
+ private String cvmisApikey;
+
+ public String getPath() { return path; }
+ public void setPath(String path) { this.path = path; }
+ public String getCntcInfoCode() { return cntcInfoCode; }
+ public void setCntcInfoCode(String cntcInfoCode) { this.cntcInfoCode = cntcInfoCode; }
+ public String getApiKey() { return apiKey; }
+ public void setApiKey(String apiKey) { this.apiKey = apiKey; }
+ public String getCvmisApikey() { return cvmisApikey; }
+ public void setCvmisApikey(String cvmisApikey) { this.cvmisApikey = cvmisApikey; }
+ }
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/controller/VehicleInterfaceController.java b/src/main/java/go/kr/project/vmis/controller/VehicleInterfaceController.java
new file mode 100644
index 0000000..aae9853
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/controller/VehicleInterfaceController.java
@@ -0,0 +1,203 @@
+package go.kr.project.vmis.controller;
+
+import go.kr.project.vmis.model.basic.BasicRequest;
+import go.kr.project.vmis.model.basic.BasicResponse;
+import go.kr.project.vmis.model.common.Envelope;
+import go.kr.project.vmis.model.ledger.LedgerRequest;
+import go.kr.project.vmis.model.ledger.LedgerResponse;
+import go.kr.project.vmis.service.CarBassMatterInqireService;
+import go.kr.project.vmis.service.CarLedgerFrmbkService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 자동차 정보 연계 REST 컨트롤러
+ *
+ * 이 컨트롤러는 시군구연계 자동차 정보 조회 API의 진입점(Entry Point)을 제공합니다.
+ * 클라이언트로부터 HTTP 요청을 받아 정부 시스템과의 통신을 중계하는 역할을 담당합니다.
+ *
+ * 주요 기능:
+ *
+ * 자동차 기본사항 조회 API 제공
+ * 자동차 등록원부(갑) 조회 API 제공
+ * 요청 데이터를 Envelope로 감싸서 정부 시스템으로 전달
+ * 정부 시스템의 응답을 클라이언트에게 반환
+ *
+ *
+ * API 경로:
+ *
+ * 기본 경로: /api/v1/vehicles
+ * 자동차 기본사항 조회: POST /api/v1/vehicles/basic
+ * 자동차 등록원부 조회: POST /api/v1/vehicles/ledger
+ *
+ *
+ * 아키텍처 설계:
+ *
+ * 컨트롤러는 얇은(Thin) 레이어로 설계되어 비즈니스 로직을 포함하지 않음
+ * 모든 실제 처리는 각 도메인별 서비스에 위임
+ * Swagger/OpenAPI 문서 자동 생성을 위한 어노테이션 포함
+ *
+ *
+ * @see Envelope
+ */
+@RestController
+@RequestMapping("/api/v1/vehicles")
+@RequiredArgsConstructor
+@Slf4j
+@Tag(name = "Vehicle Interfaces", description = "시군구연계 자동차 정보 연계 API")
+public class VehicleInterfaceController {
+
+ private final CarBassMatterInqireService carBassMatterInqireService;
+ private final CarLedgerFrmbkService carLedgerFrmbkService;
+
+
+ /**
+ * 자동차 기본사항 조회 API
+ *
+ * 차량번호, 소유자 정보 등 자동차의 기본적인 정보를 조회하는 API입니다.
+ *
+ * 요청 처리 흐름:
+ *
+ * 클라이언트로부터 JSON 형식의 요청 수신
+ * 요청 바디가 Envelope<BasicRequest> 객체로 역직렬화됨
+ * Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
+ * 변환된 요청 객체를 GovernmentApiClient로 전달
+ * GovernmentApiClient가 정부 시스템과 통신 수행
+ * 응답 데이터를 Envelope<BasicResponse>로 감싸서 반환
+ * Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
+ *
+ *
+ * HTTP 메서드 및 경로:
+ *
+ * 메서드: POST
+ * 경로: /api/v1/vehicles/basic
+ * Content-Type: application/json (요청)
+ * Accept: application/json (응답)
+ *
+ *
+ * 데이터 구조:
+ *
+ * Envelope: 공통 헤더 정보(전문 ID, 전송 시각 등)와 실제 데이터를 감싸는 래퍼 객체
+ * BasicRequest: 자동차 기본사항 조회를 위한 요청 데이터 (차량번호 등)
+ * BasicResponse: 자동차 기본사항 조회 결과 데이터 (소유자, 차종, 연식 등)
+ *
+ *
+ * 에러 처리:
+ *
+ * JSON 파싱 오류 시 400 Bad Request 반환 (Spring 자동 처리)
+ * 정부 API 호출 실패 시 해당 HTTP 상태 코드 그대로 반환
+ * 네트워크 오류나 기타 예외 발생 시 RuntimeException으로 래핑되어 처리
+ *
+ *
+ * @param envelope 자동차 기본사항 조회 요청을 담은 Envelope 객체 공통 헤더(header)와 실제 요청 데이터(data)로 구성됨
+ * @return ResponseEntity<Envelope<BasicResponse>> 조회 결과를 담은 응답 객체 HTTP 상태 코드, 헤더, 응답 바디를 포함
+ */
+ @PostMapping(value = "/basic", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ summary = "자동차기본사항조회",
+ description = "시군구연계 자동차기본사항조회 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.",
+ requestBody = @RequestBody(
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON_VALUE,
+ examples = @ExampleObject(
+ name = "기본사항조회 예제",
+ value = "{\"data\": [{\"INFO_SYS_ID\": \"41-345\", \"VHRNO\": \"12가3456\"}]}"
+ )
+ )
+ )
+ )
+ public ResponseEntity> basic(
+ @org.springframework.web.bind.annotation.RequestBody Envelope envelope
+ ) {
+ // 서비스에서 요청 보강/로깅/호출을 모두 오케스트레이션
+ return carBassMatterInqireService.basic(envelope);
+ }
+
+ /**
+ * 자동차 등록원부(갑) 조회 API
+ *
+ * 자동차 등록원부의 갑지에 기재된 상세한 등록 정보를 조회하는 API입니다.
+ * 등록원부는 자동차의 법적 소유관계와 등록사항을 증명하는 공적 장부입니다.
+ *
+ * 요청 처리 흐름:
+ *
+ * 클라이언트로부터 JSON 형식의 요청 수신
+ * 요청 바디가 Envelope<LedgerRequest> 객체로 역직렬화됨
+ * Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
+ * 변환된 요청 객체를 GovernmentApiClient로 전달
+ * GovernmentApiClient가 정부 시스템과 통신 수행
+ * 응답 데이터를 Envelope<LedgerResponse>로 감싸서 반환
+ * Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
+ *
+ *
+ * HTTP 메서드 및 경로:
+ *
+ * 메서드: POST
+ * 경로: /api/v1/vehicles/ledger
+ * Content-Type: application/json (요청)
+ * Accept: application/json (응답)
+ *
+ *
+ * 데이터 구조:
+ *
+ * Envelope: 공통 헤더 정보(전문 ID, 전송 시각 등)와 실제 데이터를 감싸는 래퍼 객체
+ * LedgerRequest: 자동차 등록원부 조회를 위한 요청 데이터 (차량번호, 소유자 정보 등)
+ * LedgerResponse: 자동차 등록원부 조회 결과 데이터 (상세한 등록사항, 소유권 이력 등)
+ *
+ *
+ * 등록원부(갑)에 포함되는 정보:
+ *
+ * 차량 기본 정보 (차명, 차종, 용도 등)
+ * 소유자 정보 (성명, 주소, 주민등록번호 등)
+ * 등록 이력 (최초 등록일, 변경 이력 등)
+ * 저당권 및 압류 정보
+ * 기타 법적 권리관계
+ *
+ *
+ * 에러 처리:
+ *
+ * JSON 파싱 오류 시 400 Bad Request 반환 (Spring 자동 처리)
+ * 정부 API 호출 실패 시 해당 HTTP 상태 코드 그대로 반환
+ * 네트워크 오류나 기타 예외 발생 시 RuntimeException으로 래핑되어 처리
+ * 권한 부족 시 403 Forbidden 또는 401 Unauthorized 반환 가능
+ *
+ *
+ * 보안 및 개인정보:
+ *
+ * 민감한 개인정보를 포함하므로 적절한 인증/인가 필요
+ * GPKI 암호화를 통한 데이터 보안 강화
+ * 로그 기록 시 개인정보 마스킹 필요
+ *
+ *
+ * @param envelope 자동차 등록원부 조회 요청을 담은 Envelope 객체 공통 헤더(header)와 실제 요청 데이터(data)로 구성됨
+ * @return ResponseEntity<Envelope<LedgerResponse>> 등록원부 조회 결과를 담은 응답 객체 HTTP 상태 코드, 헤더, 응답 바디를 포함
+ */
+ @PostMapping(value = "/ledger", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ summary = "자동차등록원부(갑)",
+ description = "시군구연계 자동차등록원부(갑) 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.",
+ requestBody = @RequestBody(
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON_VALUE,
+ examples = @ExampleObject(
+ name = "등록원부 조회 예제",
+ value = "{\"data\": [{\"INFO_SYS_ID\": \"41-345\", \"VHRNO\": \"12가3456\"}]}"
+ )
+ )
+ )
+ )
+ public ResponseEntity> ledger(
+ @org.springframework.web.bind.annotation.RequestBody Envelope envelope
+ ) {
+ // 서비스에서 요청 보강/호출을 오케스트레이션
+ return carLedgerFrmbkService.ledger(envelope);
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/gpki/GpkiService.java b/src/main/java/go/kr/project/vmis/gpki/GpkiService.java
new file mode 100644
index 0000000..6057875
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/gpki/GpkiService.java
@@ -0,0 +1,7 @@
+package go.kr.project.vmis.gpki;
+
+public interface GpkiService {
+ String encrypt(String plain) throws Exception;
+ String decrypt(String cipher) throws Exception;
+ boolean isEnabled();
+}
diff --git a/src/main/java/go/kr/project/vmis/gpki/NoopGpkiService.java b/src/main/java/go/kr/project/vmis/gpki/NoopGpkiService.java
new file mode 100644
index 0000000..e4ee3d8
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/gpki/NoopGpkiService.java
@@ -0,0 +1,18 @@
+package go.kr.project.vmis.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;
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/gpki/RealGpkiService.java b/src/main/java/go/kr/project/vmis/gpki/RealGpkiService.java
new file mode 100644
index 0000000..57470cf
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/gpki/RealGpkiService.java
@@ -0,0 +1,41 @@
+package go.kr.project.vmis.gpki;
+
+import go.kr.project.vmis.config.properties.VmisProperties;
+import go.kr.project.vmis.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;
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/mapper/CarBassMatterInqireMapper.java b/src/main/java/go/kr/project/vmis/mapper/CarBassMatterInqireMapper.java
new file mode 100644
index 0000000..9be1bb4
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/mapper/CarBassMatterInqireMapper.java
@@ -0,0 +1,54 @@
+package go.kr.project.vmis.mapper;
+
+import go.kr.project.vmis.model.basic.CarBassMatterInqireVO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 자동차 기본 사항 조회 Mapper
+ *
+ * API 호출 정보를 관리하는 Mapper 인터페이스입니다.
+ *
+ * 최초 요청 시: insertCarBassMatterInqire() 호출
+ * 결과 수신 시: updateCarBassMatterInqire() 호출
+ *
+ */
+@Mapper
+public interface CarBassMatterInqireMapper {
+
+ /**
+ * 시퀀스로 새로운 자동차 기본 사항 조회 ID를 생성합니다.
+ *
+ * 형식: CBMI000000000001
+ *
+ * @return 생성된 ID
+ */
+ String selectNextCarBassMatterInqireId();
+
+ /**
+ * 최초 API 요청 정보를 등록합니다.
+ *
+ * 요청 시점의 정보만 저장하며, 응답 정보는 null 상태입니다.
+ *
+ * @param carBassMatterInqireVO 요청 정보
+ * @return 등록된 행 수
+ */
+ int insertCarBassMatterInqire(CarBassMatterInqireVO carBassMatterInqireVO);
+
+ /**
+ * API 응답 결과를 업데이트합니다.
+ *
+ * 응답 받은 데이터를 기존 레코드에 업데이트합니다.
+ *
+ * @param carBassMatterInqireVO 응답 정보 (carBassMatterInqire 필드는 필수)
+ * @return 업데이트된 행 수
+ */
+ int updateCarBassMatterInqire(CarBassMatterInqireVO carBassMatterInqireVO);
+
+ /**
+ * ID로 조회 정보를 조회합니다.
+ *
+ * @param carBassMatterInqire 자동차 기본 사항 조회 ID
+ * @return 조회된 정보
+ */
+ CarBassMatterInqireVO selectCarBassMatterInqireById(String carBassMatterInqire);
+}
diff --git a/src/main/java/go/kr/project/vmis/mapper/CarLedgerFrmbkMapper.java b/src/main/java/go/kr/project/vmis/mapper/CarLedgerFrmbkMapper.java
new file mode 100644
index 0000000..5f72781
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/mapper/CarLedgerFrmbkMapper.java
@@ -0,0 +1,28 @@
+package go.kr.project.vmis.mapper;
+
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkDtlVO;
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 자동차 등록 원부(갑) Mapper
+ */
+@Mapper
+public interface CarLedgerFrmbkMapper {
+
+ // 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에서 사용할 수도 있으나, 여기서는 단건 호출을 반복)
+}
diff --git a/src/main/java/go/kr/project/vmis/model/basic/BasicRequest.java b/src/main/java/go/kr/project/vmis/model/basic/BasicRequest.java
new file mode 100644
index 0000000..69f8f28
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/basic/BasicRequest.java
@@ -0,0 +1,90 @@
+package go.kr.project.vmis.model.basic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Getter
+@Setter
+@Schema(description = "자동차기본사항조회 요청 항목")
+public class BasicRequest {
+
+ // 본문 공통 메타
+ @Schema(description = "정보시스템ID", example = "41-345")
+ @JsonProperty("INFO_SYS_ID")
+ private String infoSysId;
+
+ @Schema(description = "정보시스템IP", example = "105.19.10.135")
+ @JsonProperty("INFO_SYS_IP")
+ private String infoSysIp;
+
+ @Schema(description = "시군구코드", example = "41460")
+ @JsonProperty("SIGUNGU_CODE")
+ private String sigunguCode;
+
+ // 서비스별 필드
+ @Schema(description = "연계정보코드", example = "AC1_FD11_01")
+ @JsonProperty("CNTC_INFO_CODE")
+ private String cntcInfoCode;
+
+ @Schema(description = "담당자ID", example = "")
+ @JsonProperty("CHARGER_ID")
+ private String chargerId;
+
+ @Schema(description = "담당자IP", example = "")
+ @JsonProperty("CHARGER_IP")
+ private String chargerIp;
+
+ @Schema(description = "담당자명(사용자)", example = "")
+ @JsonProperty("CHARGER_NM")
+ private String chargerNm;
+
+ @Schema(description = "부과기준일", example = "20250101")
+ @JsonProperty("LEVY_STDDE")
+ private String levyStdde;
+
+ @Schema(description = "조회구분코드")
+ @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;
+ */
+
+}
diff --git a/src/main/java/go/kr/project/vmis/model/basic/BasicResponse.java b/src/main/java/go/kr/project/vmis/model/basic/BasicResponse.java
new file mode 100644
index 0000000..f7ad01f
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/basic/BasicResponse.java
@@ -0,0 +1,121 @@
+package go.kr.project.vmis.model.basic;
+
+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;
+
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @Schema(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;
+
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/model/basic/CarBassMatterInqireVO.java b/src/main/java/go/kr/project/vmis/model/basic/CarBassMatterInqireVO.java
new file mode 100644
index 0000000..c423893
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/basic/CarBassMatterInqireVO.java
@@ -0,0 +1,646 @@
+package go.kr.project.vmis.model.basic;
+
+import go.kr.project.vmis.config.ApiConstant;
+import go.kr.project.vmis.model.common.Envelope;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 자동차 기본 사항 조회 엔티티
+ *
+ * API 호출 정보를 저장하는 테이블 매핑 클래스입니다.
+ * 최초 요청 시 INSERT, 결과 수신 시 UPDATE 형태로 사용됩니다.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CarBassMatterInqireVO {
+
+ // ==== Static factory/mapping methods (moved from Service) ====
+ public static CarBassMatterInqireVO fromRequest(BasicRequest request) {
+ return CarBassMatterInqireVO.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 CarBassMatterInqireVO fromResponse(String id, Envelope envelope) {
+ if (envelope == null || envelope.getData() == null || envelope.getData().isEmpty()) return null;
+ BasicResponse response = envelope.getData().get(0);
+ CarBassMatterInqireVO.CarBassMatterInqireVOBuilder builder = CarBassMatterInqireVO.builder()
+ .carBassMatterInqire(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(CarBassMatterInqireVO.CarBassMatterInqireVOBuilder 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()) // 이전 차량번호
+ .vhclno(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 carBassMatterInqire;
+
+ // ===== 요청 정보 =====
+ /**
+ * 정보 시스템 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 vhclno;
+
+ /**
+ * 차대번호
+ */
+ 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;
+}
diff --git a/src/main/java/go/kr/project/vmis/model/common/Envelope.java b/src/main/java/go/kr/project/vmis/model/common/Envelope.java
new file mode 100644
index 0000000..5f1699c
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/common/Envelope.java
@@ -0,0 +1,32 @@
+package go.kr.project.vmis.model.common;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 공통 래퍼: { "data": [ ... ] }
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Getter
+@Setter
+public class Envelope {
+
+ @JsonProperty("data")
+ private List data = new ArrayList<>();
+
+ public Envelope() {}
+
+ public Envelope(T single) {
+ if (single != null) this.data.add(single);
+ }
+
+ public Envelope(List data) {
+ this.data = data;
+ }
+
+}
diff --git a/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkDtlVO.java b/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkDtlVO.java
new file mode 100644
index 0000000..722bcb1
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkDtlVO.java
@@ -0,0 +1,73 @@
+package go.kr.project.vmis.model.ledger;
+
+import go.kr.project.vmis.config.ApiConstant;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 자동차 등록 원부(갑) 상세 엔티티 VO
+ *
+ * tb_car_ledger_frmbk_dtl 테이블 매핑
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CarLedgerFrmbkDtlVO {
+
+ // ==== Static factory/mapping methods (moved from Service) ====
+ public static List listFromResponse(LedgerResponse res, String masterId) {
+ List list = new ArrayList<>();
+ if (res == null || res.getRecord() == null) return list;
+ for (LedgerResponse.Record r : res.getRecord()) {
+ CarLedgerFrmbkDtlVO vo = CarLedgerFrmbkDtlVO.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;
+}
diff --git a/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkVO.java b/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkVO.java
new file mode 100644
index 0000000..f760687
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/ledger/CarLedgerFrmbkVO.java
@@ -0,0 +1,192 @@
+package go.kr.project.vmis.model.ledger;
+
+import go.kr.project.vmis.config.ApiConstant;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 자동차 등록 원부(갑) 마스터 엔티티 VO
+ *
+ * tb_car_ledger_frmbk 테이블 매핑
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CarLedgerFrmbkVO {
+
+ // ==== Static factory/mapping methods (moved from Service) ====
+ public static CarLedgerFrmbkVO fromRequest(go.kr.project.vmis.model.ledger.LedgerRequest request) {
+ return CarLedgerFrmbkVO.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 CarLedgerFrmbkVO fromResponseMaster(String id, go.kr.project.vmis.model.ledger.LedgerResponse res) {
+ return CarLedgerFrmbkVO.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;
+}
diff --git a/src/main/java/go/kr/project/vmis/model/ledger/LedgerRequest.java b/src/main/java/go/kr/project/vmis/model/ledger/LedgerRequest.java
new file mode 100644
index 0000000..ce3de6e
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/ledger/LedgerRequest.java
@@ -0,0 +1,82 @@
+package go.kr.project.vmis.model.ledger;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(description = "자동차등록원부(갑) 요청 항목")
+@Getter
+@Setter
+public class LedgerRequest {
+
+ // 본문 공통 메타
+ @Schema(description = "정보시스템ID", example = "41-345")
+ @JsonProperty("INFO_SYS_ID")
+ private String infoSysId;
+
+ @Schema(description = "정보시스템IP", example = "105.19.10.135")
+ @JsonProperty("INFO_SYS_IP")
+ private String infoSysIp;
+
+ @Schema(description = "시군구코드", example = "41460")
+ @JsonProperty("SIGUNGU_CODE")
+ private String sigunguCode;
+
+ // 서비스별 필드
+ @Schema(description = "연계정보코드", example = "AC1_FD11_02")
+ @JsonProperty("CNTC_INFO_CODE")
+ private String cntcInfoCode;
+
+ @Schema(description = "담당자ID", example = "")
+ @JsonProperty("CHARGER_ID")
+ private String chargerId;
+
+ @Schema(description = "담당자IP", example = "")
+ @JsonProperty("CHARGER_IP")
+ private String chargerIp;
+
+ @Schema(description = "담당자명(사용자)", example = "")
+ @JsonProperty("CHARGER_NM")
+ private String chargerNm;
+
+ @Schema(description = "자동차등록번호")
+ @JsonProperty("VHRNO")
+ private String vhrno;
+
+ /*
+ // 추가 항목 (명세 샘플 기준)
+ @Schema(description = "개인정보공개")
+ @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;
+
+ @Schema(description = "조회구분코드")
+ @JsonProperty("INQIRE_SE_CODE")
+ private String inqireSeCode;
+ */
+
+}
diff --git a/src/main/java/go/kr/project/vmis/model/ledger/LedgerResponse.java b/src/main/java/go/kr/project/vmis/model/ledger/LedgerResponse.java
new file mode 100644
index 0000000..fff62de
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/model/ledger/LedgerResponse.java
@@ -0,0 +1,250 @@
+package go.kr.project.vmis.model.ledger;
+
+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;
+
+ @JsonProperty("SPMNNO1")
+ private String spmnno1;
+
+ @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;
+
+ @JsonProperty("ADRES1")
+ private String adres1;
+
+ @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;
+
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @Schema(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; }
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireLogService.java b/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireLogService.java
new file mode 100644
index 0000000..7fedbe7
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireLogService.java
@@ -0,0 +1,56 @@
+package go.kr.project.vmis.service;
+
+import go.kr.project.vmis.mapper.CarBassMatterInqireMapper;
+import go.kr.project.vmis.model.basic.CarBassMatterInqireVO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 자동차 기본사항 조회 로그 전용 서비스.
+ *
+ * 로그 적재만 별도 트랜잭션(REQUIRES_NEW)으로 처리하여,
+ * 외부 호출 실패나 상위 트랜잭션 롤백 상황에서도 로그는 영속화되도록 보장한다.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CarBassMatterInqireLogService {
+
+ private final CarBassMatterInqireMapper carBassMatterInqireMapper;
+
+ /**
+ * 최초 API 요청 정보를 등록한다. (REQUIRES_NEW)
+ * @param request 요청 정보
+ * @return 생성된 ID
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public String createInitialRequestNewTx(CarBassMatterInqireVO request) {
+ String generatedId = carBassMatterInqireMapper.selectNextCarBassMatterInqireId();
+ request.setCarBassMatterInqire(generatedId);
+ int result = carBassMatterInqireMapper.insertCarBassMatterInqire(request);
+ if (result != 1) {
+ throw new RuntimeException("자동차 기본 사항 조회 정보 등록 실패");
+ }
+ log.info("[BASIC-REQ-LOG] 요청 정보 저장 완료(별도TX) - ID: {}, 차량번호: {}", generatedId, request.getDmndVhrno());
+ return generatedId;
+ }
+
+ /**
+ * 응답/에러 결과를 업데이트한다. (REQUIRES_NEW)
+ * @param response 업데이트 내용
+ */
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public void updateResponseNewTx(CarBassMatterInqireVO response) {
+ if (response.getCarBassMatterInqire() == null) {
+ throw new IllegalArgumentException("자동차 기본 사항 조회 ID는 필수입니다.");
+ }
+ int result = carBassMatterInqireMapper.updateCarBassMatterInqire(response);
+ if (result != 1) {
+ throw new RuntimeException("자동차 기본 사항 조회 정보 업데이트 실패 - ID: " + response.getCarBassMatterInqire());
+ }
+ log.info("[BASIC-RES-LOG] 응답/에러 정보 저장 완료(별도TX) - ID: {}, 결과코드: {}", response.getCarBassMatterInqire(), response.getCntcResultCode());
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireService.java b/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireService.java
new file mode 100644
index 0000000..4051f95
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/service/CarBassMatterInqireService.java
@@ -0,0 +1,87 @@
+package go.kr.project.vmis.service;
+
+import go.kr.project.vmis.client.GovernmentApi;
+import go.kr.project.vmis.config.ApiConstant;
+import go.kr.project.vmis.util.ExceptionDetailUtil;
+import go.kr.project.vmis.model.basic.BasicRequest;
+import go.kr.project.vmis.model.basic.BasicResponse;
+import go.kr.project.vmis.model.basic.CarBassMatterInqireVO;
+import go.kr.project.vmis.model.common.Envelope;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 자동차 기본 사항 조회 서비스
+ *
+ * API 호출 정보를 관리하는 서비스 클래스입니다.
+ *
+ * 최초 요청: createInitialRequest() - 시퀀스로 ID 생성 후 INSERT
+ * 결과 업데이트: updateResponse() - 응답 데이터 UPDATE
+ *
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CarBassMatterInqireService {
+
+ private final GovernmentApi governmentApi;
+ private final RequestEnricher enricher;
+ private final CarBassMatterInqireLogService logService;
+
+
+
+
+ /**
+ * 자동차 기본사항 조회: 보강 -> 최초요청로그 -> 외부호출 -> 응답로그.
+ */
+ @Transactional
+ public ResponseEntity> basic(Envelope envelope) {
+ // 1) 요청 보강
+ enricher.enrichBasic(envelope);
+
+ String generatedId = null;
+ try {
+ // 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
+ if (envelope.getData() != null && !envelope.getData().isEmpty()) {
+ BasicRequest req = envelope.getData().get(0);
+ CarBassMatterInqireVO logEntity = CarBassMatterInqireVO.fromRequest(req);
+ generatedId = logService.createInitialRequestNewTx(logEntity);
+ }
+
+ // 3) 외부 API 호출
+ ResponseEntity> response = governmentApi.callBasic(envelope);
+
+ // 4) 응답 로그 업데이트
+ // 원본 소스, 정상적인 호출, 리턴(에러 리턴포함) 일 경우에만 에러 로그 남김
+ if (generatedId != null && response.getBody() != null) {
+ CarBassMatterInqireVO update = CarBassMatterInqireVO.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);
+ CarBassMatterInqireVO errorLog = CarBassMatterInqireVO.builder()
+ .carBassMatterInqire(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;
+ }
+ }
+
+}
diff --git a/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkLogService.java b/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkLogService.java
new file mode 100644
index 0000000..2b4a2dd
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkLogService.java
@@ -0,0 +1,61 @@
+package go.kr.project.vmis.service;
+
+import go.kr.project.vmis.mapper.CarLedgerFrmbkMapper;
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkDtlVO;
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkVO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 자동차 등록 원부(갑) 로그 전용 서비스.
+ * - 모든 로깅(write)은 별도 트랜잭션(REQUIRES_NEW)으로 수행하여 상위 트랜잭션 롤백의 영향을 받지 않도록 한다.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CarLedgerFrmbkLogService {
+
+ private final CarLedgerFrmbkMapper mapper;
+
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public String createInitialRequestNewTx(CarLedgerFrmbkVO request) {
+ String id = mapper.selectNextCarLedgerFrmbkId();
+ request.setCarLedgerFrmbkId(id);
+ int result = mapper.insertCarLedgerFrmbk(request);
+ if (result != 1) {
+ throw new RuntimeException("자동차 등록 원부(갑) 최초요청 등록 실패");
+ }
+ log.info("[LEDGER-REQ-LOG] 최초 요청 저장(별도TX) - ID: {}, 차량번호: {}", id, request.getDmndVhrno());
+ return id;
+ }
+
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ 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());
+ }
+ log.info("[LEDGER-RES-LOG] 마스터 응답 업데이트(별도TX) - ID: {}, 결과코드: {}",
+ response.getCarLedgerFrmbkId(), response.getCntcResultCode());
+ }
+
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public void saveDetailsNewTx(String masterId, List details) {
+ if (details == null || details.isEmpty()) return;
+ for (CarLedgerFrmbkDtlVO dtl : details) {
+ String dtlId = mapper.selectNextCarLedgerFrmbkDtlId();
+ dtl.setCarLedgerFrmbkDtlId(dtlId);
+ dtl.setCarLedgerFrmbkId(masterId);
+ mapper.insertCarLedgerFrmbkDtl(dtl);
+ }
+ log.info("[LEDGER-RES-LOG] 상세 {}건 저장(별도TX) - ID: {}", details.size(), masterId);
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkService.java b/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkService.java
new file mode 100644
index 0000000..e2b9cc7
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/service/CarLedgerFrmbkService.java
@@ -0,0 +1,85 @@
+package go.kr.project.vmis.service;
+
+import go.kr.project.vmis.client.GovernmentApi;
+import go.kr.project.vmis.config.ApiConstant;
+import go.kr.project.vmis.model.common.Envelope;
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkDtlVO;
+import go.kr.project.vmis.model.ledger.CarLedgerFrmbkVO;
+import go.kr.project.vmis.model.ledger.LedgerRequest;
+import go.kr.project.vmis.model.ledger.LedgerResponse;
+import go.kr.project.vmis.util.ExceptionDetailUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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 CarLedgerFrmbkService {
+
+ private final GovernmentApi governmentApi;
+ private final RequestEnricher enricher;
+ private final CarLedgerFrmbkLogService logService;
+
+ /**
+ * 자동차 등록원부(갑) 조회: 보강 -> 최초요청로그(별도TX) -> 외부호출 -> 응답로그(마스터/상세, 별도TX) -> 오류 시 에러로그(별도TX).
+ */
+ @Transactional
+ public ResponseEntity> ledger(Envelope envelope) {
+ // 1) 요청 보강
+ enricher.enrichLedger(envelope);
+
+ String generatedId = null;
+ try {
+ // 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
+ if (envelope.getData() != null && !envelope.getData().isEmpty()) {
+ LedgerRequest req = envelope.getData().get(0);
+ CarLedgerFrmbkVO init = CarLedgerFrmbkVO.fromRequest(req);
+ generatedId = logService.createInitialRequestNewTx(init);
+ }
+
+ // 3) 외부 API 호출
+ ResponseEntity> 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);
+ CarLedgerFrmbkVO masterUpdate = CarLedgerFrmbkVO.fromResponseMaster(generatedId, body);
+ logService.updateResponseNewTx(masterUpdate);
+
+ List details = CarLedgerFrmbkDtlVO.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);
+ CarLedgerFrmbkVO errorLog = CarLedgerFrmbkVO.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;
+ }
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/service/RequestEnricher.java b/src/main/java/go/kr/project/vmis/service/RequestEnricher.java
new file mode 100644
index 0000000..ef0f37f
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/service/RequestEnricher.java
@@ -0,0 +1,63 @@
+package go.kr.project.vmis.service;
+
+import go.kr.project.vmis.config.properties.VmisProperties;
+import go.kr.project.vmis.model.basic.BasicRequest;
+import go.kr.project.vmis.model.common.Envelope;
+import go.kr.project.vmis.model.ledger.LedgerRequest;
+import org.springframework.stereotype.Component;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 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 RequestEnricher {
+
+ private final VmisProperties props;
+
+ public RequestEnricher(VmisProperties props) {
+ this.props = props;
+ }
+
+ public void enrichBasic(Envelope 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.getRegionCode());
+ req.setCntcInfoCode(cntc);
+ req.setChargerId(sys.getChargerId());
+ req.setChargerIp(sys.getChargerIp());
+ req.setChargerNm(sys.getChargerNm());
+ }
+ log.debug("[ENRICH] basic: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
+ sys.getInfoSysId(), sys.getInfoSysIp(), sys.getRegionCode(), cntc);
+ }
+
+ public void enrichLedger(Envelope 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.getRegionCode());
+ req.setCntcInfoCode(cntc);
+ req.setChargerId(sys.getChargerId());
+ req.setChargerIp(sys.getChargerIp());
+ req.setChargerNm(sys.getChargerNm());
+ }
+ log.debug("[ENRICH] ledger: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
+ sys.getInfoSysId(), sys.getInfoSysIp(), sys.getRegionCode(), cntc);
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/util/ExceptionDetailUtil.java b/src/main/java/go/kr/project/vmis/util/ExceptionDetailUtil.java
new file mode 100644
index 0000000..0a7ab04
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/util/ExceptionDetailUtil.java
@@ -0,0 +1,29 @@
+package go.kr.project.vmis.util;
+
+/**
+ * Common helper to extract root-cause message and truncate to DB column limit (default 4000 chars).
+ */
+public final class ExceptionDetailUtil {
+
+ private ExceptionDetailUtil() {}
+
+ public static String buildForLog(Throwable t) {
+ return buildForLog(t, 4000);
+ }
+
+ public static String buildForLog(Throwable t, int maxLen) {
+ if (t == null) return "오류: unknown";
+ Throwable root = t;
+ int guard = 0;
+ while (root.getCause() != null && root.getCause() != root && guard++ < 20) {
+ root = root.getCause();
+ }
+ String className = root.getClass().getName();
+ String message = root.getMessage();
+ String detail = (message != null && !message.isEmpty()) ? (className + ": " + message) : root.toString();
+ if (detail != null && detail.length() > maxLen) {
+ detail = detail.substring(0, maxLen);
+ }
+ return detail;
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/util/GpkiCryptoUtil.java b/src/main/java/go/kr/project/vmis/util/GpkiCryptoUtil.java
new file mode 100644
index 0000000..39da89a
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/util/GpkiCryptoUtil.java
@@ -0,0 +1,98 @@
+package go.kr.project.vmis.util;
+
+import go.kr.project.vmis.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).");
+ }
+ }
+}
diff --git a/src/main/java/go/kr/project/vmis/util/NewGpkiUtil.java b/src/main/java/go/kr/project/vmis/util/NewGpkiUtil.java
new file mode 100644
index 0000000..3d657d9
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/util/NewGpkiUtil.java
@@ -0,0 +1,383 @@
+package go.kr.project.vmis.util;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.gpki.gpkiapi_jni;
+import com.gpki.gpkiapi.GpkiApi;
+import com.gpki.gpkiapi.cert.X509Certificate;
+import com.gpki.gpkiapi.crypto.PrivateKey;
+import com.gpki.gpkiapi.storage.Disk;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class NewGpkiUtil {
+ byte[] myEnvCert, myEnvKey, mySigCert, mySigKey;
+ private Map targetServerCertMap = new HashMap();
+
+ // 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;
+ }
+
+}
diff --git a/src/main/java/go/kr/project/vmis/util/TxIdUtil.java b/src/main/java/go/kr/project/vmis/util/TxIdUtil.java
new file mode 100644
index 0000000..aa4a74c
--- /dev/null
+++ b/src/main/java/go/kr/project/vmis/util/TxIdUtil.java
@@ -0,0 +1,18 @@
+package go.kr.project.vmis.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;
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index ce485c8..187057d 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -29,7 +29,9 @@ spring:
# 최종적으로 datasource 와 mybatis 연결
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
- mapper-locations: classpath:mybatis/mapper/**/*_${Globals.DbType}.xml
+ mapper-locations:
+ - classpath:mybatis/mapper/**/*_${Globals.DbType}.xml
+ - classpath:mybatis/mapper/vmis/**/*_${Globals.DbType}.xml
type-aliases-package: go.kr.project.**.model,egovframework.**.model
# Springdoc OpenAPI 설정
@@ -99,3 +101,62 @@ interceptor:
- /common/** #공통 페이지
- /swagger-ui/** #Swagger UI
- /v3/api-docs/** #Swagger API 문서
+
+# ===== VMIS 통합 설정 =====
+# 차량 정보 조회 모드 설정
+vmis:
+ integration:
+ mode: internal # internal: 내부 VMIS 모듈 직접 호출, external: 외부 REST API 호출 (기본값: external)
+
+ # Internal Mode 설정 (내부 VMIS 모듈 사용 시)
+ system:
+ infoSysId: "41-345" # 정보시스템 ID
+ infoSysIp: "${SERVER_IP:105.19.10.135}" # 시스템 IP
+ regionCode: "41460" # 지역 코드
+ departmentCode: "" # 부서 코드
+ chargerId: "" # 담당자 ID
+ chargerIp: "" # 담당자 IP
+ chargerNm: "" # 담당자명
+
+ # GPKI 암호화 설정 (운영 환경에서만 사용)
+ gpki:
+ enabled: "N" # Y: 암호화 사용, N: 암호화 미사용 (개발 환경: N, 운영 환경: Y)
+ useSign: true
+ charset: "UTF-8"
+ certServerId: "SVR5640020001"
+ targetServerId: "SVR1500000015"
+ ldap: true
+ gpkiLicPath: "src/GPKI/conf"
+ certFilePath: "src/GPKI/certs"
+ envCertFilePathName: "src/GPKI/certs/SVR5640020001_env.cer"
+ envPrivateKeyFilePathName: "src/GPKI/certs/SVR5640020001_env.key"
+ envPrivateKeyPasswd: "${GPKI_ENV_PASSWORD:*sbm204221}"
+ sigCertFilePathName: "src/GPKI/certs/SVR5640020001_sig.cer"
+ sigPrivateKeyFilePathName: "src/GPKI/certs/SVR5640020001_sig.key"
+ sigPrivateKeyPasswd: "${GPKI_SIG_PASSWORD:*sbm204221}"
+
+ # 정부 API 연동 설정
+ gov:
+ scheme: "http"
+ host: "10.188.225.94:29001" # 개발(DEV) 행정망
+ basePath: "/piss/api/molit"
+ connectTimeoutMillis: 5000
+ readTimeoutMillis: 10000
+ services:
+ basic: # 시군구연계 자동차기본사항조회
+ path: "/SignguCarBassMatterInqireService"
+ cntcInfoCode: "AC1_FD11_01"
+ apiKey: "${GOV_API_KEY_BASIC:05e8d748fb366a0831dce71a32424460746a72d591cf483ccc130534dd51e394}"
+ cvmisApikey: "${GOV_CVMIS_API_KEY_BASIC:014F9215-B6D9A3B6-4CED5225-68408C46}"
+ ledger: # 시군구연계 자동차등록원부(갑)
+ path: "/SignguCarLedgerFrmbkService"
+ cntcInfoCode: "AC1_FD11_02"
+ apiKey: "${GOV_API_KEY_LEDGER:1beeb01857c2e7e9b41c002b007ccb9754d9c272f66d4bb64fc45b302c69e529}"
+ cvmisApikey: "${GOV_CVMIS_API_KEY_LEDGER:63DF159B-7B9C64C5-86CCB15C-5F93E750}"
+
+ # External Mode 설정 (외부 REST API 사용 시)
+ external:
+ api:
+ url: "http://localhost:8081/api/v1/vehicles" # VMIS-interface 서버 URL
+ connectTimeoutMillis: 5000
+ readTimeoutMillis: 10000
diff --git a/src/main/resources/mybatis/mapper/vmis/CarBassMatterInqireMapper_maria.xml b/src/main/resources/mybatis/mapper/vmis/CarBassMatterInqireMapper_maria.xml
new file mode 100644
index 0000000..a322e25
--- /dev/null
+++ b/src/main/resources/mybatis/mapper/vmis/CarBassMatterInqireMapper_maria.xml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+ SELECT CONCAT('CBMI', LPAD(NEXTVAL(seq_car_bass_matter_inqire), 16, '0')) AS id
+
+
+
+
+ INSERT INTO tb_car_bass_matter_inqire (
+ CAR_BASS_MATTER_INQIRE,
+ INFO_SYS_ID,
+ INFO_SYS_IP,
+ SIGUNGU_CODE,
+ CNTC_INFO_CODE,
+ CHARGER_ID,
+ CHARGER_IP,
+ CHARGER_NM,
+ DMND_LEVY_STDDE,
+ DMND_INQIRE_SE_CODE,
+ DMND_VHRNO,
+ DMND_VIN,
+ REG_DT,
+ RGTR
+ ) VALUES (
+ #{carBassMatterInqire},
+ #{infoSysId},
+ #{infoSysIp},
+ #{sigunguCode},
+ #{cntcInfoCode},
+ #{chargerId},
+ #{chargerIp},
+ #{chargerNm},
+ #{dmndLevyStdde},
+ #{dmndInqireSeCode},
+ #{dmndVhrno},
+ #{dmndVin},
+ NOW(),
+ #{rgtr}
+ )
+
+
+
+
+ UPDATE tb_car_bass_matter_inqire
+
+ CNTC_RESULT_CODE = #{cntcResultCode},
+ CNTC_RESULT_DTLS = #{cntcResultDtls},
+ PRYE = #{prye},
+ REGIST_DE = #{registDe},
+ ERSR_REGIST_SE_CODE = #{ersrRegistSeCode},
+ ERSR_REGIST_SE_NM = #{ersrRegistSeNm},
+ ERSR_REGIST_DE = #{ersrRegistDe},
+ REGIST_DETAIL_CODE = #{registDetailCode},
+ DSPLVL = #{dsplvl},
+ USE_STRNGHLD_LEGALDONG_CODE = #{useStrnghldLegaldongCode},
+ USE_STRNGHLD_ADSTRD_CODE = #{useStrnghldAdstrdCode},
+ USE_STRNGHLD_MNTN = #{useStrnghldMntn},
+ USE_STRNGHLD_LNBR = #{useStrnghldLnbr},
+ USE_STRNGHLD_HO = #{useStrnghldHo},
+ USE_STRNGHLD_ADRES_NM = #{useStrnghldAdresNm},
+ USE_STRNGHLD_ROAD_NM_CODE = #{useStrnghldRoadNmCode},
+ USGSRHLD_UNDGRND_BULD_SE_CODE = #{usgsrhldUndgrndBuldSeCode},
+ USE_STRNGHLD_BULD_MAIN_NO = #{useStrnghldBuldMainNo},
+ USE_STRNGHLD_BULD_SUB_NO = #{useStrnghldBuldSubNo},
+ USGSRHLD_ADRES_FULL = #{usgsrhldAdresFull},
+ MBER_SE_CODE = #{mberSeCode},
+ MBER_SE_NO = #{mberSeNo},
+ TELNO = #{telno},
+ OWNER_LEGALDONG_CODE = #{ownerLegaldongCode},
+ OWNER_ADSTRD_CODE = #{ownerAdstrdCode},
+ OWNER_MNTN = #{ownerMntn},
+ OWNER_LNBR = #{ownerLnbr},
+ OWNER_HO = #{ownerHo},
+ OWNER_ADRES_NM = #{ownerAdresNm},
+ OWNER_ROAD_NM_CODE = #{ownerRoadNmCode},
+ OWNER_UNDGRND_BULD_SE_CODE = #{ownerUndgrndBuldSeCode},
+ OWNER_BULD_MAIN_NO = #{ownerBuldMainNo},
+ OWNER_BULD_SUB_NO = #{ownerBuldSubNo},
+ OWNR_WHOLADDR = #{ownrWholaddr},
+ AFTR_VHRNO = #{aftrVhrno},
+ USE_FUEL_CODE = #{useFuelCode},
+ PRPOS_SE_CODE = #{prposSeCode},
+ MTRS_FOM_NM = #{mtrsFomNm},
+ FRNT_VHRNO = #{frntVhrno},
+ VHCLNO = #{vhclno},
+ VIN = #{vin},
+ CNM = #{cnm},
+ VHCLE_TOT_WT = #{vhcleTotWt},
+ CAAG_ENDDE = #{caagEndde},
+ CHANGE_DE = #{changeDe},
+ VHCTY_ASORT_CODE = #{vhctyAsortCode},
+ VHCTY_TY_CODE = #{vhctyTyCode},
+ VHCTY_SE_CODE = #{vhctySeCode},
+ MXMM_LDG = #{mxmmLdg},
+ VHCTY_ASORT_NM = #{vhctyAsortNm},
+ VHCTY_TY_NM = #{vhctyTyNm},
+ VHCTY_SE_NM = #{vhctySeNm},
+ FRST_REGIST_DE = #{frstRegistDe},
+ FOM_NM = #{fomNm},
+ ACQS_DE = #{acqsDe},
+ ACQS_END_DE = #{acqsEndDe},
+ YBL_MD = #{yblMd},
+ TRANSR_REGIST_DE = #{transrRegistDe},
+ SPCF_REGIST_STTUS_CODE = #{spcfRegistSttusCode},
+ COLOR_NM = #{colorNm},
+ MRTG_CO = #{mrtgCo},
+ SEIZR_CO = #{seizrCo},
+ STMD_CO = #{stmdCo},
+ NMPL_CSDY_AT = #{nmplCsdyAt},
+ NMPL_CSDY_REMNR_DE = #{nmplCsdyRemnrDe},
+ ORIGIN_SE_CODE = #{originSeCode},
+ NMPL_STNDRD_CODE = #{nmplStndrdCode},
+ ACQS_AMOUNT = #{acqsAmount},
+ INSPT_VALID_PD_BGNDE = #{insptValidPdBgnde},
+ INSPT_VALID_PD_ENDDE = #{insptValidPdEndde},
+ USE_STRNGHLD_GRC_CODE = #{useStrnghldGrcCode},
+ TKCAR_PSCAP_CO = #{tkcarPscapCo},
+ SPMNNO = #{spmnno},
+ TRVL_DSTNC = #{trvlDstnc},
+ FRST_REGIST_RQRCNO = #{frstRegistRqrcno},
+ VLNT_ERSR_PRVNTC_NTICE_DE = #{vlntErsrPrvntcNticeDe},
+ REGIST_INSTT_NM = #{registInsttNm},
+ PROCESS_IMPRTY_RESN_CODE = #{processImprtyResnCode},
+ PROCESS_IMPRTY_RESN_DTLS = #{processImprtyResnDtls},
+ CBD_LT = #{cbdLt},
+ CBD_BT = #{cbdBt},
+ CBD_HG = #{cbdHg},
+ FRST_MXMM_LDG = #{frstMxmmLdg},
+ FUEL_CNSMP_RT = #{fuelCnsmpRt},
+ ELCTY_CMPND_FUEL_CNSMP_RT = #{elctyCmpndFuelCnsmpRt},
+ MBER_NM = #{mberNm},
+
+ WHERE CAR_BASS_MATTER_INQIRE = #{carBassMatterInqire}
+
+
+
+
+ SELECT *
+ FROM tb_car_bass_matter_inqire
+ WHERE CAR_BASS_MATTER_INQIRE = #{carBassMatterInqire}
+
+
+
diff --git a/src/main/resources/mybatis/mapper/vmis/CarLedgerFrmbkMapper_maria.xml b/src/main/resources/mybatis/mapper/vmis/CarLedgerFrmbkMapper_maria.xml
new file mode 100644
index 0000000..ae829a0
--- /dev/null
+++ b/src/main/resources/mybatis/mapper/vmis/CarLedgerFrmbkMapper_maria.xml
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+ SELECT CONCAT('CLFB', LPAD(NEXTVAL(seq_car_ledger_frmbk), 16, '0')) AS id
+
+
+
+ SELECT CONCAT('CLFD', LPAD(NEXTVAL(seq_car_ledger_frmbk_dtl), 16, '0')) AS id
+
+
+
+
+ INSERT INTO tb_car_ledger_frmbk (
+ CAR_LEDGER_FRMBK_ID,
+ INFO_SYS_ID,
+ INFO_SYS_IP,
+ SIGUNGU_CODE,
+ CNTC_INFO_CODE,
+ CHARGER_ID,
+ CHARGER_IP,
+ CHARGER_NM,
+ DMND_VHRNO,
+ DMND_ONES_INFORMATION_OPEN,
+ DMND_CPTTR_NM,
+ DMND_CPTTR_IHIDNUM,
+ DMND_CPTTR_LEGALDONG_CODE,
+ DMND_ROUTE_SE_CODE,
+ DMND_DETAIL_EXPRESSION,
+ DMND_INQIRE_SE_CODE,
+ REG_DT,
+ RGTR
+ ) VALUES (
+ #{carLedgerFrmbkId},
+ #{infoSysId},
+ #{infoSysIp},
+ #{sigunguCode},
+ #{cntcInfoCode},
+ #{chargerId},
+ #{chargerIp},
+ #{chargerNm},
+ #{dmndVhrno},
+ #{dmndOnesInformationOpen},
+ #{dmndCpttrNm},
+ #{dmndCpttrIhidnum},
+ #{dmndCpttrLegaldongCode},
+ #{dmndRouteSeCode},
+ #{dmndDetailExpression},
+ #{dmndInqireSeCode},
+ NOW(),
+ #{rgtr}
+ )
+
+
+
+
+ UPDATE tb_car_ledger_frmbk
+
+ CNTC_RESULT_CODE = #{cntcResultCode},
+ CNTC_RESULT_DTLS = #{cntcResultDtls},
+ LEDGER_GROUP_NO = #{ledgerGroupNo},
+ LEDGER_INDVDLZ_NO = #{ledgerIndvdlzNo},
+ VHMNO = #{vhmno},
+ VHRNO = #{vhrno},
+ VIN = #{vin},
+ VHCTY_ASORT_CODE = #{vhctyAsortCode},
+ VHCTY_ASORT_NM = #{vhctyAsortNm},
+ CNM = #{cnm},
+ COLOR_CODE = #{colorCode},
+ COLOR_NM = #{colorNm},
+ NMPL_STNDRD_CODE = #{nmplStndrdCode},
+ NMPL_STNDRD_NM = #{nmplStndrdNm},
+ PRPOS_SE_CODE = #{prposSeCode},
+ PRPOS_SE_NM = #{prposSeNm},
+ MTRS_FOM_NM = #{mtrsFomNm},
+ FOM_NM = #{fomNm},
+ ACQS_AMOUNT = #{acqsAmount},
+ REGIST_DETAIL_CODE = #{registDetailCode},
+ REGIST_DETAIL_NM = #{registDetailNm},
+ FRST_REGIST_DE = #{frstRegistDe},
+ CAAG_ENDDE = #{caagEndde},
+ PRYE = #{prye},
+ SPMNNO1 = #{spmnno1},
+ SPMNNO2 = #{spmnno2},
+ YBL_MD = #{yblMd},
+ TRVL_DSTNC = #{trvlDstnc},
+ INSPT_VALID_PD_BGNDE = #{insptValidPdBgnde},
+ INSPT_VALID_PD_ENDDE = #{insptValidPdEndde},
+ CHCK_VALID_PD_BGNDE = #{chckValidPdBgnde},
+ CHCK_VALID_PD_ENDDE = #{chckValidPdEndde},
+ REGIST_REQST_SE_NM = #{registReqstSeNm},
+ FRST_REGIST_RQRCNO = #{frstRegistRqrcno},
+ NMPL_CSDY_REMNR_DE = #{nmplCsdyRemnrDe},
+ NMPL_CSDY_AT = #{nmplCsdyAt},
+ BSS_USE_PD = #{bssUsePd},
+ OCTHT_ERSR_PRVNTC_NTICE_DE = #{octhtErsrPrvntcNticeDe},
+ ERSR_REGIST_DE = #{ersrRegistDe},
+ ERSR_REGIST_SE_CODE = #{ersrRegistSeCode},
+ ERSR_REGIST_SE_NM = #{ersrRegistSeNm},
+ MRTGCNT = #{mrtgcnt},
+ VHCLECNT = #{vhclecnt},
+ STMDCNT = #{stmdcnt},
+ ADRES1 = #{adres1},
+ ADRES_NM1 = #{adresNm1},
+ ADRES = #{adres},
+ ADRES_NM = #{adresNm},
+ INDVDL_BSNM_AT = #{indvdlBsnmAt},
+ TELNO = #{telno},
+ MBER_NM = #{mberNm},
+ MBER_SE_CODE = #{mberSeCode},
+ MBER_SE_NO = #{mberSeNo},
+ TAXXMPT_TRGTER_SE_CODE = #{taxxmptTrgterSeCode},
+ TAXXMPT_TRGTER_SE_CODE_NM = #{taxxmptTrgterSeCodeNm},
+ CNT_MATTER = #{cntMatter},
+ EMD_NM = #{emdNm},
+ PRVNTCCNT = #{prvntccnt},
+ XPORT_FLFL_AT_STTEMNT_DE = #{xportFlflAtSttemntDe},
+ PARTN_RQRCNO = #{partnRqrcno},
+ FRST_TRNSFR_DE = #{frstTrnsfrDe},
+ PROCESS_IMPRTY_RESN_CODE = #{processImprtyResnCode},
+ PROCESS_IMPRTY_RESN_DTLS = #{processImprtyResnDtls},
+
+ WHERE CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId}
+
+
+
+
+ INSERT INTO tb_car_ledger_frmbk_dtl (
+ CAR_LEDGER_FRMBK_DTL_ID,
+ CAR_LEDGER_FRMBK_ID,
+ MAINCHK,
+ CHANGE_JOB_SE_CODE,
+ MAINNO,
+ SUBNO,
+ DTLS,
+ RQRCNO,
+ VHMNO,
+ LEDGER_GROUP_NO,
+ LEDGER_INDVDLZ_NO,
+ GUBUN_NM,
+ CHANGE_DE,
+ DETAIL_SN,
+ FLAG,
+ REG_DT,
+ RGTR
+ ) VALUES (
+ #{carLedgerFrmbkDtlId},
+ #{carLedgerFrmbkId},
+ #{mainchk},
+ #{changeJobSeCode},
+ #{mainno},
+ #{subno},
+ #{dtls},
+ #{rqrcno},
+ #{vhmno},
+ #{ledgerGroupNo},
+ #{ledgerIndvdlzNo},
+ #{gubunNm},
+ #{changeDe},
+ #{detailSn},
+ #{flag},
+ NOW(),
+ #{rgtr}
+ )
+
+
+
+
+ SELECT *
+ FROM tb_car_ledger_frmbk
+ WHERE CAR_LEDGER_FRMBK_ID = #{carLedgerFrmbkId}
+
+
+