외부호출만 있는 프로젝트로 변경 작업 진행중...
parent
298f1fe27f
commit
e128e126be
@ -0,0 +1,195 @@
|
||||
# 자동차 과태료 비교 로직 명세서
|
||||
|
||||
## 개요
|
||||
|
||||
자동차 과태료 부과 대상을 검증하기 위한 비교 로직 정의서입니다.
|
||||
|
||||
### 기본 설정
|
||||
|
||||
- **API 선택**: YML flag 값에 따라 구/신 API 호출 결정
|
||||
- **통합 모델**: 구/신 API 응답을 통일된 model object로 처리
|
||||
- 구 API: 자동차기본정보 API
|
||||
- 신 API: 자동차기본정보 API, 자동차등록원부(갑)
|
||||
- **통합 오브젝트**: 자동차기본정보(구, 신)만 필요
|
||||
|
||||
### 처리 규칙
|
||||
|
||||
> **중요**: 순서가 중요함!
|
||||
> - 조건에 걸리는 순간 다음 차량번호 비교로 진행
|
||||
> - 각 비교 로직별로 개별 API 호출 수행
|
||||
|
||||
---
|
||||
|
||||
## 비교 로직 상세
|
||||
|
||||
### 1. 상품용 검증
|
||||
|
||||
**필요 API**: 자동차등록원부(갑)
|
||||
|
||||
#### API 호출 순서
|
||||
|
||||
| 순서 | API | 입력 파라미터 | 출력 데이터 |
|
||||
|------|-----|--------------|-------------|
|
||||
| 1 | 자동차기본정보 | `차량번호`, `부과일자=검사일` | `차대번호`, `소유자명` |
|
||||
| 2 | 자동차기본정보 | `1.차대번호`, `부과일자=오늘일자` | `차량번호`, `성명`, `민원인주민번호`, `민원인법정동코드` |
|
||||
| 3 | 자동차등록원본(갑) | `2.차량번호`, `2.성명`, `2.민원인주민번호`, `2.민원인법정동코드` | 갑부 상세 List |
|
||||
|
||||
#### 비교 조건
|
||||
|
||||
```java
|
||||
// 조건 1: 소유자명에 '상품용' 포함 여부
|
||||
api.MBER_NM.contains("상품용")
|
||||
|
||||
// 조건 2: 갑부 상세 목록에서 명의이전 이력 확인
|
||||
for (LedgerRecord record : 갑부상세List) {
|
||||
if (record.CHG_YMD >= TB_CAR_FFNLG_TRGT.유효기간만료일
|
||||
&& record.CHG_YMD <= TB_CAR_FFNLG_TRGT.검사종료일자
|
||||
&& record.CHANGE_JOB_SE_CODE == "11") { // 11 = 명의이전 코드
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 결과 처리
|
||||
|
||||
- **비고 컬럼**: `"[상품용] 갑부정보"`
|
||||
|
||||
---
|
||||
|
||||
### 2. 이첩 검증 (이첩-1, 이첩-2 병합 로직)
|
||||
|
||||
**필요 API**: 자동차기본정보
|
||||
|
||||
#### 부과기준일 결정
|
||||
|
||||
```java
|
||||
int dayCnt = TB_CAR_FFNLG_TRGT.DAYCNT; // textFile 일수
|
||||
|
||||
if (dayCnt > 115) {
|
||||
// 이첩-2
|
||||
부과기준일 = TB_CAR_FFNLG_TRGT.검사종료일자.plusDays(115);
|
||||
} else {
|
||||
// 이첩-1
|
||||
부과기준일 = TB_CAR_FFNLG_TRGT.검사일자;
|
||||
}
|
||||
```
|
||||
|
||||
#### API 호출
|
||||
|
||||
```java
|
||||
// 부과기준일 기준으로 자동차기본정보 API 호출
|
||||
BasicResponse response = 자동차기본정보API.call(부과기준일, 차량번호);
|
||||
```
|
||||
|
||||
#### 법정동코드 비교 로직 (공통)
|
||||
|
||||
```java
|
||||
/**
|
||||
* 이첩 조건: 법정동코드 불일치 검증
|
||||
* 사용본거지법정동코드 앞 4자리 != 사용자 조직코드 앞 4자리
|
||||
*/
|
||||
private boolean checkTransferCondition_LegalDongMismatch(
|
||||
BasicResponse.Record basicInfo,
|
||||
String userId,
|
||||
String vhclno
|
||||
) {
|
||||
String useStrnghldLegaldongCode = basicInfo.getUseStrnghldLegaldongCode();
|
||||
|
||||
// 1. 법정동코드 유효성 검사
|
||||
if (useStrnghldLegaldongCode == null || useStrnghldLegaldongCode.length() < 4) {
|
||||
log.debug("[이첩] 법정동코드 없음. 차량번호: {}", vhclno);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 사용자 정보 조회
|
||||
SystemUserVO userInfo = userMapper.selectUser(userId);
|
||||
if (userInfo == null || userInfo.getOrgCd() == null) {
|
||||
log.debug("[이첩] 사용자 정보 없음. 사용자ID: {}", userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 법정동코드 앞 4자리 vs 사용자 조직코드 앞 4자리 비교
|
||||
String legalDong4 = useStrnghldLegaldongCode.substring(0, 4);
|
||||
String userOrgCd = userInfo.getOrgCd();
|
||||
String userOrg4 = userOrgCd.length() >= 4
|
||||
? userOrgCd.substring(0, 4)
|
||||
: userOrgCd;
|
||||
|
||||
// 4. 일치 여부 판단
|
||||
if (legalDong4.equals(userOrg4)) {
|
||||
log.debug("[이첩] 법정동코드 일치. 차량번호: {}, 법정동: {}, 조직: {}",
|
||||
vhclno, legalDong4, userOrg4);
|
||||
return false; // 일치하면 이첩 대상 아님
|
||||
}
|
||||
|
||||
log.info("[이첩] 법정동코드 불일치! 차량번호: {}, 법정동: {}, 조직: {}",
|
||||
vhclno, legalDong4, userOrg4);
|
||||
return true; // 불일치하면 이첩 대상
|
||||
}
|
||||
```
|
||||
|
||||
#### 결과 처리
|
||||
|
||||
| 구분 | 조건 | 비고 컬럼 형식 |
|
||||
|------|------|---------------|
|
||||
| 이첩-1 | `DAYCNT <= 115` | `"서울시 용산구/ 이경호, 검사일사용본거지, [검사대상, 사용자 조직코드 앞 4자리 및 법정동명]"` |
|
||||
| 이첩-2 | `DAYCNT > 115` | `"전라남도 순천시 / 김정대, 115일 도래지, [2개의 api 법정동코드 및 법정동명]"` |
|
||||
|
||||
---
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
### TB_CAR_FFNLG_TRGT (과태료 대상 테이블)
|
||||
|
||||
| 컬럼명 | 설명 | 용도 |
|
||||
|--------|------|------|
|
||||
| 검사일 | 검사 기준일 | API 호출 파라미터 |
|
||||
| 검사종료일자 | 검사 종료 일자 | 115일 계산 기준 |
|
||||
| 유효기간만료일 | 유효기간 만료일 | 상품용 갑부 비교 시작일 |
|
||||
| DAYCNT | textFile 일수 | 이첩-1/2 분기 조건 |
|
||||
| 비고 | 검증 결과 메시지 | 결과 저장 |
|
||||
|
||||
### 코드 정의
|
||||
|
||||
| 코드 | 코드값 | 설명 |
|
||||
|------|--------|------|
|
||||
| CHANGE_JOB_SE_CODE | 11 | 명의이전 |
|
||||
|
||||
---
|
||||
|
||||
## 처리 흐름도
|
||||
|
||||
```
|
||||
시작
|
||||
│
|
||||
▼
|
||||
[차량번호 조회]
|
||||
│
|
||||
▼
|
||||
[1. 상품용 검증] ──(조건 충족)──> [비고 기록] ──> [다음 차량]
|
||||
│
|
||||
│ (조건 미충족)
|
||||
▼
|
||||
[2. DAYCNT 확인]
|
||||
│
|
||||
├─ (> 115) ──> [이첩-2: 115일 도래지 기준]
|
||||
│
|
||||
└─ (<= 115) ──> [이첩-1: 검사일 기준]
|
||||
│
|
||||
▼
|
||||
[법정동코드 비교]
|
||||
│
|
||||
├─ (불일치) ──> [비고 기록] ──> [다음 차량]
|
||||
│
|
||||
└─ (일치) ──> [다음 차량]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 구현 시 주의사항
|
||||
|
||||
1. **API 호출 순서 준수**: 각 검증 단계별로 필요한 API만 호출
|
||||
2. **조건 우선순위**: 상품용 > 이첩 순서로 검증
|
||||
3. **조기 종료**: 조건 충족 시 즉시 다음 차량으로 이동
|
||||
4. **비고 컬럼**: 각 조건별 정해진 형식으로 기록
|
||||
5. **법정동코드 길이 검증**: 최소 4자리 이상 필요
|
||||
Binary file not shown.
@ -1,23 +0,0 @@
|
||||
package go.kr.project.api.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* API 전용 MyBatis Mapper 스캔 설정
|
||||
*
|
||||
* <p>VMIS API 통합 모듈의 Mapper 인터페이스만 스캔합니다.</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>DataSource: egovframework 기본 설정 사용 (DataSourceProxyConfig)</li>
|
||||
* <li>TransactionManager: egovframework 기본 설정 사용 (EgovConfigTransaction.txManager)</li>
|
||||
* <li>SqlSessionFactory: MyBatis Spring Boot Starter가 자동 생성</li>
|
||||
* <li>MapperScan: go.kr.project.api.internal.mapper만 스캔 (API 전용)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>일반 프로젝트의 Mapper는 egovframework 설정에서 별도로 스캔됩니다.</p>
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan(basePackages = "go.kr.project.api.internal.mapper")
|
||||
public class ApiMapperConfig {
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
package go.kr.project.api.config;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import go.kr.project.api.service.VehicleInfoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* VMIS 통합 설정 및 모니터링
|
||||
*
|
||||
* <p>이 설정 클래스는 VMIS 통합 모드에 대한 정보를 제공하고,
|
||||
* 애플리케이션 시작 시 현재 활성화된 모드를 로그로 출력합니다.</p>
|
||||
*
|
||||
* <h3>주요 기능:</h3>
|
||||
* <ul>
|
||||
* <li>애플리케이션 시작 시 VMIS 통합 모드 출력</li>
|
||||
* <li>활성화된 VehicleInfoService 구현체 표시</li>
|
||||
* <li>설정 검증 및 경고 메시지 출력</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>지원하는 모드:</h3>
|
||||
* <ul>
|
||||
* <li>internal: 내부 VMIS 모듈 직접 호출 (InternalVehicleInfoServiceImpl)</li>
|
||||
* <li>external: 외부 REST API 호출 (ExternalVehicleInfoServiceImpl)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class VmisIntegrationConfig {
|
||||
|
||||
private final VmisProperties vmisProperties;
|
||||
|
||||
/**
|
||||
* VMIS 통합 모드 정보 출력
|
||||
*
|
||||
* <p>애플리케이션 시작 시 현재 설정된 VMIS 통합 모드와
|
||||
* 관련 설정 정보를 로그로 출력합니다.</p>
|
||||
*
|
||||
* @param vehicleInfoService 활성화된 VehicleInfoService 구현체
|
||||
* @return CommandLineRunner
|
||||
*/
|
||||
@Bean
|
||||
public CommandLineRunner vmisIntegrationModeLogger(VehicleInfoService vehicleInfoService) {
|
||||
return args -> {
|
||||
String mode = vmisProperties.getIntegration().getMode();
|
||||
String implClass = vehicleInfoService.getClass().getSimpleName();
|
||||
|
||||
log.info("========================================");
|
||||
log.info("VMIS Integration Mode: {}", mode);
|
||||
log.info("Active Implementation: {}", implClass);
|
||||
log.info("========================================");
|
||||
|
||||
if ("internal".equalsIgnoreCase(mode)) {
|
||||
logInternalModeInfo();
|
||||
} else if ("external".equalsIgnoreCase(mode)) {
|
||||
logExternalModeInfo();
|
||||
} else {
|
||||
log.warn("알 수 없는 VMIS 통합 모드: {}. 'internal' 또는 'external'을 사용하세요.", mode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal 모드 설정 정보 출력
|
||||
*/
|
||||
private void logInternalModeInfo() {
|
||||
log.info("[Internal Mode] 내부 VMIS 모듈을 직접 사용합니다");
|
||||
log.info(" - 정부 API 호스트: {}://{}",
|
||||
vmisProperties.getGov().getScheme(),
|
||||
vmisProperties.getGov().getHost());
|
||||
log.info(" - 기본사항 조회 경로: {}",
|
||||
vmisProperties.getGov().getServices().getBasic().getPath());
|
||||
log.info(" - 등록원부 조회 경로: {}",
|
||||
vmisProperties.getGov().getServices().getLedger().getPath());
|
||||
log.info(" - GPKI 암호화: {}",
|
||||
vmisProperties.getGpki().getEnabled());
|
||||
log.info(" - 연결 타임아웃: {}ms",
|
||||
vmisProperties.getRestTemplate().getInternal().getTimeout().getConnectTimeoutMillis());
|
||||
log.info(" - 읽기 타임아웃: {}ms",
|
||||
vmisProperties.getRestTemplate().getInternal().getTimeout().getReadTimeoutMillis());
|
||||
log.info(" - Rate Limit: 초당 {} 건",
|
||||
vmisProperties.getRestTemplate().getInternal().getRateLimit().getPermitsPerSecond());
|
||||
|
||||
if ("Y".equalsIgnoreCase(vmisProperties.getGpki().getEnabled())) {
|
||||
log.info(" - GPKI 인증서 서버 ID: {}",
|
||||
vmisProperties.getGpki().getCertServerId());
|
||||
log.info(" - GPKI 대상 서버 ID: {}",
|
||||
vmisProperties.getGpki().getTargetServerId());
|
||||
} else {
|
||||
log.warn(" - GPKI 암호화가 비활성화되어 있습니다. 개발 환경에서만 사용하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External 모드 설정 정보 출력
|
||||
*/
|
||||
private void logExternalModeInfo() {
|
||||
log.info("[External Mode] 외부 REST API를 사용합니다");
|
||||
log.info(" - 외부 API Base URL: {}",
|
||||
vmisProperties.getExternal().getApi().getUrl().getBase());
|
||||
log.info(" - 연결 타임아웃: {}ms",
|
||||
vmisProperties.getRestTemplate().getExternal().getTimeout().getConnectTimeoutMillis());
|
||||
log.info(" - 읽기 타임아웃: {}ms",
|
||||
vmisProperties.getRestTemplate().getExternal().getTimeout().getReadTimeoutMillis());
|
||||
log.info(" - Rate Limit: 초당 {} 건",
|
||||
vmisProperties.getRestTemplate().getExternal().getRateLimit().getPermitsPerSecond());
|
||||
log.warn(" - 외부 VMIS-interface 서버가 실행 중이어야 합니다.");
|
||||
log.info(" - 기본사항 조회: POST {}",
|
||||
vmisProperties.getExternal().getApi().getUrl().buildBasicUrl());
|
||||
log.info(" - 등록원부 조회: POST {}",
|
||||
vmisProperties.getExternal().getApi().getUrl().buildLedgerUrl());
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package go.kr.project.api.external.service.impl;
|
||||
|
||||
import go.kr.project.api.external.service.ExternalVehicleApiService;
|
||||
import go.kr.project.api.model.VehicleApiResponseVO;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import go.kr.project.api.service.VehicleInfoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 외부 REST API를 호출하는 차량 정보 조회 서비스 구현체
|
||||
*
|
||||
* <p>이 구현체는 외부 VMIS-interface 서버의 REST API를 호출하여
|
||||
* 차량 정보를 조회합니다. 기존 ExternalVehicleApiService를 그대로 활용합니다.</p>
|
||||
*
|
||||
* <h3>활성화 조건:</h3>
|
||||
* <pre>
|
||||
* # application.yml
|
||||
* vmis:
|
||||
* integration:
|
||||
* mode: external
|
||||
* </pre>
|
||||
*
|
||||
* <h3>처리 흐름:</h3>
|
||||
* <ol>
|
||||
* <li>기존 ExternalVehicleApiService에 요청 위임</li>
|
||||
* <li>ExternalVehicleApiService가 외부 REST API 호출</li>
|
||||
* <li>결과를 그대로 반환</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>외부 API 서버:</h3>
|
||||
* <ul>
|
||||
* <li>서버 URL: vmis.external.api.url 설정값 사용</li>
|
||||
* <li>기본값: http://localhost:8081/api/v1/vehicles</li>
|
||||
* <li>별도의 VMIS-interface 프로젝트가 실행 중이어야 함</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see VehicleInfoService
|
||||
* @see ExternalVehicleApiService
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true)
|
||||
public class ExternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl implements VehicleInfoService {
|
||||
|
||||
private final ExternalVehicleApiService externalVehicleApiService;
|
||||
|
||||
@Override
|
||||
public VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest) {
|
||||
String vehicleNumber = basicRequest.getVhrno();
|
||||
log.info("[External Mode] 차량 정보 조회 시작 - 차량번호: {}, 부과기준일: {}, 조회구분: {}",
|
||||
vehicleNumber, basicRequest.getLevyStdde(), basicRequest.getInqireSeCode());
|
||||
|
||||
VehicleApiResponseVO response = externalVehicleApiService.getVehicleInfo(basicRequest);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("[External Mode] 차량번호 {} 조회 성공", vehicleNumber);
|
||||
} else {
|
||||
log.warn("[External Mode] 차량번호 {} 조회 실패 - {}", vehicleNumber, response.getMessage());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 REST - 기본정보 단독 조회
|
||||
* 중요 로직: 외부 API 호출은 ExternalVehicleApiService에 위임 (BasicRequest 전체 전달)
|
||||
*/
|
||||
@Override
|
||||
public BasicResponse getBasicInfo(BasicRequest request) {
|
||||
// 중요 로직: 외부 API 호출은 ExternalVehicleApiService에 위임
|
||||
return externalVehicleApiService.getBasicInfo(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 REST - 등록원부 단독 조회
|
||||
* 중요 로직: 등록원부 조회는 LedgerRequest 전체를 받아서 외부 API에 전달
|
||||
*/
|
||||
@Override
|
||||
public LedgerResponse getLedgerInfo(LedgerRequest request) {
|
||||
// 중요 로직: 외부 API 호출은 ExternalVehicleApiService에 위임
|
||||
return externalVehicleApiService.getLedgerInfo(request);
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package go.kr.project.api.internal.client;
|
||||
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* 정부 시스템 연계 API 추상화 인터페이스.
|
||||
*
|
||||
* <p>외부 정부 시스템과의 통신 계약을 명확히 하여 테스트 용이성과
|
||||
* 추후 교체 가능성을 높입니다.</p>
|
||||
*/
|
||||
public interface GovernmentApi {
|
||||
|
||||
ResponseEntity<Envelope<BasicResponse>> callBasic(Envelope<BasicRequest> envelope);
|
||||
|
||||
ResponseEntity<Envelope<LedgerResponse>> callLedger(Envelope<LedgerRequest> envelope);
|
||||
}
|
||||
@ -1,627 +0,0 @@
|
||||
package go.kr.project.api.internal.client;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import go.kr.project.api.internal.gpki.GpkiService;
|
||||
import go.kr.project.api.internal.util.TxIdUtil;
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 정부 시스템 API 클라이언트
|
||||
*
|
||||
* <p>이 클래스는 시군구연계 자동차 정보 조회를 위해 정부 시스템의 API를 호출하는
|
||||
* 클라이언트 역할을 수행합니다. HTTP 통신, 암호화, 에러 처리 등 정부 API와의
|
||||
* 모든 상호작용을 캡슐화합니다.</p>
|
||||
*
|
||||
* <h3>주요 책임:</h3>
|
||||
* <ul>
|
||||
* <li>정부 API 엔드포인트로 HTTP 요청 전송</li>
|
||||
* <li>GPKI(행정전자서명) 암호화/복호화 처리</li>
|
||||
* <li>필수 HTTP 헤더 구성 및 관리</li>
|
||||
* <li>요청/응답 데이터의 JSON 직렬화/역직렬화</li>
|
||||
* <li>트랜잭션 ID(tx_id) 생성 및 추적</li>
|
||||
* <li>네트워크 오류 및 HTTP 에러 처리</li>
|
||||
* <li>상세한 로깅을 통한 디버깅 지원</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>아키텍처 패턴:</h3>
|
||||
* <ul>
|
||||
* <li>Adapter 패턴: 외부 정부 시스템 API를 내부 인터페이스로 변환</li>
|
||||
* <li>Template Method 패턴: callModel 메서드가 공통 흐름을 정의</li>
|
||||
* <li>Dependency Injection: 생성자를 통한 의존성 주입</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>보안 특성:</h3>
|
||||
* <ul>
|
||||
* <li>GPKI 암호화를 통한 데이터 보안 (선택적 활성화)</li>
|
||||
* <li>API 키 기반 인증</li>
|
||||
* <li>기관 식별 정보(INFO_SYS_ID, REGION_CODE 등)를 헤더에 포함</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see RestTemplate
|
||||
* @see GpkiService
|
||||
* @see VmisProperties
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class GovernmentApiClient implements GovernmentApi {
|
||||
|
||||
/**
|
||||
* Spring RestTemplate (통합 RestTemplate 사용)
|
||||
*
|
||||
* <p>HTTP 클라이언트로서 실제 네트워크 통신을 수행합니다.
|
||||
* 이 객체는 Spring Bean으로 주입되며, 설정에 따라 다음을 포함할 수 있습니다:</p>
|
||||
* <ul>
|
||||
* <li>Connection Timeout 설정</li>
|
||||
* <li>Read Timeout 설정</li>
|
||||
* <li>Connection Pool 관리</li>
|
||||
* <li>Rate Limiting (초당 요청 수 제한)</li>
|
||||
* <li>메시지 컨버터 (Jackson for JSON)</li>
|
||||
* <li>인터셉터 (로깅, 헤더 추가 등)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
/**
|
||||
* VMIS 설정 속성
|
||||
*
|
||||
* <p>application.yml 또는 application.properties에서 로드된 설정값들입니다.
|
||||
* 포함되는 주요 설정:</p>
|
||||
* <ul>
|
||||
* <li>정부 API URL (호스트, 포트, 경로)</li>
|
||||
* <li>API 키 및 인증 정보</li>
|
||||
* <li>시스템 식별 정보 (INFO_SYS_ID, REGION_CODE 등)</li>
|
||||
* <li>GPKI 설정 (인증서 서버 ID 등)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final VmisProperties props;
|
||||
|
||||
/**
|
||||
* GPKI(행정전자서명) 서비스
|
||||
*
|
||||
* <p>정부24 등 공공기관 간 통신에 사용되는 암호화 서비스입니다.
|
||||
* 주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>요청 데이터 암호화 (공개키 암호화)</li>
|
||||
* <li>응답 데이터 복호화 (개인키 복호화)</li>
|
||||
* <li>전자서명 생성 및 검증</li>
|
||||
* <li>암호화 활성화 여부 확인</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>암호화가 비활성화된 경우 평문(Plain Text)으로 통신합니다.</p>
|
||||
*/
|
||||
private final GpkiService gpkiService;
|
||||
|
||||
/**
|
||||
* Jackson ObjectMapper
|
||||
*
|
||||
* <p>Java 객체와 JSON 문자열 간의 변환을 담당합니다.
|
||||
* 주요 역할:</p>
|
||||
* <ul>
|
||||
* <li>요청 객체를 JSON 문자열로 직렬화 (Serialization)</li>
|
||||
* <li>응답 JSON을 Java 객체로 역직렬화 (Deserialization)</li>
|
||||
* <li>제네릭 타입 처리 (TypeReference 사용)</li>
|
||||
* <li>날짜/시간 포맷 변환</li>
|
||||
* <li>null 값 처리</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Spring Boot가 자동 구성한 ObjectMapper를 주입받아 사용하므로
|
||||
* 전역 설정(날짜 포맷, 네이밍 전략 등)이 일관되게 적용됩니다.</p>
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
/**
|
||||
* 서비스 타입 열거형
|
||||
*
|
||||
* <p>정부 API 서비스의 종류를 구분하는 열거형입니다.
|
||||
* 각 서비스 타입은 서로 다른 엔드포인트와 API 키를 가집니다.</p>
|
||||
*
|
||||
* <h4>서비스 타입:</h4>
|
||||
* <ul>
|
||||
* <li>BASIC: 자동차 기본사항 조회 서비스
|
||||
* <ul>
|
||||
* <li>차량번호로 기본 정보(소유자, 차종, 용도 등) 조회</li>
|
||||
* <li>비교적 간단한 정보 제공</li>
|
||||
* <li>응답 속도가 빠름</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>LEDGER: 자동차 등록원부(갑) 조회 서비스
|
||||
* <ul>
|
||||
* <li>상세한 등록 정보 및 법적 권리관계 조회</li>
|
||||
* <li>저당권, 압류, 소유권 이전 이력 등 포함</li>
|
||||
* <li>민감 정보를 포함하여 권한 검증이 엄격함</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum ServiceType {
|
||||
/**
|
||||
* Basic service type.
|
||||
*/
|
||||
BASIC,
|
||||
/**
|
||||
* Ledger service type.
|
||||
*/
|
||||
LEDGER }
|
||||
|
||||
/**
|
||||
* HTTP 헤더 구성
|
||||
*
|
||||
* <p>정부 API 호출에 필요한 모든 HTTP 헤더를 구성하는 private 메서드입니다.
|
||||
* 정부 시스템은 엄격한 헤더 검증을 수행하므로 모든 필수 헤더가 정확히 포함되어야 합니다.</p>
|
||||
*
|
||||
* <h3>헤더 구성 항목:</h3>
|
||||
* <table border="1">
|
||||
* <tr>
|
||||
* <th>헤더명</th>
|
||||
* <th>설명</th>
|
||||
* <th>예시값</th>
|
||||
* <th>필수여부</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Content-Type</td>
|
||||
* <td>요청 바디의 미디어 타입 및 문자 인코딩</td>
|
||||
* <td>application/json; charset=UTF-8</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Accept</td>
|
||||
* <td>클라이언트가 수용 가능한 응답 형식</td>
|
||||
* <td>application/json</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>gpki_yn</td>
|
||||
* <td>GPKI 암호화 사용 여부 (Y/N)</td>
|
||||
* <td>Y</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>tx_id</td>
|
||||
* <td>트랜잭션 고유 ID (요청 추적용)</td>
|
||||
* <td>20250104123045_abc123</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>cert_server_id</td>
|
||||
* <td>인증서 서버 식별자</td>
|
||||
* <td>VMIS_SERVER_01</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>api_key</td>
|
||||
* <td>서비스별 API 인증 키</td>
|
||||
* <td>abc123def456...</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>cvmis_apikey</td>
|
||||
* <td>CVMIS 시스템 API 키</td>
|
||||
* <td>xyz789uvw012...</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>INFO_SYS_ID</td>
|
||||
* <td>정보시스템 식별자</td>
|
||||
* <td>VMIS_SEOUL</td>
|
||||
* <td>필수</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* <h3>문자 인코딩:</h3>
|
||||
* <ul>
|
||||
* <li>Content-Type에 UTF-8 인코딩을 명시적으로 지정</li>
|
||||
* <li>한글 데이터 처리를 위해 필수</li>
|
||||
* <li>정부 시스템이 다양한 클라이언트와 호환되도록 표준 인코딩 사용</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>보안 고려사항:</h3>
|
||||
* <ul>
|
||||
* <li>API 키는 설정 파일에서 안전하게 관리</li>
|
||||
* <li>로그에 API 키가 노출되지 않도록 주의</li>
|
||||
* <li>각 서비스(BASIC, LEDGER)마다 별도의 API 키 사용 가능</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param svc 서비스 설정 객체 (API 키, 경로 등 포함)
|
||||
* @param txId 이번 요청의 트랜잭션 ID
|
||||
* @return HttpHeaders 구성된 HTTP 헤더 객체
|
||||
*/
|
||||
private HttpHeaders buildHeaders(VmisProperties.GovProps.Service svc, String txId) {
|
||||
// 1. 빈 HttpHeaders 객체 생성
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
// 2. Content-Type 설정
|
||||
// UTF-8 인코딩을 명시하여 한글 데이터가 올바르게 전송되도록 함
|
||||
headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
|
||||
|
||||
// 3. Accept 헤더 설정
|
||||
// 서버에게 JSON 형식의 응답을 요청함
|
||||
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
// 4. GPKI 암호화 사용 여부
|
||||
// 정부 서버가 요청 바디 복호화 여부를 결정하는 데 사용
|
||||
headers.add("gpki_yn", gpkiService.isEnabled() ? "Y" : "N");
|
||||
|
||||
// 5. 트랜잭션 ID
|
||||
// 요청 추적, 로그 연관, 문제 해결 시 사용
|
||||
headers.add("tx_id", txId);
|
||||
|
||||
// 6. 인증서 서버 ID
|
||||
// GPKI 인증서를 발급받은 서버의 식별자
|
||||
headers.add("cert_server_id", props.getGpki().getCertServerId());
|
||||
|
||||
// 7. API 인증 키
|
||||
// 서비스별로 다른 API 키 사용 가능 (BASIC과 LEDGER 각각)
|
||||
headers.add("api_key", svc.getApiKey());
|
||||
|
||||
// 8. CVMIS API 키
|
||||
// CVMIS(Car Vehicle Management Information System) 전용 API 키
|
||||
headers.add("cvmis_apikey", svc.getCvmisApikey());
|
||||
|
||||
// 구성 완료된 헤더 반환
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동차 기본사항 조회 API 호출
|
||||
*
|
||||
* <p>타입 안전성이 보장되는 자동차 기본사항 조회 메서드입니다.
|
||||
* 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.</p>
|
||||
*
|
||||
* <h3>특징:</h3>
|
||||
* <ul>
|
||||
* <li>제네릭 타입으로 컴파일 타입 타입 체크</li>
|
||||
* <li>요청/응답 객체가 Envelope로 감싸져 있음</li>
|
||||
* <li>Jackson TypeReference를 사용한 제네릭 역직렬화</li>
|
||||
* <li>API 호출 전후로 DB에 로그성 데이터 저장</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>처리 흐름:</h3>
|
||||
* <ol>
|
||||
* <li>요청 정보를 DB에 INSERT (로그 저장)</li>
|
||||
* <li>정부 API 호출</li>
|
||||
* <li>응답 정보를 DB에 UPDATE</li>
|
||||
* <li>에러 발생 시 에러 정보도 DB에 UPDATE</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>사용 예시:</h3>
|
||||
* <pre>
|
||||
* BasicRequest request = new BasicRequest();
|
||||
* request.setVehicleNo("12가3456");
|
||||
*
|
||||
* Envelope<BasicRequest> envelope = new Envelope<>();
|
||||
* envelope.setData(request);
|
||||
*
|
||||
* ResponseEntity<Envelope<BasicResponse>> response = govClient.callBasic(envelope);
|
||||
* BasicResponse data = response.getBody().getData();
|
||||
* </pre>
|
||||
*
|
||||
* @param envelope 자동차 기본사항 조회 요청을 담은 Envelope
|
||||
* @return ResponseEntity<Envelope<BasicResponse>> 조회 결과를 담은 응답
|
||||
*/
|
||||
public ResponseEntity<Envelope<BasicResponse>> callBasic(Envelope<BasicRequest> envelope) {
|
||||
// 순수한 전송 책임만 수행: DB 로깅은 서비스 레이어에서 처리
|
||||
return callModel(ServiceType.BASIC, envelope, new TypeReference<Envelope<BasicResponse>>(){});
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동차 등록원부(갑) 조회 API 호출
|
||||
*
|
||||
* <p>타입 안전성이 보장되는 자동차 등록원부 조회 메서드입니다.
|
||||
* 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.</p>
|
||||
*
|
||||
* <h3>특징:</h3>
|
||||
* <ul>
|
||||
* <li>제네릭 타입으로 컴파일 타임 타입 체크</li>
|
||||
* <li>요청/응답 객체가 Envelope로 감싸져 있음</li>
|
||||
* <li>Jackson TypeReference를 사용한 제네릭 역직렬화</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>사용 예시:</h3>
|
||||
* <pre>
|
||||
* LedgerRequest request = new LedgerRequest();
|
||||
* request.setVehicleNo("12가3456");
|
||||
* request.setOwnerName("홍길동");
|
||||
*
|
||||
* Envelope<LedgerRequest> envelope = new Envelope<>();
|
||||
* envelope.setData(request);
|
||||
*
|
||||
* ResponseEntity<Envelope<LedgerResponse>> response = govClient.callLedger(envelope);
|
||||
* LedgerResponse data = response.getBody().getData();
|
||||
* </pre>
|
||||
*
|
||||
* @param envelope 자동차 등록원부 조회 요청을 담은 Envelope
|
||||
* @return ResponseEntity<Envelope<LedgerResponse>> 조회 결과를 담은 응답
|
||||
*/
|
||||
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><TReq>: 요청 데이터 타입 (BasicRequest 또는 LedgerRequest)</li>
|
||||
* <li><TResp>: 응답 데이터 타입 (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<TReq>)를 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<TResp>)로 변환</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<Envelope<TResp>> 응답 데이터를 담은 ResponseEntity
|
||||
* @throws RuntimeException JSON 직렬화/역직렬화 실패, 네트워크 오류, GPKI 암복호화 실패 등
|
||||
*/
|
||||
private <TReq, TResp> ResponseEntity<Envelope<TResp>> callModel(ServiceType type,
|
||||
Envelope<TReq> envelope,
|
||||
TypeReference<Envelope<TResp>> respType) {
|
||||
// 1. 서비스 타입에 따른 설정 로드
|
||||
VmisProperties.GovProps gov = props.getGov();
|
||||
VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC)
|
||||
? gov.getServices().getBasic()
|
||||
: gov.getServices().getLedger();
|
||||
|
||||
// 2. URL 및 트랜잭션 ID 생성
|
||||
String url = gov.buildServiceUrl(svc.getPath());
|
||||
String txId = TxIdUtil.generate();
|
||||
|
||||
try {
|
||||
// 3. 직렬화: Java 객체 → JSON 문자열
|
||||
// ObjectMapper가 Envelope 객체를 JSON으로 변환
|
||||
// 날짜, null 값 등의 처리는 ObjectMapper 설정에 따름
|
||||
String jsonBody = objectMapper.writeValueAsString(envelope);
|
||||
|
||||
// 4. HTTP 헤더 구성
|
||||
HttpHeaders headers = buildHeaders(svc, txId);
|
||||
|
||||
// 5. GPKI 암호화 처리
|
||||
String bodyToSend = jsonBody;
|
||||
if (gpkiService.isEnabled()) {
|
||||
// JSON 평문을 암호화된 문자열로 변환
|
||||
bodyToSend = gpkiEncrypt(jsonBody);
|
||||
}
|
||||
|
||||
// 6. HTTP 엔티티 생성 (헤더 + 바디)
|
||||
HttpEntity<String> request = new HttpEntity<>(bodyToSend, headers);
|
||||
|
||||
// 7. 요청 로그 기록
|
||||
log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0);
|
||||
|
||||
// 8. 실제 HTTP POST 요청 전송
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
|
||||
String respBody = response.getBody();
|
||||
|
||||
// 9. GPKI 복호화 처리 (성공 응답인 경우만)
|
||||
if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) {
|
||||
// 암호화된 응답을 평문 JSON으로 복호화
|
||||
respBody = gpkiDecrypt(respBody);
|
||||
}
|
||||
|
||||
// 10. 역직렬화: JSON 문자열 → Java 객체
|
||||
// TypeReference를 사용하여 제네릭 타입 정보 전달
|
||||
Envelope<TResp> mapped = objectMapper.readValue(respBody, respType);
|
||||
|
||||
// 11. 응답 반환 (상태 코드, 헤더, 바디 모두 포함)
|
||||
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(mapped);
|
||||
|
||||
} catch (HttpStatusCodeException ex) {
|
||||
// HTTP 에러 처리 (4xx, 5xx)
|
||||
log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString());
|
||||
|
||||
// 에러 응답 바디 파싱 시도
|
||||
// 정부 API는 에러 응답도 Envelope 형식으로 반환할 수 있음
|
||||
Envelope<TResp> empty = new Envelope<>();
|
||||
try {
|
||||
// 에러 응답을 Envelope 객체로 파싱
|
||||
Envelope<TResp> parsed = objectMapper.readValue(ex.getResponseBodyAsString(), respType);
|
||||
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(parsed);
|
||||
} catch (Exception parseEx) {
|
||||
// 파싱 실패 시 빈 Envelope 반환
|
||||
// 호출자는 HTTP 상태 코드로 에러 판단 가능
|
||||
return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(empty);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 기타 모든 예외 (네트워크 오류, JSON 파싱 오류, GPKI 오류 등)
|
||||
// RuntimeException으로 래핑하여 상위로 전파
|
||||
// Spring의 @ExceptionHandler에서 처리 가능
|
||||
throw new RuntimeException("정부 API 호출 중 오류", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GPKI 암호화 처리
|
||||
*
|
||||
* <p>평문 JSON 문자열을 GPKI(행정전자서명) 알고리즘으로 암호화하는 헬퍼 메서드입니다.
|
||||
* 암호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.</p>
|
||||
*
|
||||
* <h3>암호화 과정:</h3>
|
||||
* <ol>
|
||||
* <li>평문 JSON 문자열을 바이트 배열로 변환</li>
|
||||
* <li>정부 시스템의 공개키를 사용하여 암호화</li>
|
||||
* <li>암호화된 바이트 배열을 Base64로 인코딩</li>
|
||||
* <li>Base64 문자열 반환</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>에러 처리:</h3>
|
||||
* <ul>
|
||||
* <li>인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우</li>
|
||||
* <li>암호화 오류: 암호화 알고리즘 실행 중 오류 발생</li>
|
||||
* <li>인코딩 오류: Base64 인코딩 실패</li>
|
||||
* <li>모든 예외는 RuntimeException으로 래핑하여 즉시 중단</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>보안 고려사항:</h3>
|
||||
* <ul>
|
||||
* <li>공개키 암호화 방식 사용 (비대칭키)</li>
|
||||
* <li>정부 시스템만 개인키로 복호화 가능</li>
|
||||
* <li>민감한 개인정보 보호</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param jsonBody 암호화할 평문 JSON 문자열
|
||||
* @return String Base64로 인코딩된 암호화 문자열
|
||||
* @throws RuntimeException GPKI 암호화 실패 시
|
||||
*/
|
||||
private String gpkiEncrypt(String jsonBody) {
|
||||
try {
|
||||
// GpkiService에 암호화 위임
|
||||
// 실제 암호화 로직은 GpkiService가 캡슐화
|
||||
return gpkiService.encrypt(jsonBody);
|
||||
} catch (Exception e) {
|
||||
// 암호화 실패는 치명적 오류
|
||||
// 평문 데이터를 전송할 수 없으므로 즉시 중단
|
||||
throw new RuntimeException("GPKI 암호화 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GPKI 복호화 처리
|
||||
*
|
||||
* <p>암호화된 응답 문자열을 GPKI(행정전자서명) 알고리즘으로 복호화하는 헬퍼 메서드입니다.
|
||||
* 복호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.</p>
|
||||
*
|
||||
* <h3>복호화 과정:</h3>
|
||||
* <ol>
|
||||
* <li>Base64로 인코딩된 암호문을 바이트 배열로 디코딩</li>
|
||||
* <li>우리 시스템의 개인키를 사용하여 복호화</li>
|
||||
* <li>복호화된 바이트 배열을 UTF-8 문자열로 변환</li>
|
||||
* <li>평문 JSON 문자열 반환</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>에러 처리:</h3>
|
||||
* <ul>
|
||||
* <li>인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우</li>
|
||||
* <li>복호화 오류: 암호문이 손상되었거나 올바르지 않은 경우</li>
|
||||
* <li>디코딩 오류: Base64 디코딩 실패</li>
|
||||
* <li>문자 인코딩 오류: UTF-8 변환 실패</li>
|
||||
* <li>모든 예외는 RuntimeException으로 래핑하여 즉시 중단</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>보안 고려사항:</h3>
|
||||
* <ul>
|
||||
* <li>개인키 암호화 방식 사용 (비대칭키)</li>
|
||||
* <li>개인키는 안전하게 저장 및 관리 필요</li>
|
||||
* <li>복호화 실패 시 상세 에러 정보 로깅 주의 (정보 유출 방지)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param cipher Base64로 인코딩된 암호화 문자열
|
||||
* @return String 복호화된 평문 JSON 문자열
|
||||
* @throws RuntimeException GPKI 복호화 실패 시
|
||||
*/
|
||||
private String gpkiDecrypt(String cipher) {
|
||||
try {
|
||||
// GpkiService에 복호화 위임
|
||||
// 실제 복호화 로직은 GpkiService가 캡슐화
|
||||
return gpkiService.decrypt(cipher);
|
||||
} catch (Exception e) {
|
||||
// 복호화 실패는 치명적 오류
|
||||
// 암호문을 해석할 수 없으므로 즉시 중단
|
||||
throw new RuntimeException("GPKI 복호화 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
package go.kr.project.api.internal.config;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import go.kr.project.api.internal.gpki.GpkiService;
|
||||
import go.kr.project.api.internal.gpki.NoopGpkiService;
|
||||
import go.kr.project.api.internal.gpki.RealGpkiService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class GpkiConfig {
|
||||
|
||||
@Bean
|
||||
public GpkiService gpkiService(VmisProperties properties) {
|
||||
if (properties.getGpki().isEnabledFlag()) {
|
||||
return new RealGpkiService(properties);
|
||||
}
|
||||
return new NoopGpkiService();
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package go.kr.project.api.internal.config;
|
||||
|
||||
import io.swagger.v3.oas.models.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI vmisOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("VMIS Interface API")
|
||||
.description("시군구연계 자동차 정보 인터페이스 API (자망연계)")
|
||||
.version("v0.1.0")
|
||||
.contact(new Contact().name("VMIS").email("support@example.com"))
|
||||
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0.html")))
|
||||
.externalDocs(new ExternalDocumentation()
|
||||
.description("Reference")
|
||||
.url(""));
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package go.kr.project.api.internal.config;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(VmisProperties.class)
|
||||
public class PropertiesConfig {
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package go.kr.project.api.internal.gpki;
|
||||
|
||||
public interface GpkiService {
|
||||
String encrypt(String plain) throws Exception;
|
||||
String decrypt(String cipher) throws Exception;
|
||||
boolean isEnabled();
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package go.kr.project.api.internal.gpki;
|
||||
|
||||
public class NoopGpkiService implements GpkiService {
|
||||
@Override
|
||||
public String encrypt(String plain) {
|
||||
return plain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String cipher) {
|
||||
return cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package go.kr.project.api.internal.gpki;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import go.kr.project.api.internal.util.GpkiCryptoUtil;
|
||||
|
||||
/**
|
||||
* Real GPKI service backed by native GPKI JNI via legacy NewGpkiUtil wrapper.
|
||||
* Uses YAML-configured paths and options in {@link VmisProperties.GpkiProps}.
|
||||
*/
|
||||
public class RealGpkiService implements GpkiService {
|
||||
|
||||
private final VmisProperties props;
|
||||
private final GpkiCryptoUtil crypto;
|
||||
|
||||
public RealGpkiService(VmisProperties props) {
|
||||
this.props = props;
|
||||
try {
|
||||
this.crypto = GpkiCryptoUtil.from(props.getGpki());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to initialize GPKI (JNI) util. Check YAML paths/passwords and license.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String plain) throws Exception {
|
||||
String charset = props.getGpki().getCharset();
|
||||
String targetId = props.getGpki().getTargetServerId();
|
||||
return crypto.encryptToBase64(plain, targetId, charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String cipher) throws Exception {
|
||||
String charset = props.getGpki().getCharset();
|
||||
return crypto.decryptFromBase64(cipher, charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package go.kr.project.api.internal.service;
|
||||
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* 자동차 기본사항 조회 서비스 인터페이스
|
||||
*
|
||||
* <p>API 호출 정보를 관리하는 서비스입니다.</p>
|
||||
* <ul>
|
||||
* <li>요청 데이터 보강</li>
|
||||
* <li>최초 요청 로그 저장</li>
|
||||
* <li>외부 API 호출</li>
|
||||
* <li>응답 로그 업데이트</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface VmisCarBassMatterInqireService {
|
||||
|
||||
/**
|
||||
* 자동차 기본사항 조회
|
||||
*
|
||||
* @param envelope 요청 Envelope
|
||||
* @return 응답 Envelope
|
||||
*/
|
||||
ResponseEntity<Envelope<BasicResponse>> basic(Envelope<BasicRequest> envelope);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package go.kr.project.api.internal.service;
|
||||
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* 자동차 등록 원부(갑) 서비스 인터페이스
|
||||
* - 요청 보강, 외부 API 호출, 로그 서비스 위임
|
||||
*/
|
||||
public interface VmisCarLedgerFrmbkService {
|
||||
|
||||
/**
|
||||
* 자동차 등록원부(갑) 조회
|
||||
*
|
||||
* @param envelope 요청 Envelope
|
||||
* @return 응답 Envelope
|
||||
*/
|
||||
ResponseEntity<Envelope<LedgerResponse>> ledger(Envelope<LedgerRequest> envelope);
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
package go.kr.project.api.internal.service;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Populates incoming request models with values from YAML configuration.
|
||||
* Unconditionally overwrites the listed fields per requirement:
|
||||
* - INFO_SYS_ID, INFO_SYS_IP, SIGUNGU_CODE
|
||||
* - CNTC_INFO_CODE (service specific)
|
||||
* - CHARGER_ID, CHARGER_IP, CHARGER_NM
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VmisRequestEnricher {
|
||||
|
||||
private final VmisProperties props;
|
||||
|
||||
public VmisRequestEnricher(VmisProperties props) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
public void enrichBasic(Envelope<BasicRequest> envelope) {
|
||||
if (envelope == null || envelope.getData() == null) return;
|
||||
VmisProperties.SystemProps sys = props.getSystem();
|
||||
String cntc = props.getGov().getServices().getBasic().getCntcInfoCode();
|
||||
for (BasicRequest req : envelope.getData()) {
|
||||
if (req == null) continue;
|
||||
req.setInfoSysId(sys.getInfoSysId());
|
||||
req.setInfoSysIp(sys.getInfoSysIp());
|
||||
req.setSigunguCode(sys.getSigunguCode());
|
||||
req.setCntcInfoCode(cntc);
|
||||
req.setChargerId(sys.getChargerId());
|
||||
req.setChargerIp(sys.getChargerIp());
|
||||
req.setChargerNm(sys.getChargerNm());
|
||||
|
||||
// 조회구분코드 자동 설정: VHRNO가 있으면 "3" (자동차번호), VIN이 있으면 "2" (차대번호)
|
||||
if (req.getInqireSeCode() == null) {
|
||||
if (req.getVhrno() != null && !req.getVhrno().trim().isEmpty()) {
|
||||
req.setInqireSeCode("3"); // 자동차번호로 조회
|
||||
} else if (req.getVin() != null && !req.getVin().trim().isEmpty()) {
|
||||
req.setInqireSeCode("2"); // 차대번호로 조회
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug("[ENRICH] basic: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
|
||||
sys.getInfoSysId(), sys.getInfoSysIp(), sys.getSigunguCode(), cntc);
|
||||
}
|
||||
|
||||
public void enrichLedger(Envelope<LedgerRequest> envelope) {
|
||||
if (envelope == null || envelope.getData() == null) return;
|
||||
VmisProperties.SystemProps sys = props.getSystem();
|
||||
String cntc = props.getGov().getServices().getLedger().getCntcInfoCode();
|
||||
for (LedgerRequest req : envelope.getData()) {
|
||||
if (req == null) continue;
|
||||
req.setInfoSysId(sys.getInfoSysId());
|
||||
req.setInfoSysIp(sys.getInfoSysIp());
|
||||
req.setSigunguCode(sys.getSigunguCode());
|
||||
req.setCntcInfoCode(cntc);
|
||||
req.setChargerId(sys.getChargerId());
|
||||
req.setChargerIp(sys.getChargerIp());
|
||||
req.setChargerNm(sys.getChargerNm());
|
||||
|
||||
// 고정값 설정 (값이 없는 경우에만 설정)
|
||||
if (req.getOnesInformationOpen() == null || req.getOnesInformationOpen().isEmpty()) {
|
||||
req.setOnesInformationOpen("1"); // 개인정보공개 (소유자공개)
|
||||
}
|
||||
if (req.getRouteSeCode() == null || req.getRouteSeCode().isEmpty()) {
|
||||
req.setRouteSeCode("3"); // 경로구분코드
|
||||
}
|
||||
if (req.getDetailExpression() == null || req.getDetailExpression().isEmpty()) {
|
||||
req.setDetailExpression("1"); // 내역표시 (전체내역)
|
||||
}
|
||||
if (req.getInqireSeCode() == null || req.getInqireSeCode().isEmpty()) {
|
||||
req.setInqireSeCode("1"); // 조회구분코드 (열람)
|
||||
}
|
||||
}
|
||||
log.debug("[ENRICH] ledger: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}",
|
||||
sys.getInfoSysId(), sys.getInfoSysIp(), sys.getSigunguCode(), cntc);
|
||||
}
|
||||
}
|
||||
@ -1,213 +0,0 @@
|
||||
package go.kr.project.api.internal.service.impl;
|
||||
|
||||
import go.kr.project.api.config.ApiConstant;
|
||||
import go.kr.project.api.internal.service.VmisCarBassMatterInqireService;
|
||||
import go.kr.project.api.internal.service.VmisCarLedgerFrmbkService;
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.VehicleApiResponseVO;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import go.kr.project.api.service.VehicleInfoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 내부 VMIS 모듈을 직접 호출하는 차량 정보 조회 서비스 구현체
|
||||
*
|
||||
* <p>이 구현체는 외부 REST API 호출 없이 내부 VMIS 모듈을 직접 사용하여
|
||||
* 정부 시스템과 통신합니다. 네트워크 오버헤드가 없어 성능이 향상됩니다.</p>
|
||||
*
|
||||
* <h3>활성화 조건:</h3>
|
||||
* <pre>
|
||||
* # application.yml
|
||||
* vmis:
|
||||
* integration:
|
||||
* mode: internal
|
||||
* </pre>
|
||||
*
|
||||
* <h3>처리 흐름:</h3>
|
||||
* <ol>
|
||||
* <li>차량번호를 받아 BasicRequest, LedgerRequest 생성</li>
|
||||
* <li>VmisCarBassMatterInqireService.basic() 호출 (기본정보)</li>
|
||||
* <li>VmisCarLedgerFrmbkService.ledger() 호출 (등록원부)</li>
|
||||
* <li>BasicResponse, LedgerResponse를 직접 VehicleApiResponseVO에 설정</li>
|
||||
* <li>VehicleApiResponseVO로 결과 반환</li>
|
||||
* </ol>
|
||||
*
|
||||
* @see VehicleInfoService
|
||||
* @see VmisCarBassMatterInqireService
|
||||
* @see VmisCarLedgerFrmbkService
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal")
|
||||
public class InternalVehicleInfoServiceImpl extends EgovAbstractServiceImpl implements VehicleInfoService {
|
||||
|
||||
private final VmisCarBassMatterInqireService carBassMatterInqireService;
|
||||
private final VmisCarLedgerFrmbkService carLedgerFrmbkService;
|
||||
private final go.kr.project.carInspectionPenalty.history.service.VehicleApiHistoryService vehicleApiHistoryService;
|
||||
|
||||
|
||||
@Override
|
||||
public VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest) {
|
||||
String vehicleNumber = basicRequest.getVhrno();
|
||||
log.info("[Internal Mode] 차량 정보 조회 시작 - 차량번호: {}, 부과기준일: {}, 조회구분: {}",
|
||||
vehicleNumber, basicRequest.getLevyStdde(), basicRequest.getInqireSeCode());
|
||||
|
||||
VehicleApiResponseVO response = new VehicleApiResponseVO();
|
||||
response.setVhrno(vehicleNumber);
|
||||
|
||||
try {
|
||||
// 1. 차량 기본정보 조회
|
||||
// 중요 로직: BasicRequest 전체를 사용하여 조회 (RequestEnricher가 나머지 채움)
|
||||
BasicResponse basicInfo = getBasicInfo(basicRequest);
|
||||
response.setBasicInfo(basicInfo);
|
||||
|
||||
// 2. 자동차 등록원부 조회
|
||||
// 중요 로직: 통합 조회 시에는 차량번호와 기본정보를 바탕으로 LedgerRequest 생성 (RequestEnricher가 나머지 채움)
|
||||
LedgerRequest ledgerRequest = new LedgerRequest();
|
||||
ledgerRequest.setVhrno(vehicleNumber);
|
||||
ledgerRequest.setOnesInformationOpen("1"); //개인정보공개 {1:소유자공개, 2:비공개, 3:비공개(주민등록번호), 4:비공개(사용본거지)}
|
||||
|
||||
// basicInfo에서 민원인 정보 가져오기
|
||||
if (basicInfo != null && basicInfo.getRecord() != null && !basicInfo.getRecord().isEmpty()) {
|
||||
BasicResponse.Record record = basicInfo.getRecord().get(0);
|
||||
ledgerRequest.setCpttrNm(record.getMberNm()); // 민원인성명
|
||||
ledgerRequest.setCpttrIhidnum(record.getMberSeNo()); // 민원인주민번호
|
||||
}
|
||||
|
||||
// 고정값 설정
|
||||
ledgerRequest.setCpttrLegaldongCode(null); // 민원인법정동코드
|
||||
|
||||
LedgerResponse ledgerInfo = getLedgerInfo(ledgerRequest);
|
||||
response.setLedgerInfo(ledgerInfo);
|
||||
|
||||
// 3. 결과 검증
|
||||
if (basicInfo != null && ApiConstant.CNTC_RESULT_CODE_SUCCESS.equals(basicInfo.getCntcResultCode())) {
|
||||
response.setSuccess(true);
|
||||
response.setMessage("조회 성공");
|
||||
log.info("[Internal Mode] 차량번호 {} 조회 성공", vehicleNumber);
|
||||
|
||||
// 4. API 호출 성공 시 히스토리 ID 조회 및 설정
|
||||
try {
|
||||
String carBassMatterInqireId = vehicleApiHistoryService.selectLatestCarBassMatterInqireIdByVhclno(vehicleNumber);
|
||||
String carLedgerFrmbkId = vehicleApiHistoryService.selectLatestCarLedgerFrmbkIdByVhclno(vehicleNumber);
|
||||
response.setCarBassMatterInqireId(carBassMatterInqireId);
|
||||
response.setCarLedgerFrmbkId(carLedgerFrmbkId);
|
||||
log.debug("[Internal Mode] 히스토리 ID 설정 완료 - 차량번호: {}, 기본정보ID: {}, 원부ID: {}",
|
||||
vehicleNumber, carBassMatterInqireId, carLedgerFrmbkId);
|
||||
} catch (Exception e) {
|
||||
log.warn("[Internal Mode] 히스토리 ID 조회 실패 - 차량번호: {}", vehicleNumber, e);
|
||||
// ID 조회 실패는 치명적이지 않으므로 계속 진행
|
||||
}
|
||||
} else {
|
||||
response.setSuccess(false);
|
||||
response.setMessage(basicInfo != null ? basicInfo.getCntcResultDtls() : "조회 실패");
|
||||
log.warn("[Internal Mode] 차량번호 {} 조회 실패 - {}", vehicleNumber, response.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
response.setSuccess(false);
|
||||
response.setMessage("내부 API 호출 오류: " + e.getMessage());
|
||||
log.error("[Internal Mode] 차량번호 {} 내부 API 호출 중 오류 발생", vehicleNumber, e);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 차량 기본정보 조회 (내부 모듈 직접 호출)
|
||||
* 중요 로직: 기본정보 조회는 BasicRequest 전체를 받아서 내부 서비스에 전달
|
||||
*
|
||||
* @param request 기본정보 조회 요청 (차량번호, 부과기준일, 조회구분 등 포함)
|
||||
* @return 차량 기본정보 응답
|
||||
*/
|
||||
@Override
|
||||
public BasicResponse getBasicInfo(BasicRequest request) {
|
||||
log.debug("[Internal Mode] 차량 기본정보 조회 - 차량번호: {}", request.getVhrno());
|
||||
|
||||
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
|
||||
Envelope<BasicRequest> requestEnvelope = new Envelope<BasicRequest>();
|
||||
requestEnvelope.setData(Collections.singletonList(request));
|
||||
|
||||
try {
|
||||
// 내부 서비스 호출
|
||||
ResponseEntity<Envelope<BasicResponse>> responseEntity =
|
||||
carBassMatterInqireService.basic(requestEnvelope);
|
||||
|
||||
if (responseEntity.getBody() != null &&
|
||||
responseEntity.getBody().getData() != null &&
|
||||
!responseEntity.getBody().getData().isEmpty()) {
|
||||
|
||||
return responseEntity.getBody().getData().get(0);
|
||||
}
|
||||
|
||||
log.warn("[Internal Mode] 차량 기본정보 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Internal Mode] 차량 기본정보 조회 실패 - 차량번호: {}", request.getVhrno(), e);
|
||||
throw new RuntimeException("차량 기본정보 조회 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동차 등록원부(갑) 조회 (내부 모듈 직접 호출)
|
||||
* 중요 로직: 등록원부 조회는 LedgerRequest 전체를 받아서 내부 서비스에 전달
|
||||
*
|
||||
* @param request 등록원부 조회 요청 (차량번호, 소유자정보, 조회구분 등 포함)
|
||||
* @return 등록원부 정보 응답
|
||||
*/
|
||||
@Override
|
||||
public LedgerResponse getLedgerInfo(LedgerRequest request) {
|
||||
log.debug("[Internal Mode] 자동차 등록원부 조회 - 차량번호: {}", request.getVhrno());
|
||||
|
||||
// Envelope로 감싸기 (요청 객체는 이미 모든 필수 파라미터를 포함)
|
||||
Envelope<LedgerRequest> requestEnvelope = new Envelope<LedgerRequest>();
|
||||
requestEnvelope.setData(Collections.singletonList(request));
|
||||
|
||||
try {
|
||||
// 내부 서비스 호출
|
||||
ResponseEntity<Envelope<LedgerResponse>> responseEntity =
|
||||
carLedgerFrmbkService.ledger(requestEnvelope);
|
||||
|
||||
if (responseEntity.getBody() != null &&
|
||||
responseEntity.getBody().getData() != null &&
|
||||
!responseEntity.getBody().getData().isEmpty()) {
|
||||
|
||||
return responseEntity.getBody().getData().get(0);
|
||||
}
|
||||
|
||||
log.warn("[Internal Mode] 자동차 등록원부 조회 응답이 비어있음 - 차량번호: {}", request.getVhrno());
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Internal Mode] 자동차 등록원부 조회 실패 - 차량번호: {}", request.getVhrno(), e);
|
||||
throw new RuntimeException("자동차 등록원부 조회 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공한 응답 개수 계산
|
||||
*/
|
||||
private int countSuccessful(List<VehicleApiResponseVO> responses) {
|
||||
int count = 0;
|
||||
for (VehicleApiResponseVO response : responses) {
|
||||
if (response.isSuccess()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
package go.kr.project.api.internal.service.impl;
|
||||
|
||||
import go.kr.project.api.config.ApiConstant;
|
||||
import go.kr.project.api.internal.client.GovernmentApi;
|
||||
import go.kr.project.api.internal.service.VmisCarBassMatterInqireService;
|
||||
import go.kr.project.api.internal.service.VmisRequestEnricher;
|
||||
import go.kr.project.api.internal.util.ExceptionDetailUtil;
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
|
||||
import go.kr.project.api.service.VmisCarBassMatterInqireLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 자동차 기본 사항 조회 서비스 구현체
|
||||
*
|
||||
* <p>API 호출 정보를 관리하는 서비스 클래스입니다.</p>
|
||||
* <ul>
|
||||
* <li>최초 요청: createInitialRequest() - 시퀀스로 ID 생성 후 INSERT</li>
|
||||
* <li>결과 업데이트: updateResponse() - 응답 데이터 UPDATE</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VmisCarBassMatterInqireServiceImpl extends EgovAbstractServiceImpl implements VmisCarBassMatterInqireService {
|
||||
|
||||
private final GovernmentApi governmentApi;
|
||||
private final VmisRequestEnricher enricher;
|
||||
private final VmisCarBassMatterInqireLogService logService;
|
||||
|
||||
/**
|
||||
* 자동차 기본사항 조회: 보강 -> 최초요청로그 -> 외부호출 -> 응답로그.
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<Envelope<BasicResponse>> basic(Envelope<BasicRequest> envelope) {
|
||||
// 1) 요청 보강
|
||||
enricher.enrichBasic(envelope);
|
||||
|
||||
String generatedId = null;
|
||||
try {
|
||||
// 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
|
||||
if (envelope.getData() != null && !envelope.getData().isEmpty()) {
|
||||
BasicRequest req = envelope.getData().get(0);
|
||||
VmisCarBassMatterInqireVO logEntity = VmisCarBassMatterInqireVO.fromRequest(req);
|
||||
generatedId = logService.createInitialRequestNewTx(logEntity);
|
||||
}
|
||||
|
||||
// 3) 외부 API 호출
|
||||
ResponseEntity<Envelope<BasicResponse>> response = governmentApi.callBasic(envelope);
|
||||
|
||||
// 4) 응답 로그 업데이트
|
||||
// 원본 소스, 정상적인 호출, 리턴(에러 리턴포함) 일 경우에만 에러 로그 남김
|
||||
if (generatedId != null && response.getBody() != null) {
|
||||
VmisCarBassMatterInqireVO update = VmisCarBassMatterInqireVO.fromResponse(generatedId, response.getBody());
|
||||
if (update != null) {
|
||||
logService.updateResponseNewTx(update);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
// 5) 오류 로그 업데이트
|
||||
if (generatedId != null) {
|
||||
try {
|
||||
String detail = ExceptionDetailUtil.buildForLog(e);
|
||||
VmisCarBassMatterInqireVO errorLog = VmisCarBassMatterInqireVO.builder()
|
||||
.carBassMatterInqireId(generatedId) // 자동차기본사항조회 ID (PK)
|
||||
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR) // 연계결과코드 (에러)
|
||||
.cntcResultDtls(detail) // 연계결과상세 (에러 메시지)
|
||||
.build();
|
||||
logService.updateResponseNewTx(errorLog);
|
||||
log.error("[BASIC-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
|
||||
} catch (Exception ignore) {
|
||||
log.error("[BASIC-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package go.kr.project.api.internal.service.impl;
|
||||
|
||||
import go.kr.project.api.config.ApiConstant;
|
||||
import go.kr.project.api.internal.client.GovernmentApi;
|
||||
import go.kr.project.api.internal.service.VmisCarLedgerFrmbkService;
|
||||
import go.kr.project.api.internal.service.VmisRequestEnricher;
|
||||
import go.kr.project.api.internal.util.ExceptionDetailUtil;
|
||||
import go.kr.project.api.model.Envelope;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
|
||||
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
|
||||
import go.kr.project.api.service.VmisCarLedgerFrmbkLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 자동차 등록 원부(갑) 서비스 구현체 (오케스트레이션)
|
||||
* - 요청 보강, 외부 API 호출, 로그 서비스 위임
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VmisCarLedgerFrmbkServiceImpl extends EgovAbstractServiceImpl implements VmisCarLedgerFrmbkService {
|
||||
|
||||
private final GovernmentApi governmentApi;
|
||||
private final VmisRequestEnricher enricher;
|
||||
private final VmisCarLedgerFrmbkLogService logService;
|
||||
|
||||
/**
|
||||
* 자동차 등록원부(갑) 조회: 보강 -> 최초요청로그(별도TX) -> 외부호출 -> 응답로그(마스터/상세, 별도TX) -> 오류 시 에러로그(별도TX).
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<Envelope<LedgerResponse>> ledger(Envelope<LedgerRequest> envelope) {
|
||||
// 1) 요청 보강
|
||||
enricher.enrichLedger(envelope);
|
||||
|
||||
String generatedId = null;
|
||||
try {
|
||||
// 2) 최초 요청 로그 저장 (첫 번째 데이터 기준)
|
||||
if (envelope.getData() != null && !envelope.getData().isEmpty()) {
|
||||
LedgerRequest req = envelope.getData().get(0);
|
||||
VmisCarLedgerFrmbkVO init = VmisCarLedgerFrmbkVO.fromRequest(req);
|
||||
generatedId = logService.createInitialRequestNewTx(init);
|
||||
}
|
||||
|
||||
// 3) 외부 API 호출
|
||||
ResponseEntity<Envelope<LedgerResponse>> response = governmentApi.callLedger(envelope);
|
||||
|
||||
// 4) 응답 로그 업데이트 (마스터 + 상세)
|
||||
if (generatedId != null && response.getBody() != null &&
|
||||
response.getBody().getData() != null && !response.getBody().getData().isEmpty()) {
|
||||
LedgerResponse body = response.getBody().getData().get(0);
|
||||
VmisCarLedgerFrmbkVO masterUpdate = VmisCarLedgerFrmbkVO.fromResponseMaster(generatedId, body);
|
||||
logService.updateResponseNewTx(masterUpdate);
|
||||
|
||||
List<VmisCarLedgerFrmbkDtlVO> details = VmisCarLedgerFrmbkDtlVO.listFromResponse(body, generatedId);
|
||||
if (details != null && !details.isEmpty()) {
|
||||
logService.saveDetailsNewTx(generatedId, details);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
// 5) 오류 로그 업데이트
|
||||
if (generatedId != null) {
|
||||
try {
|
||||
String detail = ExceptionDetailUtil.buildForLog(e);
|
||||
VmisCarLedgerFrmbkVO errorLog = VmisCarLedgerFrmbkVO.builder()
|
||||
.carLedgerFrmbkId(generatedId)
|
||||
.cntcResultCode(ApiConstant.CNTC_RESULT_CODE_ERROR)
|
||||
.cntcResultDtls(detail)
|
||||
.build();
|
||||
logService.updateResponseNewTx(errorLog);
|
||||
log.error("[LEDGER-ERR-LOG] API 호출 에러 정보 저장 완료(별도TX) - ID: {}, detail: {}", generatedId, detail, e);
|
||||
} catch (Exception ignore) {
|
||||
log.error("[LEDGER-ERR-LOG] 에러 로그 저장 실패 - ID: {}", generatedId, ignore);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package go.kr.project.api.internal.util;
|
||||
|
||||
import go.kr.project.api.config.properties.VmisProperties;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Wrapper utility around legacy {@link NewGpkiUtil} using configuration from YAML.
|
||||
*
|
||||
* Notes:
|
||||
* - Place this class under src/main/java/util as requested.
|
||||
* - Uses Lombok for getters/setters.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class GpkiCryptoUtil {
|
||||
|
||||
private String gpkiLicPath;
|
||||
private Boolean ldap; // null -> legacy default
|
||||
private String certFilePath;
|
||||
private String envCertFilePathName;
|
||||
private String envPrivateKeyFilePathName;
|
||||
private String envPrivateKeyPasswd;
|
||||
private String sigCertFilePathName;
|
||||
private String sigPrivateKeyFilePathName;
|
||||
private String sigPrivateKeyPasswd;
|
||||
private String myServerId; // equals to certServerId (INFO system server cert id)
|
||||
private String targetServerIdList; // comma joined list (can be single id)
|
||||
|
||||
private transient NewGpkiUtil delegate;
|
||||
|
||||
public static GpkiCryptoUtil from(VmisProperties.GpkiProps props) throws Exception {
|
||||
GpkiCryptoUtil util = new GpkiCryptoUtil();
|
||||
util.setGpkiLicPath(props.getGpkiLicPath());
|
||||
util.setLdap(props.getLdap());
|
||||
util.setCertFilePath(props.getCertFilePath());
|
||||
util.setEnvCertFilePathName(props.getEnvCertFilePathName());
|
||||
util.setEnvPrivateKeyFilePathName(props.getEnvPrivateKeyFilePathName());
|
||||
util.setEnvPrivateKeyPasswd(props.getEnvPrivateKeyPasswd());
|
||||
util.setSigCertFilePathName(props.getSigCertFilePathName());
|
||||
util.setSigPrivateKeyFilePathName(props.getSigPrivateKeyFilePathName());
|
||||
util.setSigPrivateKeyPasswd(props.getSigPrivateKeyPasswd());
|
||||
util.setMyServerId(props.getCertServerId());
|
||||
// Accept single targetServerId but allow list if provided by YAML in future
|
||||
util.setTargetServerIdList(props.getTargetServerId());
|
||||
util.initialize();
|
||||
return util;
|
||||
}
|
||||
|
||||
public void initialize() throws Exception {
|
||||
NewGpkiUtil g = new NewGpkiUtil();
|
||||
if (gpkiLicPath != null) g.setGpkiLicPath(gpkiLicPath);
|
||||
if (ldap != null) g.setIsLDAP(ldap);
|
||||
if (certFilePath != null) g.setCertFilePath(certFilePath);
|
||||
if (envCertFilePathName != null) g.setEnvCertFilePathName(envCertFilePathName);
|
||||
if (envPrivateKeyFilePathName != null) g.setEnvPrivateKeyFilePathName(envPrivateKeyFilePathName);
|
||||
if (envPrivateKeyPasswd != null) g.setEnvPrivateKeyPasswd(envPrivateKeyPasswd);
|
||||
if (sigCertFilePathName != null) g.setSigCertFilePathName(sigCertFilePathName);
|
||||
if (sigPrivateKeyFilePathName != null) g.setSigPrivateKeyFilePathName(sigPrivateKeyFilePathName);
|
||||
if (sigPrivateKeyPasswd != null) g.setSigPrivateKeyPasswd(sigPrivateKeyPasswd);
|
||||
if (myServerId != null) g.setMyServerId(myServerId);
|
||||
if (targetServerIdList != null) g.setTargetServerIdList(targetServerIdList);
|
||||
g.init();
|
||||
this.delegate = g;
|
||||
}
|
||||
|
||||
public String encryptToBase64(String plain, String targetServerId, String charset) throws Exception {
|
||||
ensureInit();
|
||||
byte[] enc = delegate.encrypt(plain.getBytes(charset), targetServerId, true);
|
||||
return delegate.encode(enc);
|
||||
}
|
||||
|
||||
public String decryptFromBase64(String base64, String charset) throws Exception {
|
||||
ensureInit();
|
||||
byte[] bin = delegate.decode(base64);
|
||||
byte[] dec = delegate.decrypt(bin);
|
||||
return new String(dec, charset);
|
||||
}
|
||||
|
||||
public String signToBase64(String plain, String charset) throws Exception {
|
||||
ensureInit();
|
||||
byte[] sig = delegate.sign(plain.getBytes(charset));
|
||||
return delegate.encode(sig);
|
||||
}
|
||||
|
||||
public String verifyAndExtractBase64(String signedBase64, String charset) throws Exception {
|
||||
ensureInit();
|
||||
byte[] signed = delegate.decode(signedBase64);
|
||||
byte[] data = delegate.validate(signed);
|
||||
return new String(data, charset);
|
||||
}
|
||||
|
||||
private void ensureInit() {
|
||||
if (delegate == null) {
|
||||
throw new IllegalStateException("GpkiCryptoUtil is not initialized. Call initialize() or from(props).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,382 +0,0 @@
|
||||
package go.kr.project.api.internal.util;
|
||||
|
||||
import com.gpki.gpkiapi.GpkiApi;
|
||||
import com.gpki.gpkiapi.cert.X509Certificate;
|
||||
import com.gpki.gpkiapi.crypto.PrivateKey;
|
||||
import com.gpki.gpkiapi.storage.Disk;
|
||||
import com.gpki.gpkiapi_jni;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class NewGpkiUtil {
|
||||
byte[] myEnvCert, myEnvKey, mySigCert, mySigKey;
|
||||
private Map<String, X509Certificate> targetServerCertMap = new HashMap<String, X509Certificate>();
|
||||
|
||||
// properties
|
||||
private String myServerId;
|
||||
private String targetServerIdList;
|
||||
private String envCertFilePathName;
|
||||
private String envPrivateKeyFilePathName;
|
||||
private String envPrivateKeyPasswd;
|
||||
private String sigCertFilePathName;
|
||||
private String sigPrivateKeyFilePathName;
|
||||
private String sigPrivateKeyPasswd;
|
||||
private String certFilePath;
|
||||
private String gpkiLicPath = ".";
|
||||
private boolean isLDAP;
|
||||
private boolean testGPKI = false;
|
||||
|
||||
|
||||
public void init() throws Exception {
|
||||
GpkiApi.init(gpkiLicPath);
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
if(log.isDebugEnabled()){
|
||||
if(gpki.API_GetInfo()==0)
|
||||
log.debug(gpki.sReturnString);
|
||||
else
|
||||
log.error(gpki.sDetailErrorString);
|
||||
}
|
||||
if(targetServerIdList!=null){
|
||||
String certIdList[] = targetServerIdList.split(",");
|
||||
for(int i = 0 ; i < certIdList.length ; i++){
|
||||
String certId = certIdList[i].trim();
|
||||
if(!certId.equals("")){
|
||||
load(gpki, certId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Loading gpki certificate : myServerId="
|
||||
+ this.getMyServerId());
|
||||
|
||||
X509Certificate _myEnvCert = Disk.readCert(this
|
||||
.getEnvCertFilePathName());
|
||||
myEnvCert = _myEnvCert.getCert();
|
||||
|
||||
PrivateKey _myEnvKey = Disk.readPriKey(this
|
||||
.getEnvPrivateKeyFilePathName(), this.getEnvPrivateKeyPasswd());
|
||||
myEnvKey = _myEnvKey.getKey();
|
||||
|
||||
X509Certificate _mySigCert = Disk.readCert(this
|
||||
.getSigCertFilePathName());
|
||||
mySigCert = _mySigCert.getCert();
|
||||
|
||||
PrivateKey _mySigKey = Disk.readPriKey(this
|
||||
.getSigPrivateKeyFilePathName(), this.getSigPrivateKeyPasswd());
|
||||
mySigKey = _mySigKey.getKey();
|
||||
|
||||
//test my cert GPKI
|
||||
if(testGPKI){
|
||||
load(gpki, this.getMyServerId());
|
||||
testGpki(gpki);
|
||||
}
|
||||
this.finish(gpki);
|
||||
log.info("GpkiUtil initialized");
|
||||
}
|
||||
|
||||
private void load(gpkiapi_jni gpki, String certId) throws Exception {
|
||||
|
||||
log.debug("Loading gpki certificate : targetServerId="+ certId);
|
||||
|
||||
X509Certificate cert = targetServerCertMap.get(certId);
|
||||
if (cert != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLDAP) {
|
||||
// String ldapUrl = "ldap://10.1.7.140:389/cn=";
|
||||
// String ldapUrl = "ldap://ldap.gcc.go.kr:389/cn=";
|
||||
String ldapUrl = "ldap://10.1.7.118:389/cn="; // 행정망인 경우
|
||||
// String ldapUrl = "ldap://152.99.57.127:389/cn="; // 인터넷망인 경우
|
||||
String ldapUri;
|
||||
if (certId.charAt(3) > '9') {
|
||||
ldapUri = ",ou=Group of Server,o=Public of Korea,c=KR";
|
||||
} else {
|
||||
ldapUri = ",ou=Group of Server,o=Government of Korea,c=KR";
|
||||
}
|
||||
|
||||
int ret = gpki.LDAP_GetAnyDataByURL("userCertificate;binary", ldapUrl + certId + ldapUri);
|
||||
this.checkResult(ret, gpki);
|
||||
cert = new X509Certificate(gpki.baReturnArray);
|
||||
} else {
|
||||
if(certFilePath != null){
|
||||
cert = Disk.readCert(certFilePath + File.separator + certId + ".cer");
|
||||
}else{
|
||||
log.debug("not certFilePath");
|
||||
}
|
||||
}
|
||||
|
||||
targetServerCertMap.put(certId, cert);
|
||||
}
|
||||
|
||||
private gpkiapi_jni getGPKI(){
|
||||
gpkiapi_jni gpki = new gpkiapi_jni();
|
||||
if(gpki.API_Init(gpkiLicPath) != 0){
|
||||
log.error(gpki.sDetailErrorString);
|
||||
}
|
||||
return gpki;
|
||||
}
|
||||
private void finish(gpkiapi_jni gpki){
|
||||
if(gpki.API_Finish() != 0){
|
||||
log.error(gpki.sDetailErrorString);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] plain, String certId , boolean load) throws Exception {
|
||||
X509Certificate targetEnvCert = targetServerCertMap.get(certId);
|
||||
if (targetEnvCert == null) {
|
||||
throw new Exception("Certificate not found : targetServerId=" + certId);
|
||||
}
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.CMS_MakeEnvelopedData(targetEnvCert.getCert(), plain,
|
||||
gpkiapi_jni.SYM_ALG_NEAT_CBC);
|
||||
checkResult(result, "Fail to encrypt message", gpki);
|
||||
|
||||
return gpki.baReturnArray;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] plain, String certId) throws Exception {
|
||||
return encrypt(plain,certId , false);
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] encrypted) throws Exception {
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.CMS_ProcessEnvelopedData(myEnvCert, myEnvKey,
|
||||
encrypted);
|
||||
checkResult(result, "Fail to decrpyt message", gpki);
|
||||
|
||||
return gpki.baReturnArray;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] sign(byte[] plain) throws Exception {
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.CMS_MakeSignedData(mySigCert, mySigKey, plain, null);
|
||||
checkResult(result, "Fail to sign message", gpki);
|
||||
|
||||
return gpki.baReturnArray;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] validate(byte[] signed) throws Exception {
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.CMS_ProcessSignedData(signed);
|
||||
checkResult(result, "Fail to validate signed message", gpki);
|
||||
return gpki.baData;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
}
|
||||
|
||||
public String encode(byte[] plain) throws Exception {
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.BASE64_Encode(plain);
|
||||
checkResult(result, "Fail to encode message", gpki);
|
||||
|
||||
return gpki.sReturnString;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public byte[] decode(String base64) throws Exception {
|
||||
|
||||
gpkiapi_jni gpki = this.getGPKI();
|
||||
try{
|
||||
int result = gpki.BASE64_Decode(base64);
|
||||
checkResult(result, "Fail to decode base64 message", gpki);
|
||||
|
||||
return gpki.baReturnArray;
|
||||
}catch(Exception ex){
|
||||
throw ex;
|
||||
}finally{
|
||||
finish(gpki);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkResult(int result, gpkiapi_jni gpki)throws Exception{
|
||||
this.checkResult(result, null, gpki);
|
||||
}
|
||||
|
||||
private void checkResult(int result ,String message, gpkiapi_jni gpki)throws Exception{
|
||||
if( 0 != result){
|
||||
if(null != gpki){
|
||||
throw new Exception(message + " : gpkiErrorMessage=" + gpki.sDetailErrorString);
|
||||
}else{
|
||||
throw new Exception(message + " : gpkiErrorCode=" + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testGpki(gpkiapi_jni gpki) throws Exception{
|
||||
//gpki test eng
|
||||
log.info("=======================================================");
|
||||
log.info("================ TEST GPKI START ======================");
|
||||
log.info("=======================================================");
|
||||
String original_Eng = "abc";
|
||||
log.info("=== TEST ENG STRING: "+ original_Eng);
|
||||
try {
|
||||
byte[] encrypted = encrypt(original_Eng.getBytes(), myServerId);
|
||||
log.info("=== TEST ENG ENCRYPT STRING: "+ encode(encrypted));
|
||||
String decrypted = new String(decrypt(encrypted));
|
||||
log.info("=== TEST ENG DECRYPT STRING: "+decrypted);
|
||||
|
||||
if (!original_Eng.equals(decrypted)) {
|
||||
throw new Exception("GpkiUtil not initialized properly(english)");
|
||||
}
|
||||
log.info("=== TEST ENG: OK");
|
||||
} catch (Exception e) {
|
||||
log.warn("Gpki Test error(english)", e);
|
||||
throw e;
|
||||
}
|
||||
//gpki test kor
|
||||
String original = "한글테스트";
|
||||
log.info("=== TEST KOR STRING: "+ original);
|
||||
try {
|
||||
byte[] encrypted = encrypt(original.getBytes(), myServerId);
|
||||
log.info("=== TEST KOR ENCRYPT STRING: "+ encode(encrypted));
|
||||
String decrypted = new String(decrypt(encrypted));
|
||||
log.info("=== TEST KOR DECRYPT STRING: "+decrypted);
|
||||
if (!original.equals(decrypted)) {
|
||||
throw new Exception("GpkiUtil not initialized properly(korean)");
|
||||
}
|
||||
log.info("=== TEST KOR: OK");
|
||||
} catch (Exception e) {
|
||||
log.warn("Gpki Test error(korean)", e);
|
||||
throw e;
|
||||
}finally{
|
||||
log.info("=======================================================");
|
||||
log.info("================ TEST GPKI END ========================");
|
||||
log.info("=======================================================");
|
||||
}
|
||||
}
|
||||
|
||||
public String getMyServerId() {
|
||||
return myServerId;
|
||||
}
|
||||
|
||||
public void setMyServerId(String myServerId) {
|
||||
this.myServerId = myServerId.trim();
|
||||
}
|
||||
|
||||
public String getEnvCertFilePathName() {
|
||||
return envCertFilePathName;
|
||||
}
|
||||
|
||||
public void setEnvCertFilePathName(String envCertFilePathName) {
|
||||
this.envCertFilePathName = envCertFilePathName.trim();
|
||||
}
|
||||
|
||||
public String getEnvPrivateKeyFilePathName() {
|
||||
return envPrivateKeyFilePathName;
|
||||
}
|
||||
|
||||
public void setEnvPrivateKeyFilePathName(String envPrivateKeyFilePathName) {
|
||||
this.envPrivateKeyFilePathName = envPrivateKeyFilePathName.trim();
|
||||
}
|
||||
|
||||
public String getEnvPrivateKeyPasswd() {
|
||||
return envPrivateKeyPasswd;
|
||||
}
|
||||
|
||||
public void setEnvPrivateKeyPasswd(String envPrivateKeyPasswd) {
|
||||
this.envPrivateKeyPasswd = envPrivateKeyPasswd.trim();
|
||||
}
|
||||
|
||||
public String getSigPrivateKeyPasswd() {
|
||||
return sigPrivateKeyPasswd;
|
||||
}
|
||||
|
||||
public void setSigPrivateKeyPasswd(String sigPrivateKeyPasswd) {
|
||||
this.sigPrivateKeyPasswd = sigPrivateKeyPasswd.trim();
|
||||
}
|
||||
|
||||
public String getSigCertFilePathName() {
|
||||
return sigCertFilePathName;
|
||||
}
|
||||
|
||||
public void setSigCertFilePathName(String sigCertFilePathName) {
|
||||
this.sigCertFilePathName = sigCertFilePathName.trim();
|
||||
}
|
||||
|
||||
public String getSigPrivateKeyFilePathName() {
|
||||
return sigPrivateKeyFilePathName;
|
||||
}
|
||||
|
||||
public void setSigPrivateKeyFilePathName(String sigPrivateKeyFilePathName) {
|
||||
this.sigPrivateKeyFilePathName = sigPrivateKeyFilePathName.trim();
|
||||
}
|
||||
|
||||
public boolean getIsLDAP() {
|
||||
return isLDAP;
|
||||
}
|
||||
|
||||
public void setIsLDAP(boolean isLDAP) {
|
||||
this.isLDAP = isLDAP;
|
||||
}
|
||||
|
||||
public String getCertFilePath() {
|
||||
return certFilePath;
|
||||
}
|
||||
|
||||
public void setCertFilePath(String certFilePath) {
|
||||
this.certFilePath = certFilePath.trim();
|
||||
}
|
||||
|
||||
public String getTargetServerIdList() {
|
||||
return targetServerIdList;
|
||||
}
|
||||
|
||||
public void setTargetServerIdList(String targetServerIdList) {
|
||||
this.targetServerIdList = targetServerIdList;
|
||||
}
|
||||
|
||||
public String getGpkiLicPath() {
|
||||
return gpkiLicPath;
|
||||
}
|
||||
|
||||
public void setGpkiLicPath(String gpkiLicPath) {
|
||||
this.gpkiLicPath = gpkiLicPath;
|
||||
}
|
||||
|
||||
public boolean getTestGPKI() {
|
||||
return testGPKI;
|
||||
}
|
||||
|
||||
public void setTestGPKI(boolean testGPKI) {
|
||||
this.testGPKI = testGPKI;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package go.kr.project.api.internal.util;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
public final class TxIdUtil {
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private TxIdUtil() {}
|
||||
|
||||
public static String generate() {
|
||||
String time = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.KOREA).format(new Date());
|
||||
int random = 100000 + RANDOM.nextInt(900000);
|
||||
return time + "_" + random;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package go.kr.project.api.internal.mapper;
|
||||
package go.kr.project.api.mapper;
|
||||
|
||||
import go.kr.project.api.model.response.VmisCarBassMatterInqireVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@ -1,4 +1,4 @@
|
||||
package go.kr.project.api.internal.mapper;
|
||||
package go.kr.project.api.mapper;
|
||||
|
||||
import go.kr.project.api.model.response.VmisCarLedgerFrmbkDtlVO;
|
||||
import go.kr.project.api.model.response.VmisCarLedgerFrmbkVO;
|
||||
@ -1,76 +0,0 @@
|
||||
package go.kr.project.api.service;
|
||||
|
||||
import go.kr.project.api.model.VehicleApiResponseVO;
|
||||
import go.kr.project.api.model.request.BasicRequest;
|
||||
import go.kr.project.api.model.request.LedgerRequest;
|
||||
import go.kr.project.api.model.response.BasicResponse;
|
||||
import go.kr.project.api.model.response.LedgerResponse;
|
||||
|
||||
/**
|
||||
* 차량 정보 조회 서비스 인터페이스
|
||||
*
|
||||
* <p>이 인터페이스는 차량 정보를 조회하는 두 가지 구현체를 추상화합니다:</p>
|
||||
* <ul>
|
||||
* <li>InternalVehicleInfoServiceImpl: 내부 VMIS 모듈을 직접 호출 (vmis.integration.mode=internal)</li>
|
||||
* <li>ExternalVehicleInfoServiceImpl: 외부 REST API를 호출 (vmis.integration.mode=external)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>설정 방법:</h3>
|
||||
* <pre>
|
||||
* # application.yml
|
||||
* vmis:
|
||||
* integration:
|
||||
* mode: internal # 또는 external
|
||||
* </pre>
|
||||
*
|
||||
* <h3>사용 예시:</h3>
|
||||
* <pre>
|
||||
* {@code
|
||||
* @Autowired
|
||||
* private VehicleInfoService vehicleInfoService;
|
||||
*
|
||||
* // 단일 차량 조회
|
||||
* VehicleApiResponseVO response = vehicleInfoService.getVehicleInfo("12가3456");
|
||||
*
|
||||
* // 여러 차량 일괄 조회
|
||||
* List<VehicleApiResponseVO> responses = vehicleInfoService.getVehiclesInfo(
|
||||
* Arrays.asList("12가3456", "34나5678")
|
||||
* );
|
||||
*
|
||||
* // 단독 조회 (기본/등록원부)
|
||||
* BasicResponse basic = vehicleInfoService.getBasicInfo("12가3456");
|
||||
* LedgerResponse ledger = vehicleInfoService.getLedgerInfo("12가3456");
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public interface VehicleInfoService {
|
||||
|
||||
/**
|
||||
* 단일 차량에 대한 정보 조회 (상세 파라미터 포함)
|
||||
*
|
||||
* <p>차량 기본정보와 등록원부 정보를 함께 조회합니다.</p>
|
||||
* <p>차량번호 외에 부과기준일, 조회구분, 차대번호 등 추가 파라미터를 포함하여 조회할 수 있습니다.</p>
|
||||
*
|
||||
* @param basicRequest 기본정보 조회 요청 (차량번호, 부과기준일, 조회구분 등 포함)
|
||||
* @return 차량 정보 응답 (기본정보 + 등록원부 정보)
|
||||
*/
|
||||
VehicleApiResponseVO getVehicleInfo(BasicRequest basicRequest);
|
||||
|
||||
/**
|
||||
* 차량 기본정보만 조회 (단독)
|
||||
* 중요: 차량번호 외에 부과기준일, 조회구분, 차대번호 등 필수 파라미터를 모두 포함한 BasicRequest 필요
|
||||
*
|
||||
* @param request 기본정보 조회 요청 (차량번호, 부과기준일, 조회구분 등 포함)
|
||||
* @return 기본정보 응답
|
||||
*/
|
||||
BasicResponse getBasicInfo(BasicRequest request);
|
||||
|
||||
/**
|
||||
* 자동차 등록원부(갑)만 조회 (단독)
|
||||
* 중요: 차량번호 외에 소유자정보, 조회구분 등 필수 파라미터를 모두 포함한 LedgerRequest 필요
|
||||
*
|
||||
* @param request 등록원부 조회 요청 (차량번호, 소유자정보, 조회구분 등 포함)
|
||||
* @return 등록원부 응답
|
||||
*/
|
||||
LedgerResponse getLedgerInfo(LedgerRequest request);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package go.kr.project.api.internal.util;
|
||||
package go.kr.project.api.util;
|
||||
|
||||
/**
|
||||
* Common helper to extract root-cause message and truncate to DB column limit (default 4000 chars).
|
||||
Loading…
Reference in New Issue