깃 이그노어 추가

main
박성영 1 month ago
commit f9d430ade6

23
.gitignore vendored

@ -0,0 +1,23 @@
/gradle/wrapper/gradle-wrapper.jar
/gradle/wrapper/gradle-wrapper.properties
/.gradle/8.14/checksums/checksums.lock
/.gradle/8.14/checksums/md5-checksums.bin
/.gradle/8.14/checksums/sha1-checksums.bin
/.gradle/8.14/executionHistory/executionHistory.bin
/.gradle/8.14/executionHistory/executionHistory.lock
/.gradle/8.14/fileChanges/last-build.bin
/.gradle/8.14/fileHashes/fileHashes.bin
/.gradle/8.14/fileHashes/fileHashes.lock
/.gradle/8.14/fileHashes/resourceHashesCache.bin
/.gradle/8.14/gc.properties
/.gradle/buildOutputCleanup/buildOutputCleanup.lock
/.gradle/buildOutputCleanup/cache.properties
/.gradle/buildOutputCleanup/outputFiles.bin
/.gradle/vcs-1/gc.properties
/.gradle/file-system.probe
/gradlew.bat
/gradlew
/.idea/
/.gradle/
/build/
/gradle/

@ -0,0 +1,49 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.vmis'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// OpenAPI/Swagger UI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
// Apache HttpClient5 for RestTemplate request factory
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.3'
// GPKI JNI local library
implementation files('lib/libgpkiapi_jni_1.5.jar')
// Log4j 1.x for legacy util (NewGpkiUtil). Consider replacing with SLF4J in future.
implementation 'log4j:log4j:1.2.17'
// Configuration metadata
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// Lombok
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
testCompileOnly 'org.projectlombok:lombok:1.18.34'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}

Binary file not shown.

@ -0,0 +1 @@
rootProject.name = 'VMIS-interface'

@ -0,0 +1,11 @@
package com.vmis.interfaceapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class VmisInterfaceApplication {
public static void main(String[] args) {
SpringApplication.run(VmisInterfaceApplication.class, args);
}
}

@ -0,0 +1,781 @@
package com.vmis.interfaceapp.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import com.vmis.interfaceapp.gpki.GpkiService;
import com.vmis.interfaceapp.model.basic.BasicRequest;
import com.vmis.interfaceapp.model.basic.BasicResponse;
import com.vmis.interfaceapp.model.common.Envelope;
import com.vmis.interfaceapp.model.ledger.LedgerRequest;
import com.vmis.interfaceapp.model.ledger.LedgerResponse;
import com.vmis.interfaceapp.util.TxIdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
/**
* API
*
* <p> API
* . HTTP , , API
* .</p>
*
* <h3> :</h3>
* <ul>
* <li> API HTTP </li>
* <li>GPKI() / </li>
* <li> HTTP </li>
* <li>/ JSON /</li>
* <li> ID(tx_id) </li>
* <li> HTTP </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>Adapter : API </li>
* <li>Template Method : callModel </li>
* <li>Dependency Injection: </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>GPKI ( )</li>
* <li>API </li>
* <li> (INFO_SYS_ID, REGION_CODE ) </li>
* </ul>
*
* @see RestTemplate
* @see GpkiService
* @see VmisProperties
*/
@Component
public class GovernmentApiClient {
/**
* SLF4J
*
* <p> API , , .
* :</p>
* <ul>
* <li>INFO: API </li>
* <li>WARN: HTTP </li>
* <li>ERROR: ( RuntimeException )</li>
* </ul>
*/
private static final Logger log = LoggerFactory.getLogger(GovernmentApiClient.class);
/**
* Spring RestTemplate
*
* <p>HTTP .
* Spring Bean , :</p>
* <ul>
* <li>Connection Timeout </li>
* <li>Read Timeout </li>
* <li>Connection Pool </li>
* <li> (Jackson for JSON)</li>
* <li> (, )</li>
* </ul>
*/
private final RestTemplate restTemplate;
/**
* VMIS
*
* <p>application.yml application.properties .
* :</p>
* <ul>
* <li> API URL (, , )</li>
* <li>API </li>
* <li> (INFO_SYS_ID, REGION_CODE )</li>
* <li>GPKI ( ID )</li>
* </ul>
*/
private final VmisProperties props;
/**
* GPKI()
*
* <p>24 .
* :</p>
* <ul>
* <li> ( )</li>
* <li> ( )</li>
* <li> </li>
* <li> </li>
* </ul>
*
* <p> (Plain Text) .</p>
*/
private final GpkiService gpkiService;
/**
* Jackson ObjectMapper
*
* <p>Java JSON .
* :</p>
* <ul>
* <li> JSON (Serialization)</li>
* <li> JSON Java (Deserialization)</li>
* <li> (TypeReference )</li>
* <li>/ </li>
* <li>null </li>
* </ul>
*
* <p>Spring Boot ObjectMapper
* ( , ) .</p>
*/
private final ObjectMapper objectMapper;
/**
*
*
* <p> API .
* API .</p>
*
* <h4> :</h4>
* <ul>
* <li>BASIC:
* <ul>
* <li> (, , ) </li>
* <li> </li>
* <li> </li>
* </ul>
* </li>
* <li>LEDGER: ()
* <ul>
* <li> </li>
* <li>, , </li>
* <li> </li>
* </ul>
* </li>
* </ul>
*/
public enum ServiceType {
/**
* Basic service type.
*/
BASIC,
/**
* Ledger service type.
*/
LEDGER }
/**
*
*
* <p>Spring .
*
* @param restTemplate HTTP RestTemplate
* @param props VMIS
* @param gpkiService GPKI /
* @param objectMapper JSON / ObjectMapper
* @Component Spring Bean .</p> <h4> :</h4> <ul> <li> ( )</li> <li> ( final )</li> <li> (Mock )</li> <li> ( )</li> </ul>
*/
public GovernmentApiClient(RestTemplate restTemplate, VmisProperties props, GpkiService gpkiService, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.props = props;
this.gpkiService = gpkiService;
this.objectMapper = objectMapper;
}
/**
* API ( )
*
* <p> JSON API (Low-level) .
* {@link #callModel} .</p>
*
* <h3> :</h3>
* <ol>
* <li><b> :</b> API </li>
* <li><b>URL :</b> , , URL </li>
* <li><b> ID :</b> ID (TxIdUtil )</li>
* <li><b> :</b> (API , , GPKI ) </li>
* <li><b> :</b> GPKI </li>
* <li><b>HTTP :</b> RestTemplate POST </li>
* <li><b> :</b> GPKI </li>
* <li><b> :</b> ResponseEntity </li>
* </ol>
*
* <h3> :</h3>
* <ul>
* <li><b>GPKI :</b> RuntimeException </li>
* <li><b>HTTP (4xx, 5xx):</b> </li>
* <li><b>GPKI :</b> RuntimeException </li>
* </ul>
*
* <h3>:</h3>
* <ul>
* <li> : URL, ID, GPKI , </li>
* <li> : HTTP , </li>
* </ul>
*
* @param type (BASIC LEDGER)
* @param jsonBody JSON ( )
* @return ResponseEntity&lt;String&gt; API ( JSON )
* @throws RuntimeException GPKI /
*/
public ResponseEntity<String> call(ServiceType type, String jsonBody) {
// 1. 서비스 타입에 따른 설정 로드
// props.getGov()는 정부 API 관련 모든 설정을 포함하는 객체를 반환
VmisProperties.GovProps gov = props.getGov();
// 삼항 연산자를 사용하여 BASIC 또는 LEDGER 서비스 설정 선택
VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC)
? gov.getServices().getBasic()
: gov.getServices().getLedger();
// 2. 완전한 URL 구성 (예: https://api.gov.kr:8080/vmis/basic)
String url = gov.buildServiceUrl(svc.getPath());
// 3. 트랜잭션 ID 생성 (요청 추적 및 디버깅 용도)
// 일반적으로 타임스탬프 + UUID 조합으로 생성됨
String txId = TxIdUtil.generate();
// 4. HTTP 헤더 구성
// API 키, 시스템 정보, GPKI 설정 등 필수 헤더 포함
HttpHeaders headers = buildHeaders(svc, txId);
// 5. GPKI 암호화 처리
String bodyToSend = jsonBody;
if (gpkiService.isEnabled()) {
try {
// 평문 JSON을 암호화된 문자열로 변환
// 암호화 결과는 Base64 인코딩된 문자열
bodyToSend = gpkiService.encrypt(jsonBody);
} catch (Exception e) {
// 암호화 실패는 치명적 오류이므로 즉시 예외 발생
throw new RuntimeException("GPKI 암호화 실패", e);
}
}
// 6. HTTP 엔티티 생성 (헤더 + 바디)
HttpEntity<String> request = new HttpEntity<>(bodyToSend, headers);
// 7. 요청 로그 기록
// 디버깅을 위해 URL, 트랜잭션 ID, GPKI 활성화 여부, 바디 길이 로깅
log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0);
try {
// 8. 실제 HTTP POST 요청 전송
// RestTemplate.exchange()는 HTTP 메서드, 헤더, 바디를 모두 지정 가능
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
String respBody = response.getBody();
// 9. 성공 응답인 경우 GPKI 복호화 처리
if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) {
try {
// 암호화된 응답을 평문 JSON으로 복호화
String decrypted = gpkiService.decrypt(respBody);
// 복호화된 바디로 새로운 ResponseEntity 생성
// 원본 응답의 상태 코드와 헤더는 그대로 유지
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(decrypted);
} catch (Exception e) {
// 복호화 실패는 치명적 오류이므로 즉시 예외 발생
throw new RuntimeException("GPKI 복호화 실패", e);
}
}
// GPKI가 비활성화되어 있거나 에러 응답인 경우 원본 그대로 반환
return response;
} catch (HttpStatusCodeException ex) {
// 10. HTTP 에러 처리 (4xx, 5xx 상태 코드)
// 경고 로그 기록 (에러는 정상적인 비즈니스 흐름일 수 있으므로 WARN 레벨)
log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString());
// 에러 응답을 ResponseEntity로 변환하여 반환
// 호출자가 상태 코드와 에러 메시지를 확인할 수 있도록 함
// 헤더가 null인 경우를 대비하여 빈 HttpHeaders 사용
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(ex.getResponseBodyAsString());
}
}
/**
* HTTP
*
* <p> API HTTP private .
* .</p>
*
* <h3> :</h3>
* <table border="1">
* <tr>
* <th></th>
* <th></th>
* <th></th>
* <th></th>
* </tr>
* <tr>
* <td>Content-Type</td>
* <td> </td>
* <td>application/json; charset=UTF-8</td>
* <td></td>
* </tr>
* <tr>
* <td>Accept</td>
* <td> </td>
* <td>application/json</td>
* <td></td>
* </tr>
* <tr>
* <td>gpki_yn</td>
* <td>GPKI (Y/N)</td>
* <td>Y</td>
* <td></td>
* </tr>
* <tr>
* <td>tx_id</td>
* <td> ID ( )</td>
* <td>20250104123045_abc123</td>
* <td></td>
* </tr>
* <tr>
* <td>cert_server_id</td>
* <td> </td>
* <td>VMIS_SERVER_01</td>
* <td></td>
* </tr>
* <tr>
* <td>api_key</td>
* <td> API </td>
* <td>abc123def456...</td>
* <td></td>
* </tr>
* <tr>
* <td>cvmis_apikey</td>
* <td>CVMIS API </td>
* <td>xyz789uvw012...</td>
* <td></td>
* </tr>
* <tr>
* <td>INFO_SYS_ID</td>
* <td> </td>
* <td>VMIS_SEOUL</td>
* <td></td>
* </tr>
* <tr>
* <td>REGION_CODE</td>
* <td> ( )</td>
* <td>11010 ( )</td>
* <td></td>
* </tr>
* <tr>
* <td>DEPT_CODE</td>
* <td> </td>
* <td>TRANS_001</td>
* <td></td>
* </tr>
* </table>
*
* <h3> :</h3>
* <ul>
* <li>Content-Type UTF-8 </li>
* <li> </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>API </li>
* <li> API </li>
* <li> (BASIC, LEDGER) API </li>
* </ul>
*
* @param svc (API , )
* @param txId ID
* @return HttpHeaders HTTP
*/
private HttpHeaders buildHeaders(VmisProperties.GovProps.Service svc, String txId) {
// 1. 빈 HttpHeaders 객체 생성
HttpHeaders headers = new HttpHeaders();
// 2. Content-Type 설정
// UTF-8 인코딩을 명시하여 한글 데이터가 올바르게 전송되도록 함
headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
// 3. Accept 헤더 설정
// 서버에게 JSON 형식의 응답을 요청함
headers.setAccept(java.util.List.of(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());
// 9. 정보시스템 ID
// 호출하는 시스템을 식별하는 필수 값
headers.add("INFO_SYS_ID", props.getSystem().getInfoSysId());
// 10. 지역 코드 (선택 사항)
// 시군구 코드 (예: 11010 = 서울특별시 종로구)
// null이 아닌 경우에만 헤더에 추가
if (props.getSystem().getRegionCode() != null) headers.add("REGION_CODE", props.getSystem().getRegionCode());
// 11. 부서 코드 (선택 사항)
// 호출하는 부서의 코드
// null이 아닌 경우에만 헤더에 추가
if (props.getSystem().getDepartmentCode() != null) headers.add("DEPT_CODE", props.getSystem().getDepartmentCode());
// 구성 완료된 헤더 반환
return headers;
}
/**
* API
*
* <p> .
* {@link #callModel} .</p>
*
* <h3>:</h3>
* <ul>
* <li> </li>
* <li>/ Envelope </li>
* <li>Jackson TypeReference </li>
* </ul>
*
* <h3> :</h3>
* <pre>
* BasicRequest request = new BasicRequest();
* request.setVehicleNo("12가3456");
*
* Envelope&lt;BasicRequest&gt; envelope = new Envelope&lt;&gt;();
* envelope.setData(request);
*
* ResponseEntity&lt;Envelope&lt;BasicResponse&gt;&gt; response = govClient.callBasic(envelope);
* BasicResponse data = response.getBody().getData();
* </pre>
*
* @param envelope Envelope
* @return ResponseEntity&lt;Envelope&lt;BasicResponse&gt;&gt;
*/
public ResponseEntity<Envelope<BasicResponse>> callBasic(Envelope<BasicRequest> envelope) {
// TypeReference를 사용하여 제네릭 타입 정보 전달
// 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결
return callModel(ServiceType.BASIC, envelope, new TypeReference<Envelope<BasicResponse>>(){});
}
/**
* () API
*
* <p> .
* {@link #callModel} .</p>
*
* <h3>:</h3>
* <ul>
* <li> </li>
* <li>/ Envelope </li>
* <li>Jackson TypeReference </li>
* </ul>
*
* <h3> :</h3>
* <pre>
* LedgerRequest request = new LedgerRequest();
* request.setVehicleNo("12가3456");
* request.setOwnerName("홍길동");
*
* Envelope&lt;LedgerRequest&gt; envelope = new Envelope&lt;&gt;();
* envelope.setData(request);
*
* ResponseEntity&lt;Envelope&lt;LedgerResponse&gt;&gt; response = govClient.callLedger(envelope);
* LedgerResponse data = response.getBody().getData();
* </pre>
*
* @param envelope Envelope
* @return ResponseEntity&lt;Envelope&lt;LedgerResponse&gt;&gt;
*/
public ResponseEntity<Envelope<LedgerResponse>> callLedger(Envelope<LedgerRequest> envelope) {
// TypeReference를 사용하여 제네릭 타입 정보 전달
// 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결
return callModel(ServiceType.LEDGER, envelope, new TypeReference<Envelope<LedgerResponse>>(){});
}
/**
* API ( )
*
* <p> API .
* Java JSON , ,
* Java .</p>
*
* <h3>Template Method :</h3>
* <ul>
* <li> Template Method 릿 </li>
* <li> </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>&lt;TReq&gt;: (BasicRequest LedgerRequest)</li>
* <li>&lt;TResp&gt;: (BasicResponse LedgerResponse)</li>
* <li> </li>
* </ul>
*
* <h3> ():</h3>
* <ol>
* <li><b> :</b>
* <ul><li> BASIC LEDGER </li></ul>
* </li>
* <li><b>URL ID :</b>
* <ul><li> API URL </li>
* <li> ID </li></ul>
* </li>
* <li><b> (Serialization):</b>
* <ul><li>Java (Envelope&lt;TReq&gt;) JSON </li>
* <li>ObjectMapper.writeValueAsString() </li></ul>
* </li>
* <li><b> :</b>
* <ul><li>buildHeaders() </li>
* <li> </li></ul>
* </li>
* <li><b>GPKI ():</b>
* <ul><li>GPKI JSON </li>
* <li>gpkiEncrypt() </li></ul>
* </li>
* <li><b>HTTP :</b>
* <ul><li>RestTemplate.exchange() POST </li>
* <li> </li></ul>
* </li>
* <li><b>GPKI ():</b>
* <ul><li> (2xx) GPKI </li>
* <li>gpkiDecrypt() </li></ul>
* </li>
* <li><b> (Deserialization):</b>
* <ul><li>JSON Java (Envelope&lt;TResp&gt;) </li>
* <li>TypeReference </li></ul>
* </li>
* <li><b> :</b>
* <ul><li>ResponseEntity HTTP </li></ul>
* </li>
* </ol>
*
* <h3> (3):</h3>
* <ol>
* <li><b>HttpStatusCodeException (HTTP ):</b>
* <ul>
* <li> API 4xx 5xx </li>
* <li> Envelope </li>
* <li> Envelope </li>
* <li> (WARN )</li>
* </ul>
* </li>
* <li><b>JSON :</b>
* <ul>
* <li> JSON </li>
* <li>RuntimeException </li>
* </ul>
* </li>
* <li><b> :</b>
* <ul>
* <li> , </li>
* <li>RuntimeException </li>
* </ul>
* </li>
* </ol>
*
* <h3>TypeReference :</h3>
* <p>Java (Type Erasure)
* {@code objectMapper.readValue(json, Envelope<TResp>.class)}
* . TypeReference
* Jackson .</p>
*
* <h3> :</h3>
* <ul>
* <li> : [GOV-REQ] url, tx_id, gpki, length</li>
* <li> : [GOV-ERR] status, body</li>
* </ul>
*
* @param <TReq>
* @param <TResp>
* @param type (BASIC LEDGER)
* @param envelope Envelope
* @param respType TypeReference ( )
* @return ResponseEntity&lt;Envelope&lt;TResp&gt;&gt; ResponseEntity
* @throws RuntimeException JSON / , , GPKI
*/
private <TReq, TResp> ResponseEntity<Envelope<TResp>> callModel(ServiceType type,
Envelope<TReq> envelope,
TypeReference<Envelope<TResp>> respType) {
// 1. 서비스 타입에 따른 설정 로드
VmisProperties.GovProps gov = props.getGov();
VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC)
? gov.getServices().getBasic()
: gov.getServices().getLedger();
// 2. URL 및 트랜잭션 ID 생성
String url = gov.buildServiceUrl(svc.getPath());
String txId = TxIdUtil.generate();
try {
// 3. 직렬화: Java 객체 → JSON 문자열
// ObjectMapper가 Envelope 객체를 JSON으로 변환
// 날짜, null 값 등의 처리는 ObjectMapper 설정에 따름
String jsonBody = objectMapper.writeValueAsString(envelope);
// 4. HTTP 헤더 구성
HttpHeaders headers = buildHeaders(svc, txId);
// 5. GPKI 암호화 처리
String bodyToSend = jsonBody;
if (gpkiService.isEnabled()) {
// JSON 평문을 암호화된 문자열로 변환
bodyToSend = gpkiEncrypt(jsonBody);
}
// 6. HTTP 엔티티 생성 (헤더 + 바디)
HttpEntity<String> request = new HttpEntity<>(bodyToSend, headers);
// 7. 요청 로그 기록
log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0);
// 8. 실제 HTTP POST 요청 전송
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
String respBody = response.getBody();
// 9. GPKI 복호화 처리 (성공 응답인 경우만)
if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) {
// 암호화된 응답을 평문 JSON으로 복호화
respBody = gpkiDecrypt(respBody);
}
// 10. 역직렬화: JSON 문자열 → Java 객체
// TypeReference를 사용하여 제네릭 타입 정보 전달
Envelope<TResp> mapped = objectMapper.readValue(respBody, respType);
// 11. 응답 반환 (상태 코드, 헤더, 바디 모두 포함)
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(mapped);
} catch (HttpStatusCodeException ex) {
// HTTP 에러 처리 (4xx, 5xx)
log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString());
// 에러 응답 바디 파싱 시도
// 정부 API는 에러 응답도 Envelope 형식으로 반환할 수 있음
Envelope<TResp> empty = new Envelope<>();
try {
// 에러 응답을 Envelope 객체로 파싱
Envelope<TResp> parsed = objectMapper.readValue(ex.getResponseBodyAsString(), respType);
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(parsed);
} catch (Exception parseEx) {
// 파싱 실패 시 빈 Envelope 반환
// 호출자는 HTTP 상태 코드로 에러 판단 가능
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(empty);
}
} catch (Exception e) {
// 기타 모든 예외 (네트워크 오류, JSON 파싱 오류, GPKI 오류 등)
// RuntimeException으로 래핑하여 상위로 전파
// Spring의 @ExceptionHandler에서 처리 가능
throw new RuntimeException("정부 API 호출 중 오류", e);
}
}
/**
* GPKI
*
* <p> JSON GPKI() .
* RuntimeException .</p>
*
* <h3> :</h3>
* <ol>
* <li> JSON </li>
* <li> </li>
* <li> Base64 </li>
* <li>Base64 </li>
* </ol>
*
* <h3> :</h3>
* <ul>
* <li> : GPKI </li>
* <li> : </li>
* <li> : Base64 </li>
* <li> RuntimeException </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> ()</li>
* <li> </li>
* <li> </li>
* </ul>
*
* @param jsonBody JSON
* @return String Base64
* @throws RuntimeException GPKI
*/
private String gpkiEncrypt(String jsonBody) {
try {
// GpkiService에 암호화 위임
// 실제 암호화 로직은 GpkiService가 캡슐화
return gpkiService.encrypt(jsonBody);
} catch (Exception e) {
// 암호화 실패는 치명적 오류
// 평문 데이터를 전송할 수 없으므로 즉시 중단
throw new RuntimeException("GPKI 암호화 실패", e);
}
}
/**
* GPKI
*
* <p> GPKI() .
* RuntimeException .</p>
*
* <h3> :</h3>
* <ol>
* <li>Base64 </li>
* <li> </li>
* <li> UTF-8 </li>
* <li> JSON </li>
* </ol>
*
* <h3> :</h3>
* <ul>
* <li> : GPKI </li>
* <li> : </li>
* <li> : Base64 </li>
* <li> : UTF-8 </li>
* <li> RuntimeException </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> ()</li>
* <li> </li>
* <li> ( )</li>
* </ul>
*
* @param cipher Base64
* @return String JSON
* @throws RuntimeException GPKI
*/
private String gpkiDecrypt(String cipher) {
try {
// GpkiService에 복호화 위임
// 실제 복호화 로직은 GpkiService가 캡슐화
return gpkiService.decrypt(cipher);
} catch (Exception e) {
// 복호화 실패는 치명적 오류
// 암호문을 해석할 수 없으므로 즉시 중단
throw new RuntimeException("GPKI 복호화 실패", e);
}
}
}

@ -0,0 +1,20 @@
package com.vmis.interfaceapp.config;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import com.vmis.interfaceapp.gpki.GpkiService;
import com.vmis.interfaceapp.gpki.NoopGpkiService;
import com.vmis.interfaceapp.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();
}
}

@ -0,0 +1,46 @@
package com.vmis.interfaceapp.config;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.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;
import java.time.Duration;
@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();
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout, java.util.concurrent.TimeUnit.MILLISECONDS)
.setResponseTimeout(readTimeout, java.util.concurrent.TimeUnit.MILLISECONDS)
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);
cm.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(cm)
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
return builder
.requestFactory(() -> requestFactory)
.setConnectTimeout(Duration.ofMillis(connectTimeout))
.setReadTimeout(Duration.ofMillis(readTimeout))
.build();
}
}

@ -0,0 +1,27 @@
package com.vmis.interfaceapp.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(""));
}
}

@ -0,0 +1,10 @@
package com.vmis.interfaceapp.config;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(VmisProperties.class)
public class PropertiesConfig {
}

@ -0,0 +1,180 @@
package com.vmis.interfaceapp.config.properties;
import jakarta.validation.constraints.NotBlank;
import jakarta.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; }
}
}
}

@ -0,0 +1,211 @@
package com.vmis.interfaceapp.controller;
import com.vmis.interfaceapp.client.GovernmentApiClient;
import com.vmis.interfaceapp.model.basic.BasicRequest;
import com.vmis.interfaceapp.model.basic.BasicResponse;
import com.vmis.interfaceapp.model.common.Envelope;
import com.vmis.interfaceapp.model.ledger.LedgerRequest;
import com.vmis.interfaceapp.model.ledger.LedgerResponse;
import com.vmis.interfaceapp.service.RequestEnricher;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* REST
*
* <p> API (Entry Point) .
* HTTP .</p>
*
* <h3> :</h3>
* <ul>
* <li> API </li>
* <li> () API </li>
* <li> Envelope </li>
* <li> </li>
* </ul>
*
* <h3>API :</h3>
* <ul>
* <li> : /api/v1/vehicles</li>
* <li> : POST /api/v1/vehicles/basic</li>
* <li> : POST /api/v1/vehicles/ledger</li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> (Thin) </li>
* <li> {@link GovernmentApiClient} </li>
* <li>Swagger/OpenAPI </li>
* </ul>
*
* @see GovernmentApiClient
* @see Envelope
*/
@RestController
@RequestMapping("/api/v1/vehicles")
@Tag(name = "Vehicle Interfaces", description = "시군구연계 자동차 정보 연계 API")
public class VehicleInterfaceController {
private final RequestEnricher enricher;
/**
* API
*
* <p> .
* :</p>
* <ul>
* <li>HTTP / </li>
* <li>GPKI /</li>
* <li> </li>
* <li> </li>
* </ul>
*
* <p> (Constructor Injection) ,
* final .</p>
*/
private final GovernmentApiClient govClient;
/**
*
*
* <p>Spring Framework .
* :</p>
* <ul>
* <li> </li>
* <li> (final )</li>
* <li> Mock </li>
* <li> </li>
* </ul>
*
* @param govClient API
*/
public VehicleInterfaceController(GovernmentApiClient govClient, RequestEnricher enricher) {
this.govClient = govClient;
this.enricher = enricher;
}
/**
* API
*
* <p>, API.</p>
*
* <h3> :</h3>
* <ol>
* <li> JSON </li>
* <li> Envelope&lt;BasicRequest&gt; </li>
* <li>Spring @RequestBody JSON </li>
* <li> GovernmentApiClient </li>
* <li>GovernmentApiClient </li>
* <li> Envelope&lt;BasicResponse&gt; </li>
* <li>Spring JSON </li>
* </ol>
*
* <h3>HTTP :</h3>
* <ul>
* <li>: POST</li>
* <li>: /api/v1/vehicles/basic</li>
* <li>Content-Type: application/json ()</li>
* <li>Accept: application/json ()</li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>Envelope: ( ID, ) </li>
* <li>BasicRequest: ( )</li>
* <li>BasicResponse: (, , )</li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>JSON 400 Bad Request (Spring )</li>
* <li> API HTTP </li>
* <li> RuntimeException </li>
* </ul>
*
* @param envelope Envelope (header) (data)
* @return ResponseEntity&lt;Envelope&lt;BasicResponse&gt;&gt; HTTP , ,
*/
@PostMapping(value = "/basic", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "자동차기본사항조회", description = "시군구연계 자동차기본사항조회 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.")
public ResponseEntity<Envelope<BasicResponse>> basic(
@org.springframework.web.bind.annotation.RequestBody Envelope<BasicRequest> envelope
) {
// YAML 설정값으로 공통 필드 자동 채움
enricher.enrichBasic(envelope);
// 실제 처리는 GovernmentApiClient에 위임
return govClient.callBasic(envelope);
}
/**
* () API
*
* <p> API.
* .</p>
*
* <h3> :</h3>
* <ol>
* <li> JSON </li>
* <li> Envelope&lt;LedgerRequest&gt; </li>
* <li>Spring @RequestBody JSON </li>
* <li> GovernmentApiClient </li>
* <li>GovernmentApiClient </li>
* <li> Envelope&lt;LedgerResponse&gt; </li>
* <li>Spring JSON </li>
* </ol>
*
* <h3>HTTP :</h3>
* <ul>
* <li>: POST</li>
* <li>: /api/v1/vehicles/ledger</li>
* <li>Content-Type: application/json ()</li>
* <li>Accept: application/json ()</li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>Envelope: ( ID, ) </li>
* <li>LedgerRequest: (, )</li>
* <li>LedgerResponse: ( , )</li>
* </ul>
*
* <h3>() :</h3>
* <ul>
* <li> (, , )</li>
* <li> (, , )</li>
* <li> ( , )</li>
* <li> </li>
* <li> </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li>JSON 400 Bad Request (Spring )</li>
* <li> API HTTP </li>
* <li> RuntimeException </li>
* <li> 403 Forbidden 401 Unauthorized </li>
* </ul>
*
* <h3> :</h3>
* <ul>
* <li> / </li>
* <li>GPKI </li>
* <li> </li>
* </ul>
*
* @param envelope Envelope (header) (data)
* @return ResponseEntity&lt;Envelope&lt;LedgerResponse&gt;&gt; HTTP , ,
*/
@PostMapping(value = "/ledger", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "자동차등록원부(갑)", description = "시군구연계 자동차등록원부(갑) 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.")
public ResponseEntity<Envelope<LedgerResponse>> ledger(
@org.springframework.web.bind.annotation.RequestBody Envelope<LedgerRequest> envelope
) {
// YAML 설정값으로 공통 필드 자동 채움
enricher.enrichLedger(envelope);
// 실제 처리는 GovernmentApiClient에 위임
return govClient.callLedger(envelope);
}
}

@ -0,0 +1,7 @@
package com.vmis.interfaceapp.gpki;
public interface GpkiService {
String encrypt(String plain) throws Exception;
String decrypt(String cipher) throws Exception;
boolean isEnabled();
}

@ -0,0 +1,18 @@
package com.vmis.interfaceapp.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;
}
}

@ -0,0 +1,41 @@
package com.vmis.interfaceapp.gpki;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import com.vmis.interfaceapp.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;
}
}

@ -0,0 +1,88 @@
package com.vmis.interfaceapp.model.basic;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Schema(description = "자동차기본사항조회 요청 항목")
public class BasicRequest {
// 본문 공통 메타
@Schema(description = "정보시스템ID")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP")
@JsonProperty("INFO_SYS_IP")
private String infoSysIp;
@Schema(description = "시군구코드")
@JsonProperty("SIGUNGU_CODE")
private String sigunguCode;
// 서비스별 필드
@Schema(description = "연계정보코드", example = "AC1_FD11_01")
@JsonProperty("CNTC_INFO_CODE")
private String cntcInfoCode;
@Schema(description = "담당자ID")
@JsonProperty("CHARGER_ID")
private String chargerId;
@Schema(description = "담당자IP")
@JsonProperty("CHARGER_IP")
private String chargerIp;
@Schema(description = "담당자명(사용자)")
@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;
}

@ -0,0 +1,121 @@
package com.vmis.interfaceapp.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> 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;
}
}

@ -0,0 +1,32 @@
package com.vmis.interfaceapp.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<T> {
@JsonProperty("data")
private List<T> data = new ArrayList<>();
public Envelope() {}
public Envelope(T single) {
if (single != null) this.data.add(single);
}
public Envelope(List<T> data) {
this.data = data;
}
}

@ -0,0 +1,79 @@
package com.vmis.interfaceapp.model.ledger;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "자동차등록원부(갑) 요청 항목")
@Getter
@Setter
public class LedgerRequest {
// 본문 공통 메타
@Schema(description = "정보시스템ID")
@JsonProperty("INFO_SYS_ID")
private String infoSysId;
@Schema(description = "정보시스템IP")
@JsonProperty("INFO_SYS_IP")
private String infoSysIp;
@Schema(description = "시군구코드")
@JsonProperty("SIGUNGU_CODE")
private String sigunguCode;
// 서비스별 필드
@Schema(description = "연계정보코드", example = "AC1_FD11_02")
@JsonProperty("CNTC_INFO_CODE")
private String cntcInfoCode;
@Schema(description = "담당자ID")
@JsonProperty("CHARGER_ID")
private String chargerId;
@Schema(description = "담당자IP")
@JsonProperty("CHARGER_IP")
private String chargerIp;
@Schema(description = "담당자명(사용자)")
@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;
}

@ -0,0 +1,250 @@
package com.vmis.interfaceapp.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> 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; }
}
}

@ -0,0 +1,63 @@
package com.vmis.interfaceapp.service;
import com.vmis.interfaceapp.config.properties.VmisProperties;
import com.vmis.interfaceapp.model.basic.BasicRequest;
import com.vmis.interfaceapp.model.common.Envelope;
import com.vmis.interfaceapp.model.ledger.LedgerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Populates incoming request models with values from YAML configuration.
* Unconditionally overwrites the listed fields per requirement:
* - INFO_SYS_ID, INFO_SYS_IP, SIGUNGU_CODE
* - CNTC_INFO_CODE (service specific)
* - CHARGER_ID, CHARGER_IP, CHARGER_NM
*/
@Component
public class RequestEnricher {
private static final Logger log = LoggerFactory.getLogger(RequestEnricher.class);
private final VmisProperties props;
public RequestEnricher(VmisProperties props) {
this.props = props;
}
public void enrichBasic(Envelope<BasicRequest> envelope) {
if (envelope == null || envelope.getData() == null) return;
VmisProperties.SystemProps sys = props.getSystem();
String cntc = props.getGov().getServices().getBasic().getCntcInfoCode();
for (BasicRequest req : envelope.getData()) {
if (req == null) continue;
req.setInfoSysId(sys.getInfoSysId());
req.setInfoSysIp(sys.getInfoSysIp());
req.setSigunguCode(sys.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<LedgerRequest> envelope) {
if (envelope == null || envelope.getData() == null) return;
VmisProperties.SystemProps sys = props.getSystem();
String cntc = props.getGov().getServices().getLedger().getCntcInfoCode();
for (LedgerRequest req : envelope.getData()) {
if (req == null) continue;
req.setInfoSysId(sys.getInfoSysId());
req.setInfoSysIp(sys.getInfoSysIp());
req.setSigunguCode(sys.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);
}
}

@ -0,0 +1,98 @@
package com.vmis.interfaceapp.util;
import com.vmis.interfaceapp.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).");
}
}
}

@ -0,0 +1,383 @@
package com.vmis.interfaceapp.util;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
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;
public class NewGpkiUtil {
private static Logger logger = Logger.getLogger(NewGpkiUtil.class);
byte[] myEnvCert, myEnvKey, mySigCert, mySigKey;
private Map<String, X509Certificate> targetServerCertMap = new HashMap<String, X509Certificate>();
// properties
private String myServerId;
private String targetServerIdList;
private String envCertFilePathName;
private String envPrivateKeyFilePathName;
private String envPrivateKeyPasswd;
private String sigCertFilePathName;
private String sigPrivateKeyFilePathName;
private String sigPrivateKeyPasswd;
private String certFilePath;
private String gpkiLicPath = ".";
private boolean isLDAP;
private boolean testGPKI = false;
public void init() throws Exception {
GpkiApi.init(gpkiLicPath);
gpkiapi_jni gpki = this.getGPKI();
if(logger.isDebugEnabled()){
if(gpki.API_GetInfo()==0)
logger.debug(gpki.sReturnString);
else
logger.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);
}
}
}
logger.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);
logger.info("GpkiUtil initialized");
}
private void load(gpkiapi_jni gpki, String certId) throws Exception {
logger.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{
logger.debug("not certFilePath");
}
}
targetServerCertMap.put(certId, cert);
}
private gpkiapi_jni getGPKI(){
gpkiapi_jni gpki = new gpkiapi_jni();
if(gpki.API_Init(gpkiLicPath) != 0){
logger.error(gpki.sDetailErrorString);
}
return gpki;
}
private void finish(gpkiapi_jni gpki){
if(gpki.API_Finish() != 0){
logger.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
logger.info("=======================================================");
logger.info("================ TEST GPKI START ======================");
logger.info("=======================================================");
String original_Eng = "abc";
logger.info("=== TEST ENG STRING: "+ original_Eng);
try {
byte[] encrypted = encrypt(original_Eng.getBytes(), myServerId);
logger.info("=== TEST ENG ENCRYPT STRING: "+ encode(encrypted));
String decrypted = new String(decrypt(encrypted));
logger.info("=== TEST ENG DECRYPT STRING: "+decrypted);
if (!original_Eng.equals(decrypted)) {
throw new Exception("GpkiUtil not initialized properly(english)");
}
logger.info("=== TEST ENG: OK");
} catch (Exception e) {
logger.warn("Gpki Test error(english)", e);
throw e;
}
//gpki test kor
String original = "한글테스트";
logger.info("=== TEST KOR STRING: "+ original);
try {
byte[] encrypted = encrypt(original.getBytes(), myServerId);
logger.info("=== TEST KOR ENCRYPT STRING: "+ encode(encrypted));
String decrypted = new String(decrypt(encrypted));
logger.info("=== TEST KOR DECRYPT STRING: "+decrypted);
if (!original.equals(decrypted)) {
throw new Exception("GpkiUtil not initialized properly(korean)");
}
logger.info("=== TEST KOR: OK");
} catch (Exception e) {
logger.warn("Gpki Test error(korean)", e);
throw e;
}finally{
logger.info("=======================================================");
logger.info("================ TEST GPKI END ========================");
logger.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;
}
}

@ -0,0 +1,18 @@
package com.vmis.interfaceapp.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;
}
}

@ -0,0 +1,45 @@
server:
port: 8080
# 인터페이스 및 연계 설정 - 개발(DEV) 환경
vmis:
system:
infoSysId: "41-345"
infoSysIp: "105.19.10.135"
regionCode: "41460"
departmentCode: ""
chargerId: ""
chargerIp: ""
chargerNm: ""
gpki:
enabled: "Y"
useSign: true
charset: "UTF-8"
certServerId: "SVR5640020001"
targetServerId: "SVR1500000015"
ldap: true
gpkiLicPath: "C:/gpki2/gpkisecureweb/conf"
certFilePath: "C:/gpki2/gpkisecureweb/certs"
envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.cer"
envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.key"
envPrivateKeyPasswd: "change-me-dev"
sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.cer"
sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.key"
sigPrivateKeyPasswd: "change-me-dev"
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: "05e8d748fb366a0831dce71a32424460746a72d591cf483ccc130534dd51e394"
cvmisApikey: "014F9215-B6D9A3B6-4CED5225-68408C46"
ledger: # 시군구연계 자동차등록원부(갑)
path: "/SignguCarLedgerFrmbkService"
cntcInfoCode: "AC1_FD11_02"
apiKey: "1beeb01857c2e7e9b41c002b007ccb9754d9c272f66d4bb64fc45b302c69e529"
cvmisApikey: "63DF159B-7B9C64C5-86CCB15C-5F93E750"

@ -0,0 +1,42 @@
server:
port: 8080
# 인터페이스 및 연계 설정 - 운영(PRD) 환경
# 주의: 실제 운영 키/호스트는 배포 환경 변수나 외부 설정(Secret)로 주입 권장
vmis:
system:
infoSysId: "41-345" # 운영 실제값으로 교체
regionCode: "" # 운영 실제값
departmentCode: "" # 운영 실제값
gpki:
enabled: "Y"
useSign: true
charset: "UTF-8"
certServerId: "SVR5640020001" # 운영 인증서 ID로 교체
targetServerId: "SVR1500000015"
ldap: true
gpkiLicPath: "C:/gpki2/gpkisecureweb/conf"
certFilePath: "C:/gpki2/gpkisecureweb/certs"
envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.cer"
envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.key"
envPrivateKeyPasswd: "change-me-prd"
sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.cer"
sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.key"
sigPrivateKeyPasswd: "change-me-prd"
gov:
scheme: "http"
host: "10.188.225.25:29001" # 예시: 운영 행정망 (명세에 맞춰 수정)
basePath: "/piss/api/molit"
connectTimeoutMillis: 5000
readTimeoutMillis: 10000
services:
basic:
path: "/SignguCarBassMatterInqireService"
cntcInfoCode: "AC1_FD11_01"
apiKey: "05e8d748fb366a0831dce71a32424460746a72d591cf483ccc130534dd51e394"
cvmisApikey: "014F9215-B6D9A3B6-4CED5225-68408C46"
ledger:
path: "/SignguCarLedgerFrmbkService"
cntcInfoCode: "AC1_FD11_02"
apiKey: "1beeb01857c2e7e9b41c002b007ccb9754d9c272f66d4bb64fc45b302c69e529"
cvmisApikey: "63DF159B-7B9C64C5-86CCB15C-5F93E750"

@ -0,0 +1,55 @@
spring:
application:
name: vmis-interface
mvc:
pathmatch:
matching-strategy: ant_path_matcher
server:
port: 8080
# 인터페이스 및 연계 설정 (공통 기본값)
vmis:
system:
infoSysId: "41-345" # INFO_SYS_ID
infoSysIp: "" # INFO_SYS_IP (예: 105.19.10.135)
# 예시: 경기도 용인시 수지구 장애인복지과
regionCode: "" # 시군구코드 (SIGUNGU_CODE, 예: 41460)
departmentCode: "" # 행정부서코드 필요 시 입력
# 담당자 기본값(미입력 시 빈 값)
chargerId: ""
chargerIp: ""
chargerNm: ""
gpki:
enabled: "Y" # Y 또는 N (환경별 yml에서 재정의 가능)
useSign: true
charset: "UTF-8"
certServerId: "SVR5640020001" # 이용기관 인증서 ID (ENV/SIG 공통 서버 ID)
targetServerId: "SVR1611000006" # 보유기관 인증서 ID(교통안전공단)
# 아래는 네이티브 GPKI 유틸(NewGpkiUtil) 설정값 (필요 시 사용)
ldap: true # LDAP 사용 여부 (true: LDAP에서 대상서버 인증서 조회)
gpkiLicPath: "C:/gpki2/gpkisecureweb/conf"
certFilePath: "C:/gpki2/gpkisecureweb/certs" # LDAP 미사용 시 대상서버 인증서 파일 폴더
envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_env.cer"
envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_env.key"
envPrivateKeyPasswd: "change-me"
sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_sig.cer"
sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_sig.key"
sigPrivateKeyPasswd: "change-me"
gov:
scheme: "http"
host: "localhost:18080" # 환경별 yml에서 재정의
basePath: "/piss/api/molit"
connectTimeoutMillis: 5000
readTimeoutMillis: 10000
services:
basic: # 시군구연계 자동차기본사항조회
path: "/SignguCarBassMatterInqireService"
cntcInfoCode: "AC1_FD11_01"
apiKey: "change-me"
cvmisApikey: "change-me"
ledger: # 시군구연계 자동차등록원부(갑)
path: "/SignguCarLedgerFrmbkService"
cntcInfoCode: "AC1_FD11_02"
apiKey: "change-me"
cvmisApikey: "change-me"

@ -0,0 +1,316 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import util.NewGpkiUtil;
import util.ShareGpki;
public class N0000001589____SignguCarBassMatterInqireService {
/**
* IF-LI-AC1-FD11-01-GSR0
*
*/
// 이용기관의 네트워크망에 따라서 호출 host 정보가 다름. (네트워크망과 개발 or 운영의 용도에 따라서 해당하는 host로 설정)
// 이용기관 : 행정망
static String host = "10.188.225.94:29001"; // 개발(DEV)
// static String host = "10.188.225.25:29001"; // 운영(OP)
// 이용기관 : 인터넷
// static String host = "116.67.73.153:39011"; // 개발(DEV)
// static String host = "116.67.73.153:29001"; // 운영(OP)
static String targetUrl = "http://"+host+"/piss/api/molit/SignguCarBassMatterInqireService";
static String method = "POST";
//--------------Client Setting---------------
//http Header Setting
static String requestType = "json";
static String apiKey = "행정정보공동이용센터 API KEY"; // 행정정보공동이용센터에서 발급
static String cvmis_apikey = "교통안전공단 cvmis_apikey"; // 교통안전공단에서 발급
static boolean useMockResponse = true; // true false
//http Body Setting
static String charset = "UTF-8";
//Gpki Setting
static boolean useGpki = true;
static boolean useSign = true;
static String gpkiCharset = "UTF-8";
static String certServerId = "이용기관 인증서"; // 이용기관 인증서 ID
static String targetServerId = "SVR1611000006"; // 보유기관 인증서 ID(교통안전공단)
public static void main(String[] args) {
N0000001589____SignguCarBassMatterInqireService.executeClient();
}
public static void executeClient(){
String requestBody = makeBody();
String tx_id = getTx_Id();
Map<String, String> requestHeader = new LinkedHashMap<String, String>();
try {
requestHeader.put("Host", InetAddress.getLocalHost().toString());
requestHeader.put("User-Agent", "java-net-httpclient");
} catch (Exception e) {
e.printStackTrace();
}
requestHeader.put("Content-Type", "application/"+requestType);
requestHeader.put("Accept", "application/"+requestType);
requestHeader.put("gpki_yn",useGpki ? "Y":"N");
// requestHeader.put("mock_yn",useMockResponse ? "Y":"N");
requestHeader.put("tx_id", tx_id);
requestHeader.put("cert_server_id",certServerId);
requestHeader.put("api_key", apiKey);
requestHeader.put("cvmis_apikey", cvmis_apikey);
System.out.println("-------- Request Header -----------");
for(Map.Entry<String, String> entry : requestHeader.entrySet()){
System.out.println(entry.getKey() + " : " + entry.getValue());
}
System.out.println("--------- Request Body ------------");
System.out.println(requestBody);
// body 암호화
if(useGpki) {
System.out.println("----- Request Body Encrypt --------");
String bodyEncryptTarget = requestBody;
String bodyEncrypt = "";
try {
bodyEncrypt = gpkiEncrypt(bodyEncryptTarget.toString());
requestBody = requestBody.replace(bodyEncryptTarget, bodyEncrypt);
System.out.println("Encrypt Body : " + requestBody);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("------------ Response -------------");
N0000001589____SignguCarBassMatterInqireService.Response response = sendRequest(targetUrl, method, requestHeader, requestBody, charset);
int responseCode = response.getResponseCode();
Map<String, Object> responseHeader = response.getHeaderMap();
String responseBody = response.getBody();
System.out.println("-> response : " + response);
System.out.println("-> responseCode : " + responseCode);
System.out.println("-> responseHeader : " + responseHeader);
System.out.println("-> responseBody : " + responseBody);
// Body 복호화
if(useGpki && responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("----- Response Body Decrypt --------");
try {
String bodyDecryptTarget = responseBody;
String bodyDecrypt = "";
bodyDecrypt = gpkiDecrypt(bodyDecryptTarget);
responseBody = responseBody.replace(bodyDecryptTarget, bodyDecrypt);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("-------- Response Message ---------");
System.out.println(responseBody);
}
private static Response sendRequest(String targetUrl, String method,
Map<String, String> requestHeader, String body, String charset) {
Response response = new Response();
HttpURLConnection con = null;
try {
URL url = new URL(targetUrl);
con = (HttpURLConnection) url.openConnection();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
try {
for(Map.Entry<String, String> entry : requestHeader.entrySet()){
con.setRequestProperty(entry.getKey(), entry.getValue());
}
con.setRequestMethod(method);
con.setDefaultUseCaches(false);
con.setAllowUserInteraction(false);
con.setReadTimeout(15000);
con.setConnectTimeout(3000);
con.setDoInput(true);
con.setDoOutput(true);
con.connect();
OutputStream os = null;
OutputStreamWriter osw = null;
StringBuffer responseBodyBuffer = new StringBuffer("");
Map<String, Object> headerMap = response.getHeaderMap();
os = con.getOutputStream();
osw = new OutputStreamWriter(os, Charset.forName(charset));
osw.write(body);
osw.flush();
int code = con.getResponseCode();
response.setResponseCode(code);
InputStream is = null;
InputStreamReader isr = null;
if(code == HttpURLConnection.HTTP_OK){
is = con.getInputStream();
}else{
is = con.getErrorStream();
}
Map<String, List<String>> headerFields = con.getHeaderFields();
for(Map.Entry<String, List<String>> headerEntry : headerFields.entrySet()){
String key = headerEntry.getKey();
List<String> values = headerEntry.getValue();
if(values.size()<=1){
headerMap.put(key, values.get(0));
}else{
headerMap.put(key, values);
}
}
try {
isr = new InputStreamReader(is, Charset.forName(charset));
int len = -1;
char[] ch = new char[32];
while((len = isr.read(ch, 0, ch.length))!= -1){
responseBodyBuffer.append(new String(ch, 0, len));
}
response.setBody(responseBodyBuffer.toString());
} catch (IOException e) {
e.printStackTrace();
}finally{
isr.close();
is.close();
os.close();
osw.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(con != null){
con.disconnect();
}
}
return response;
}
private static String makeBody() {
StringBuffer sb = new StringBuffer();
// request data
sb.append("{\n");
sb.append(" \"data\": [\n");
sb.append(" {\n");
sb.append(" \"CNTC_INFO_CODE\": \"\",\n");
sb.append(" \"ONES_INFORMATION_OPEN\": \"\",\n");
sb.append(" \"VHRNO\": \"\",\n");
sb.append(" \"CPTTR_NM\": \"\",\n");
sb.append(" \"CPTTR_IHIDNUM\": \"\",\n");
sb.append(" \"CPTTR_LEGALDONG_CODE\": \"\",\n");
sb.append(" \"ROUTE_SE_CODE\": \"\",\n");
sb.append(" \"DETAIL_EXPRESSION\": \"\",\n");
sb.append(" \"INQIRE_SE_CODE\": \"\"\n");
sb.append(" }\n");
sb.append(" ]\n");
sb.append("}\n");
return sb.toString();
}
// 암호화
private static String gpkiEncrypt(String str) throws Exception {
NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId);
byte[] encrypted = g.encrypt(str.getBytes(gpkiCharset), targetServerId); // 암호화
byte[] signed = encrypted;
if(useSign) {
signed = g.sign(encrypted); // digital sign
}
String encoded = g.encode(signed); // base64 encode
return encoded;
}
// 복호화
public static String gpkiDecrypt(String str) throws Exception {
NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId);
byte[] decode = g.decode(str); // base64 decode
byte[] validate = decode;
if(useSign) {
validate = g.validate(decode);
}
byte[] decrypt = g.decrypt(validate); // 복호화
String decrypted = new String(decrypt,gpkiCharset);
// System.out.println("body(encrypted) : " + str); // Testing log
// System.out.println("body(decrypt) : " + decrypted); // Testing log
return decrypted;
}
private static String getTx_Id() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS",Locale.KOREA);
String cur = sdf.format(new Date());
String transactionUniqueId = cur + keyGen(8);
return transactionUniqueId;
}
private static Random r = new Random(System.currentTimeMillis());
public static String keyGen(int length) {
char[] key = new char[length];
int tmp = 0;
for (int i = 0; i < length; i++) {
tmp = r.nextInt(3);
if (tmp == 0)
key[i] = (char) (r.nextInt(26) + 65);
else if (tmp == 1)
key[i] = (char) (r.nextInt(10) + 48);
else if (tmp == 2)
key[i] = (char) (r.nextInt(26) + 97);
else {
key[i] = (char) r.nextInt(256);
}
}
return String.valueOf(key);
}
/*
* Response
*/
public static class Response {
int responseCode;
String body;
Map<String, Object> headerMap = new LinkedHashMap<String, Object>();
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Map<String, Object> getHeaderMap() {
return headerMap;
}
public void setHeaderMap(Map<String, Object> headerMap) {
this.headerMap = headerMap;
}
}
}

@ -0,0 +1,311 @@
package ___;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import util.NewGpkiUtil;
import util.ShareGpki;
public class N0000002006____SignguCarLedgerFrmbkService {
/**
* IF-LI-AC1-FD11-02-GSR0
* ()
*/
// 이용기관의 네트워크망에 따라서 호출 host 정보가 다름. (네트워크망과 개발 or 운영의 용도에 따라서 해당하는 host로 설정)
// 이용기관 : 행정망
static String host = "10.188.225.94:29001"; // 개발(DEV)
// static String host = "10.188.225.25:29001"; // 운영(OP)
// 이용기관 : 인터넷
// static String host = "116.67.73.153:39011"; // 개발(DEV)
// static String host = "116.67.73.153:29001"; // 운영(OP)
static String targetUrl = "http://"+host+"/piss/api/molit/SignguCarLedgerFrmbkService";
static String method = "POST";
//--------------Client Setting---------------
//http Header Setting
static String requestType = "json";
static String apiKey = "행정정보공동이용센터 API KEY"; // 행정정보공동이용센터에서 발급
static String cvmis_apikey = "교통안전공단 cvmis_apikey"; // 교통안전공단에서 발급
static boolean useMockResponse = true; // true false
//http Body Setting
static String charset = "UTF-8";
//Gpki Setting
static boolean useGpki = true;
static boolean useSign = true;
static String gpkiCharset = "UTF-8";
static String certServerId = "이용기관 인증서"; // 이용기관 인증서 ID
static String targetServerId = "SVR1611000006"; // 보유기관 인증서 ID(교통안전공단)
public static void main(String[] args) {
N0000002006____SignguCarLedgerFrmbkService.executeClient();
}
public static void executeClient(){
String requestBody = makeBody();
String tx_id = getTx_Id();
Map<String, String> requestHeader = new LinkedHashMap<String, String>();
try {
requestHeader.put("Host", InetAddress.getLocalHost().toString());
requestHeader.put("User-Agent", "java-net-httpclient");
} catch (Exception e) {
e.printStackTrace();
}
requestHeader.put("Content-Type", "application/"+requestType);
requestHeader.put("Accept", "application/"+requestType);
requestHeader.put("gpki_yn",useGpki ? "Y":"N");
// requestHeader.put("mock_yn",useMockResponse ? "Y":"N");
requestHeader.put("tx_id", tx_id);
requestHeader.put("cert_server_id",certServerId);
requestHeader.put("api_key", apiKey);
requestHeader.put("cvmis_apikey", cvmis_apikey);
System.out.println("-------- Request Header -----------");
for(Map.Entry<String, String> entry : requestHeader.entrySet()){
System.out.println(entry.getKey() + " : " + entry.getValue());
}
System.out.println("--------- Request Body ------------");
System.out.println(requestBody);
// body 암호화
if(useGpki) {
System.out.println("----- Request Body Encrypt --------");
String bodyEncryptTarget = requestBody;
String bodyEncrypt = "";
try {
bodyEncrypt = gpkiEncrypt(bodyEncryptTarget.toString());
requestBody = requestBody.replace(bodyEncryptTarget, bodyEncrypt);
System.out.println("Encrypt Body : " + requestBody);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("------------ Response -------------");
N0000002006____SignguCarLedgerFrmbkService.Response response = sendRequest(targetUrl, method, requestHeader, requestBody, charset);
int responseCode = response.getResponseCode();
Map<String, Object> responseHeader = response.getHeaderMap();
String responseBody = response.getBody();
System.out.println("-> response : " + response);
System.out.println("-> responseCode : " + responseCode);
System.out.println("-> responseHeader : " + responseHeader);
System.out.println("-> responseBody : " + responseBody);
// Body 복호화
if(useGpki && responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("----- Response Body Decrypt --------");
try {
String bodyDecryptTarget = responseBody;
String bodyDecrypt = "";
bodyDecrypt = gpkiDecrypt(bodyDecryptTarget);
responseBody = responseBody.replace(bodyDecryptTarget, bodyDecrypt);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("-------- Response Message ---------");
System.out.println(responseBody);
}
private static Response sendRequest(String targetUrl, String method,
Map<String, String> requestHeader, String body, String charset) {
Response response = new Response();
HttpURLConnection con = null;
try {
URL url = new URL(targetUrl);
con = (HttpURLConnection) url.openConnection();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
try {
for(Map.Entry<String, String> entry : requestHeader.entrySet()){
con.setRequestProperty(entry.getKey(), entry.getValue());
}
con.setRequestMethod(method);
con.setDefaultUseCaches(false);
con.setAllowUserInteraction(false);
con.setReadTimeout(15000);
con.setConnectTimeout(3000);
con.setDoInput(true);
con.setDoOutput(true);
con.connect();
OutputStream os = null;
OutputStreamWriter osw = null;
StringBuffer responseBodyBuffer = new StringBuffer("");
Map<String, Object> headerMap = response.getHeaderMap();
os = con.getOutputStream();
osw = new OutputStreamWriter(os, Charset.forName(charset));
osw.write(body);
osw.flush();
int code = con.getResponseCode();
response.setResponseCode(code);
InputStream is = null;
InputStreamReader isr = null;
if(code == HttpURLConnection.HTTP_OK){
is = con.getInputStream();
}else{
is = con.getErrorStream();
}
Map<String, List<String>> headerFields = con.getHeaderFields();
for(Map.Entry<String, List<String>> headerEntry : headerFields.entrySet()){
String key = headerEntry.getKey();
List<String> values = headerEntry.getValue();
if(values.size()<=1){
headerMap.put(key, values.get(0));
}else{
headerMap.put(key, values);
}
}
try {
isr = new InputStreamReader(is, Charset.forName(charset));
int len = -1;
char[] ch = new char[32];
while((len = isr.read(ch, 0, ch.length))!= -1){
responseBodyBuffer.append(new String(ch, 0, len));
}
response.setBody(responseBodyBuffer.toString());
} catch (IOException e) {
e.printStackTrace();
}finally{
isr.close();
is.close();
os.close();
osw.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(con != null){
con.disconnect();
}
}
return response;
}
private static String makeBody() {
StringBuffer sb = new StringBuffer();
// request data
sb.append("{\n");
sb.append(" \"data\": [\n");
sb.append(" {\n");
sb.append(" \"CHARGER_ID\": \"\",\n");
sb.append(" \"CHARGER_IP\": \"\",\n");
sb.append(" \"CHARGER_NM\": \"\",\n");
sb.append(" \"REQUST_VHRNO\": \"\"\n");
sb.append(" }\n");
sb.append(" ]\n");
sb.append("}\n");
return sb.toString();
}
// 암호화
private static String gpkiEncrypt(String str) throws Exception {
NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId);
byte[] encrypted = g.encrypt(str.getBytes(gpkiCharset), targetServerId); // 암호화
byte[] signed = encrypted;
if(useSign) {
signed = g.sign(encrypted); // digital sign
}
String encoded = g.encode(signed); // base64 encode
return encoded;
}
// 복호화
public static String gpkiDecrypt(String str) throws Exception {
NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId);
byte[] decode = g.decode(str); // base64 decode
byte[] validate = decode;
if(useSign) {
validate = g.validate(decode);
}
byte[] decrypt = g.decrypt(validate); // 복호화
String decrypted = new String(decrypt,gpkiCharset);
// System.out.println("body(encrypted) : " + str); // Testing log
// System.out.println("body(decrypt) : " + decrypted); // Testing log
return decrypted;
}
private static String getTx_Id() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS",Locale.KOREA);
String cur = sdf.format(new Date());
String transactionUniqueId = cur + keyGen(8);
return transactionUniqueId;
}
private static Random r = new Random(System.currentTimeMillis());
public static String keyGen(int length) {
char[] key = new char[length];
int tmp = 0;
for (int i = 0; i < length; i++) {
tmp = r.nextInt(3);
if (tmp == 0)
key[i] = (char) (r.nextInt(26) + 65);
else if (tmp == 1)
key[i] = (char) (r.nextInt(10) + 48);
else if (tmp == 2)
key[i] = (char) (r.nextInt(26) + 97);
else {
key[i] = (char) r.nextInt(256);
}
}
return String.valueOf(key);
}
/*
* Response
*/
public static class Response {
int responseCode;
String body;
Map<String, Object> headerMap = new LinkedHashMap<String, Object>();
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Map<String, Object> getHeaderMap() {
return headerMap;
}
public void setHeaderMap(Map<String, Object> headerMap) {
this.headerMap = headerMap;
}
}
}

@ -0,0 +1,30 @@
● [이용기관 방화벽 오픈]
- 이용기관에서 방화벽 포트오픈 공문요청 수행
- 방화벽 오픈 가이드(수신처 포함).hwp
● [개발가이드]
- 미래행공 실시간 연계 가이드_이용기관.hwp
- 서비스 별 메세지규격서 (예시: 0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.hwp)
● [API-KEY, 호출 URL 정보]
- 교통안전공단 인터페이스별 행정정보공동이용센터 호출 URL 및 API-KEY정보.hwp
● [교통안전공단 cvmis key]
- 교통안전공단 차세대 사업단으로 문의
● 이용기관 클라이언트(이용기관용Client_시군구.zip) 기관 별 수정사항
(1) apiKey : 행정정보공동이용센터 API KEY 입력
(2) cvmis_apikey : 교통안전공단 cvmis_apikey 입력 (교통안전공단 차세대사업단으로 문의)
(3) certServerId : 이용기관 GPKI 인증서 입력
(4) src/util/ShareGpki.java : 서버에 있는 라이센스(.lic)와 GPKI 인증서 경로 입력
(ShareGpki에 적혀있는 경로는 샘플을 적은것이므로 기관 사정에 맞추어 수정하시면 됩니다)
++ 이용기관이 인터넷망인경우 5,6번 추가작업
(5) host : 116.67.73.153 으로 수정 (주석처리 해놓았으므로 주석변경 하면 됩니다)
(6) com.vmis.interfaceapp.util.NewGpkiUtil.java : ldapUrl 152.99.57.127:389로 수정 (주석처리 해놓았으므로 주석변경 하면 됩니다)
문의 있으시면 아래 번호로 연락 부탁드립니다.
감사합니다.
Loading…
Cancel
Save