파트너코드 조회

This commit is contained in:
Rinjae
2025-11-26 14:28:12 +09:00
parent f0cffdf9da
commit 818155242f
9 changed files with 518 additions and 38 deletions
+112
View File
@@ -0,0 +1,112 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 프로젝트 개요
`eapim-admin-kjb`는 광주은행(KJB) 전용 커스터마이징을 위한 eapim-admin 확장 모듈입니다. 빠른 부팅 속도를 위해 메인 eapim-admin 프로젝트에서 분리되어 독립적으로 테스트할 수 있습니다.
## 빌드 및 테스트 명령어
```bash
# 빌드
gradle build
gradle clean build
# 테스트 실행 (환경변수 필요)
KJB_EAPIM_TEST_ENABLED=TRUE gradle :eapim-admin-kjb:test
# 특정 테스트 클래스 실행
KJB_EAPIM_TEST_ENABLED=TRUE gradle :eapim-admin-kjb:test --tests "com.eactive.ext.kjb.ums.test.UmsTest.testSms"
KJB_EAPIM_TEST_ENABLED=TRUE gradle :eapim-admin-kjb:test --tests "com.eactive.ext.kjb.ums.test.UmsTest.testEmail"
# SafeDB 실제 모드로 테스트 (Linux 서버)
KJB_EAPIM_TEST_ENABLED=TRUE gradle :eapim-admin-kjb:test -Psafedb
```
**테스트 실행 조건**: `KJB_EAPIM_TEST_ENABLED=TRUE` 환경변수가 설정되어야 UMS/이메일 테스트가 실행됩니다.
## 모듈 의존성
```
eapim-admin-kjb
├── elink-portal-common # JPA 엔티티 (MessageRequest, MessageCode 등)
└── kjb-safedb # 암호화 라이브러리
```
의존 모듈들은 `../elink-portal-common/`, `../kjb-safedb/` 경로에 위치합니다.
## 아키텍처
### 주요 컴포넌트
| 컴포넌트 | 위치 | 설명 |
|---------|------|------|
| `KjbProperty` | `common/` | DB 테이블 `TSEAIRM24`에서 로드되는 설정 관리. `@KjbPropertyValue` 애노테이션으로 키 매핑 |
| `KjbEaiBatchModule` | `eai/` | EAI 배치 시스템 HTTP 연동. 파일 기반 배치 전송 및 보관 기간 관리 |
| `KjbUmsService` | `ums/` | UMS API를 통한 카카오 알림톡 발송 |
| `KjbEmailSendModule` | `ums/` | EAI 배치를 통한 이메일 발송. 파일 생성 후 배치 호출 |
| `KjbObpModule` | `obp/` | OBP(Open Banking Platform) API 연동 |
### 프로퍼티 키 목록 (`KjbProperty.java`)
**EAI 배치 연동**
- `kjb.eai.batch.url` - EAI 배치 서버 URL
- `kjb.eai.batch.connection_timeout` - 연결 타임아웃 (초, 기본값: 5)
- `kjb.eai.batch.response_timeout` - 응답 타임아웃 (초, 기본값: 5)
- `kjb.eai.batch.charset` - 문자셋 (기본값: euc-kr)
**이메일 발송 (EAI 배치 경유)**
- `kjb.ums.eai_batch.send_dir` - 발송 파일 디렉토리
- `kjb.ums.eai_batch.backup_dir` - 백업 디렉토리
- `kjb.ums.eai_batch.error_dir` - 에러 디렉토리
- `kjb.ums.eai_batch.interface_id` - 인터페이스 ID (13자리)
- `kjb.ums.eai_batch.retention_date` - 파일 보관 기간 (일, 기본값: 60)
**UMS (카카오 알림톡)**
- `kjb.ums.host` - UMS API 엔드포인트
- `kjb.ums.api_key` - API 인증 키 (36자리 UUID 형식)
- `kjb.ums.connection_timeout` - 연결 타임아웃 (초, 기본값: 5)
- `kjb.ums.response_timeout` - 응답 타임아웃 (초, 기본값: 5)
**OBP 연동**
- `kjb.obp.url` - OBP API URL
- `kjb.obp.trust_system.key/value` - 신뢰 시스템 헤더
- `kjb.obp.partner_code.key/value` - 파트너 코드 헤더
### 메시지 채널
- `KjbUmsService`: 카카오 알림톡 (`MessageCode.Channels.KAKAO_ALIMTALK`)
- `KjbEmailSendModule`: 이메일 (`MessageCode.Channels.EMAIL`)
## 로컬 개발 환경 (Windows)
테스트 시 `OsUtil.isWindows()` 조건으로 로컬 경로가 자동 설정됩니다:
- 발송 디렉토리: `c:\kjb-logs\sendmail\snd`
- 백업 디렉토리: `c:\kjb-logs\sendmail\bak`
- 에러 디렉토리: `c:\kjb-logs\sendmail\error`
URL이 빈 문자열로 설정되면 실제 API 호출 없이 모의 응답을 반환합니다.
## 기술 스택
- **Java 8**: Gradle toolchain 강제
- **Spring Framework 5.3**: 의존성 주입
- **Apache HttpClient 5.1**: HTTP 통신
- **Gson 2.3.1**: JSON 처리
- **Lombok 1.18.28**: 보일러플레이트 코드 생성
- **JUnit 5**: 테스트 프레임워크
- **Logback 1.2.10**: 로깅
## 패키지 구조
```
com.eactive.ext.kjb
├── common/ # 공통 (KjbProperty, 예외 클래스)
├── eai/ # EAI 배치 연동
├── obp/ # OBP API 연동
│ └── common/ # OBP HTTP 클라이언트
├── ums/ # UMS/이메일 발송
│ └── gson/ # UMS 요청/응답 JSON 모델
└── utils/ # 유틸리티 (JSON, 검증, OS 판별)
```
@@ -0,0 +1,22 @@
package com.eactive.ext.kjb.common;
public class KjbObpException extends Exception {
private int statusCode;
public KjbObpException(String message) {
super(message);
}
public KjbObpException(String message, Throwable cause) {
super(message, cause);
}
public KjbObpException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
}
@@ -91,6 +91,18 @@ public class KjbProperty {
@NotEmpty @NotEmpty
private String obpPartnerCodeValue = "100001-01"; private String obpPartnerCodeValue = "100001-01";
@KjbPropertyValue(key = "kjb.obp.connection_timeout", defaultValue = "5")
@Min(1) @Max(10)
private int obpConnectionTimeout = 5;
@KjbPropertyValue(key = "kjb.obp.response_timeout", defaultValue = "5")
@Min(1) @Max(30)
private int obpResponseTimeout = 5;
@KjbPropertyValue(key = "kjb.obp.timeout", defaultValue = "10")
@Min(1) @Max(30)
private int obpTimeout = 10;
@KjbPropertyValue(key = "kjb.obp.url.commission", defaultValue = "/api/billing/billing/findBillingForCondition") @KjbPropertyValue(key = "kjb.obp.url.commission", defaultValue = "/api/billing/billing/findBillingForCondition")
@NotEmpty @NotEmpty
@@ -1,36 +1,84 @@
package com.eactive.ext.kjb.obp; package com.eactive.ext.kjb.obp;
import com.google.gson.JsonObject; import com.eactive.ext.kjb.common.KjbObpException;
import com.eactive.ext.kjb.common.KjbProperty;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.stream.Collectors;
@Slf4j @Slf4j
public class KjbObpModule { public class KjbObpModule {
private static final String FAKE_PARTNER_CODE = "999999-99";
private static KjbObpModule _instance;
private final KjbProperty property;
private final ObpClient client;
public static synchronized KjbObpModule getInstance(KjbProperty kjbProperty) {
if (_instance == null) {
_instance = new KjbObpModule(kjbProperty);
}
return _instance;
}
private KjbObpModule(KjbProperty kjbProperty) {
if (kjbProperty == null) {
throw new IllegalArgumentException("KjbProperty cannot be null");
}
this.property = kjbProperty;
this.client = ObpClient.getInstance(kjbProperty);
}
/** /**
* Trim, 숫자이외 자동 치환 * 사업자등록번호로 partnerCode 조회
* @param bizNo * @param bizNo 사업자등록번호 (숫자 외 문자 자동 제거)
* @return * @return partnerCode
* @throws KjbObpException 조회 실패 시
*/ */
public static JsonObject queryOrganization(String bizNo) { public String queryPartnerCode(String bizNo) throws KjbObpException {
bizNo = ObpUtil.onlyDigit(bizNo); bizNo = ObpUtil.onlyDigit(bizNo);
if (StringUtils.isEmpty(bizNo)) { if (StringUtils.isEmpty(bizNo)) {
throw new IllegalArgumentException("BizNo is not empty."); throw new IllegalArgumentException("BizNo is empty.");
} }
if (bizNo.length() != 10) { if (bizNo.length() != 10) {
throw new IllegalArgumentException("BizNo is not valid."); throw new IllegalArgumentException("BizNo must be 10 digits. (input: " + bizNo + ")");
} }
return null; // 가상모드: URL 미설정 시 가상 파트너코드 반환
if (!client.isRealMode()) {
log.info("[OBP] 가상모드 - 가상 partnerCode 반환: {}", FAKE_PARTNER_CODE);
return FAKE_PARTNER_CODE;
} }
String path = String.format(property.getObpUrlOrganizationFmt(), bizNo);
log.debug("[OBP] queryPartnerCode - bizNo: {}, path: {}", bizNo, path);
public static void main(String[] args) { ObpResponse response = client.get(path);
log.info("queryOrganization Test");
queryOrganization("123asd123asd123"); if (response.getStatus() != 200) {
log.warn("[OBP] queryPartnerCode failed - status: {}, body: {}", response.getStatus(), response.getBody());
throw new KjbObpException(response.getStatus(), "OBP 조회 실패: " + response.getBody());
}
ObpOrganization org = ObpOrganization.fromResponse(response);
if (org == null) {
throw new KjbObpException("OBP 응답 파싱 실패");
}
if (!org.hasPartnerCode()) {
throw new KjbObpException("partnerCode가 없습니다. bizNo: " + bizNo);
}
return org.getPartnerCode();
}
/**
* ObpClient 반환 (직접 API 호출용)
*/
public ObpClient getClient() {
return client;
} }
} }
@@ -0,0 +1,183 @@
package com.eactive.ext.kjb.obp;
import com.eactive.ext.kjb.common.KjbProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.*;
@Slf4j
public class ObpClient {
private static ObpClient _instance;
private final KjbProperty property;
private final CloseableHttpClient httpClient;
private final RequestConfig requestConfig;
private final boolean isRealMode;
public synchronized static ObpClient getInstance(KjbProperty kjbProperty) {
if (_instance == null) {
_instance = new ObpClient(kjbProperty);
}
return _instance;
}
private ObpClient(KjbProperty kjbProperty) {
if (kjbProperty == null) {
throw new IllegalArgumentException("KjbProperty cannot be null");
}
this.property = kjbProperty;
this.isRealMode = StringUtils.isNotEmpty(kjbProperty.getObpUrl());
this.requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(kjbProperty.getObpConnectionTimeout()))
.setResponseTimeout(Timeout.ofSeconds(kjbProperty.getObpResponseTimeout()))
.build();
if (isRealMode) {
this.httpClient = HttpClients.createDefault();
} else {
this.httpClient = null;
log.warn("[OBP] URL이 설정되지 않음. 실제 OBP 연동 없이 동작함");
}
}
/**
* GET 요청
* @param path API 경로 (예: /api/customer/...)
* @return ObpResponse
*/
public ObpResponse get(String path) {
return get(path, null);
}
/**
* GET 요청 (추가 헤더 포함)
* @param path API 경로
* @param additionalHeaders 추가 헤더
* @return ObpResponse
*/
public ObpResponse get(String path, Map<String, String> additionalHeaders) {
String url = property.getObpUrl() + path;
HttpGet httpGet = new HttpGet(url);
applyDefaultHeaders(httpGet);
applyAdditionalHeaders(httpGet, additionalHeaders);
httpGet.setConfig(requestConfig);
return execute(httpGet);
}
/**
* POST 요청 (JSON body)
* @param path API 경로
* @param jsonBody JSON 문자열
* @return ObpResponse
*/
public ObpResponse post(String path, String jsonBody) {
return post(path, jsonBody, null);
}
/**
* POST 요청 (JSON body, 추가 헤더 포함)
* @param path API 경로
* @param jsonBody JSON 문자열
* @param additionalHeaders 추가 헤더
* @return ObpResponse
*/
public ObpResponse post(String path, String jsonBody, Map<String, String> additionalHeaders) {
String url = property.getObpUrl() + path;
HttpPost httpPost = new HttpPost(url);
applyDefaultHeaders(httpPost);
applyAdditionalHeaders(httpPost, additionalHeaders);
httpPost.setConfig(requestConfig);
if (StringUtils.isNotEmpty(jsonBody)) {
httpPost.setEntity(EntityBuilder.create()
.setText(jsonBody)
.setContentType(ContentType.APPLICATION_JSON)
.build());
}
return execute(httpPost);
}
private void applyDefaultHeaders(HttpUriRequestBase request) {
request.setHeader("Content-Type", "application/json; charset=utf-8");
request.setHeader(property.getObpTrustSystemKey(), property.getObpTrustSystemValue());
request.setHeader(property.getObpPartnerCodeKey(), property.getObpPartnerCodeValue());
}
private void applyAdditionalHeaders(HttpUriRequestBase request, Map<String, String> additionalHeaders) {
if (additionalHeaders != null) {
additionalHeaders.forEach(request::setHeader);
}
}
private ObpResponse execute(HttpUriRequestBase request) {
if (!isRealMode) {
log.warn("[OBP] URL 미설정으로 실제 호출하지 않음. 요청: {} {}", request.getMethod(), request.getRequestUri());
ObpResponse mockResponse = new ObpResponse();
mockResponse.setStatus(200);
mockResponse.setBody("{}");
return mockResponse;
}
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
log.debug("[OBP] 요청: {} {}", request.getMethod(), request.getRequestUri());
Future<ObpResponse> future = executor.submit(() ->
httpClient.execute(request, response -> {
ObpResponse obpResponse = new ObpResponse();
obpResponse.setStatus(response.getCode());
obpResponse.setBody(EntityUtils.toString(response.getEntity(), "UTF-8"));
Arrays.asList(response.getHeaders()).forEach(header ->
obpResponse.getHeaders().put(header.getName(), header.getValue()));
return obpResponse;
})
);
ObpResponse response = future.get(property.getObpTimeout(), TimeUnit.SECONDS);
if (log.isDebugEnabled()) {
log.debug("[OBP] 응답: status={}, body={}", response.getStatus(), response.getBody());
}
return response;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
log.error("[OBP] 호출 실패: {} {}", request.getMethod(), request.getRequestUri(), e);
ObpResponse errorResponse = new ObpResponse();
errorResponse.setStatus(-1);
errorResponse.setBody(e.getMessage());
return errorResponse;
} finally {
executor.shutdown();
}
}
/**
* 실제 모드 여부
*/
public boolean isRealMode() {
return isRealMode;
}
}
@@ -0,0 +1,121 @@
package com.eactive.ext.kjb.obp;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
/**
* OBP Organization 조회 응답
* - 필수 필드만 명시적으로 선언하고, 나머지는 rawJson으로 보관하여 유연성 확보
*/
@Data
public class ObpOrganization {
private static final Gson gson = new Gson();
// 핵심 필드
@SerializedName("partnerCode")
private String partnerCode;
@SerializedName("id")
private String id;
@SerializedName("name")
private String name;
@SerializedName("state")
private String state;
@SerializedName("status")
private String status;
@SerializedName("businessManRegistrationNo")
private String businessManRegistrationNo;
@SerializedName("corporateRegistrationNo")
private String corporateRegistrationNo;
@SerializedName("representativeName")
private String representativeName;
// 원본 JSON (추가 필드 접근용
private transient JsonObject rawJson;
/**
* JSON 문자열에서 파싱
*/
public static ObpOrganization fromJson(String json) {
if (json == null || json.trim().isEmpty()) {
return null;
}
ObpOrganization org = gson.fromJson(json, ObpOrganization.class);
org.rawJson = new JsonParser().parse(json).getAsJsonObject();
return org;
}
/**
* ObpResponse에서 파싱
*/
public static ObpOrganization fromResponse(ObpResponse response) {
if (response == null || response.getStatus() != 200) {
return null;
}
return fromJson(response.getBody());
}
/**
* 원본 JSON에서 추가 필드 조회
* @param fieldName 필드명 (예: "englishName", "description")
* @return 필드 값 (없으면 null)
*/
public String getField(String fieldName) {
if (rawJson == null || !rawJson.has(fieldName) || rawJson.get(fieldName).isJsonNull()) {
return null;
}
return rawJson.get(fieldName).getAsString();
}
/**
* 원본 JSON에서 중첩 필드 조회
* @param path 경로 (예: "type.name", "type.parent.name")
* @return 필드 값 (없으면 null)
*/
public String getNestedField(String path) {
if (rawJson == null || path == null) {
return null;
}
String[] parts = path.split("\\.");
JsonObject current = rawJson;
for (int i = 0; i < parts.length - 1; i++) {
if (!current.has(parts[i]) || current.get(parts[i]).isJsonNull()) {
return null;
}
current = current.getAsJsonObject(parts[i]);
}
String lastField = parts[parts.length - 1];
if (!current.has(lastField) || current.get(lastField).isJsonNull()) {
return null;
}
return current.get(lastField).getAsString();
}
/**
* 원본 JSON 객체 반환
*/
public JsonObject getRawJson() {
return rawJson;
}
/**
* partnerCode 유효성 확인
*/
public boolean hasPartnerCode() {
return partnerCode != null && !partnerCode.trim().isEmpty();
}
}
@@ -0,0 +1,4 @@
package com.eactive.ext.kjb.obp;
public class ObpRequest {
}
@@ -1,4 +1,4 @@
package com.eactive.ext.kjb.obp.common; package com.eactive.ext.kjb.obp;
import lombok.Data; import lombok.Data;
@@ -1,22 +0,0 @@
package com.eactive.ext.kjb.obp.common;
import com.eactive.ext.kjb.common.KjbProperty;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ObpClient {
private static ObpClient _instance;
public synchronized static ObpClient getInstance(KjbProperty kjbProperty) {
if (_instance == null) {
_instance = new ObpClient(kjbProperty);
}
return _instance;
}
private ObpClient(KjbProperty kjbProperty) {
}
}