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_ynGPKI 암호화 사용 여부 (Y/N)Y필수
tx_id트랜잭션 고유 ID (요청 추적용)20250104123045_abc123필수
cert_server_id인증서 서버 식별자VMIS_SERVER_01필수
api_key서비스별 API 인증 키abc123def456...필수
cvmis_apikeyCVMIS 시스템 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에 로그성 데이터 저장
  • + *
+ * + *

처리 흐름:

+ *
    + *
  1. 요청 정보를 DB에 INSERT (로그 저장)
  2. + *
  3. 정부 API 호출
  4. + *
  5. 응답 정보를 DB에 UPDATE
  6. + *
  7. 에러 발생 시 에러 정보도 DB에 UPDATE
  8. + *
+ * + *

사용 예시:

+ *
+     * 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)
  • + *
  • 타입 안전성을 보장하여 런타임 에러 방지
  • + *
+ * + *

처리 흐름 (상세):

+ *
    + *
  1. 설정 로드: + *
    • 서비스 타입에 따라 BASIC 또는 LEDGER 설정 선택
    + *
  2. + *
  3. URL 및 트랜잭션 ID 구성: + *
    • 완전한 API URL 생성
    • + *
    • 고유 트랜잭션 ID 생성
    + *
  4. + *
  5. 직렬화 (Serialization): + *
    • Java 객체(Envelope<TReq>)를 JSON 문자열로 변환
    • + *
    • ObjectMapper.writeValueAsString() 사용
    + *
  6. + *
  7. 헤더 구성: + *
    • buildHeaders() 메서드 호출
    • + *
    • 모든 필수 헤더 추가
    + *
  8. + *
  9. GPKI 암호화 (선택적): + *
    • GPKI가 활성화된 경우 JSON을 암호화
    • + *
    • gpkiEncrypt() 메서드 호출
    + *
  10. + *
  11. HTTP 요청 전송: + *
    • RestTemplate.exchange()로 POST 요청
    • + *
    • 요청 로그 기록
    + *
  12. + *
  13. GPKI 복호화 (선택적): + *
    • 성공 응답(2xx)이고 GPKI가 활성화된 경우
    • + *
    • gpkiDecrypt() 메서드 호출
    + *
  14. + *
  15. 역직렬화 (Deserialization): + *
    • JSON 문자열을 Java 객체(Envelope<TResp>)로 변환
    • + *
    • TypeReference를 사용하여 제네릭 타입 정보 보존
    + *
  16. + *
  17. 응답 반환: + *
    • ResponseEntity로 감싸서 HTTP 정보 포함
    + *
  18. + *
+ * + *

에러 처리 전략 (3단계):

+ *
    + *
  1. HttpStatusCodeException (HTTP 에러): + *
      + *
    • 정부 API가 4xx 또는 5xx 상태 코드를 반환한 경우
    • + *
    • 에러 응답 바디를 파싱하여 Envelope 객체로 변환 시도
    • + *
    • 파싱 실패 시 빈 Envelope 객체 반환
    • + *
    • 에러 로그 기록 (WARN 레벨)
    • + *
    + *
  2. + *
  3. JSON 파싱 에러: + *
      + *
    • 응답 JSON이 예상한 형식과 다른 경우
    • + *
    • RuntimeException으로 래핑하여 상위로 전파
    • + *
    + *
  4. + *
  5. 기타 예외: + *
      + *
    • 네트워크 타임아웃, 연결 실패 등
    • + *
    • RuntimeException으로 래핑하여 상위로 전파
    • + *
    + *
  6. + *
+ * + *

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을 발생시킵니다.

+ * + *

암호화 과정:

+ *
    + *
  1. 평문 JSON 문자열을 바이트 배열로 변환
  2. + *
  3. 정부 시스템의 공개키를 사용하여 암호화
  4. + *
  5. 암호화된 바이트 배열을 Base64로 인코딩
  6. + *
  7. Base64 문자열 반환
  8. + *
+ * + *

에러 처리:

+ *
    + *
  • 인증서 오류: 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을 발생시킵니다.

+ * + *

복호화 과정:

+ *
    + *
  1. Base64로 인코딩된 암호문을 바이트 배열로 디코딩
  2. + *
  3. 우리 시스템의 개인키를 사용하여 복호화
  4. + *
  5. 복호화된 바이트 배열을 UTF-8 문자열로 변환
  6. + *
  7. 평문 JSON 문자열 반환
  8. + *
+ * + *

에러 처리:

+ *
    + *
  • 인증서 오류: 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입니다.

+ * + *

요청 처리 흐름:

+ *
    + *
  1. 클라이언트로부터 JSON 형식의 요청 수신
  2. + *
  3. 요청 바디가 Envelope<BasicRequest> 객체로 역직렬화됨
  4. + *
  5. Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
  6. + *
  7. 변환된 요청 객체를 GovernmentApiClient로 전달
  8. + *
  9. GovernmentApiClient가 정부 시스템과 통신 수행
  10. + *
  11. 응답 데이터를 Envelope<BasicResponse>로 감싸서 반환
  12. + *
  13. Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
  14. + *
+ * + *

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입니다. + * 등록원부는 자동차의 법적 소유관계와 등록사항을 증명하는 공적 장부입니다.

+ * + *

요청 처리 흐름:

+ *
    + *
  1. 클라이언트로부터 JSON 형식의 요청 수신
  2. + *
  3. 요청 바디가 Envelope<LedgerRequest> 객체로 역직렬화됨
  4. + *
  5. Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
  6. + *
  7. 변환된 요청 객체를 GovernmentApiClient로 전달
  8. + *
  9. GovernmentApiClient가 정부 시스템과 통신 수행
  10. + *
  11. 응답 데이터를 Envelope<LedgerResponse>로 감싸서 반환
  12. + *
  13. Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
  14. + *
+ * + *

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 @@ + + + + + + + + + + + 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} + + + + + + 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 @@ + + + + + + + + + + + + + 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} + ) + + + + + +