diff --git a/.gitignore b/.gitignore
index 25c7ff0..8cf334c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ build/
.settings
.classpath
.project
+out
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6b191af
--- /dev/null
+++ b/CLAUDE.md
@@ -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 판별)
+```
diff --git a/bin/.gitignore b/bin/.gitignore
index 7eed456..7ac70cd 100644
--- a/bin/.gitignore
+++ b/bin/.gitignore
@@ -1,2 +1,3 @@
/main/
/test/
+/default/
diff --git a/build.gradle b/build.gradle
index d9e6691..e5b9cb4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,16 +9,25 @@ group = 'com.eactive.ext.kjb'
version = '1.0-SNAPSHOT'
def springVersion = "5.3.27"
+def nexusUrl = "https://nexus.eactive.synology.me:8090"
+repositories {
+ maven {
+ url "${nexusUrl}/repository/maven-public/"
+ allowInsecureProtocol = true
+ }
+}
dependencies {
annotationProcessor "org.projectlombok:lombok:1.18.28"
api project(':elink-portal-common')
- api project(":kjb-safedb")
+ //api project(":kjb-safedb")
api 'com.eactive.elink.common:elink-common-data:4.5.5'
+ implementation fileTree(dir: "ext-libs", include: ["*.jar"])
+
// api "org.springframework:spring-context:${springVersion}"
// api "org.springframework:spring-webmvc:${springVersion}"
// api "org.springframework.security.oauth:spring-security-oauth2:2.3.8.RELEASE"
@@ -27,12 +36,16 @@ dependencies {
implementation "org.apache.httpcomponents.client5:httpclient5:5.1"
+ compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
+
compileOnly 'org.slf4j:jul-to-slf4j:1.7.35'
compileOnly 'org.slf4j:jcl-over-slf4j:1.7.35'
compileOnly 'org.slf4j:log4j-over-slf4j:1.7.35'
compileOnly 'org.slf4j:slf4j-api:1.7.35'
- compileOnly fileTree(dir: "ext-libs/compileOnly", include: ["*.jar"])
+ compileOnly "org.jdom:jdom:1.1"
+
+ compileOnly fileTree(dir: "ext-libs/compileOnly", include: ["*.jar"])
testAnnotationProcessor "org.projectlombok:lombok:1.18.28"
diff --git a/ext-libs/INICrypto_v4.2.10.jar b/ext-libs/INICrypto_v4.2.10.jar
new file mode 100644
index 0000000..d4b108d
Binary files /dev/null and b/ext-libs/INICrypto_v4.2.10.jar differ
diff --git a/ext-libs/external_v4.2.1.00_kjbank18.jar b/ext-libs/external_v4.2.1.00_kjbank18.jar
new file mode 100644
index 0000000..eaf35b2
Binary files /dev/null and b/ext-libs/external_v4.2.1.00_kjbank18.jar differ
diff --git a/ext-libs/jdom2-2.0.6.jar b/ext-libs/jdom2-2.0.6.jar
new file mode 100644
index 0000000..2850ca1
Binary files /dev/null and b/ext-libs/jdom2-2.0.6.jar differ
diff --git a/ext-libs/log4j-1.2-api-2.17.2.jar b/ext-libs/log4j-1.2-api-2.17.2.jar
new file mode 100644
index 0000000..1f73132
Binary files /dev/null and b/ext-libs/log4j-1.2-api-2.17.2.jar differ
diff --git a/ext-libs/log4j-api-2.17.2.jar b/ext-libs/log4j-api-2.17.2.jar
new file mode 100644
index 0000000..16d9061
Binary files /dev/null and b/ext-libs/log4j-api-2.17.2.jar differ
diff --git a/ext-libs/log4j-core-2.17.2.jar b/ext-libs/log4j-core-2.17.2.jar
new file mode 100644
index 0000000..0fd0051
Binary files /dev/null and b/ext-libs/log4j-core-2.17.2.jar differ
diff --git a/ext-libs/poolit.jar b/ext-libs/poolit.jar
new file mode 100644
index 0000000..272db37
Binary files /dev/null and b/ext-libs/poolit.jar differ
diff --git a/ext-libs/xercesImpl-2.12.2.jar b/ext-libs/xercesImpl-2.12.2.jar
new file mode 100644
index 0000000..ccbae9f
Binary files /dev/null and b/ext-libs/xercesImpl-2.12.2.jar differ
diff --git a/kjb-docs/obp/obp-query-partner/README.md b/kjb-docs/obp/obp-query-partner/README.md
new file mode 100644
index 0000000..8a6bcd6
--- /dev/null
+++ b/kjb-docs/obp/obp-query-partner/README.md
@@ -0,0 +1 @@
+- API Client 그대로 구현
\ No newline at end of file
diff --git a/kjb-docs/obp/obp-query-partner/organizationFromObp.http b/kjb-docs/obp/obp-query-partner/organizationFromObp.http
new file mode 100644
index 0000000..56ce58a
--- /dev/null
+++ b/kjb-docs/obp/obp-query-partner/organizationFromObp.http
@@ -0,0 +1,94 @@
+GET /api/organizations/organizationFromObp?cacheBuster=1764073311139&licenseNumber=1838700854&organizationId=15 HTTP/1.1
+Accept: application/json, text/plain, */*
+Accept-Encoding: gzip, deflate
+Accept-Language: ko,en;q=0.9,en-US;q=0.8
+Connection: keep-alive
+Cookie: XSRF-TOKEN=0e8e6398-9231-4b2b-859a-a0ace85c9f12; NG_TRANSLATE_LANG_KEY=%22ko%22; JSESSIONID_APIPORTAL=DE09F0098148634E05D434D3D8D9EFE5; loginStatus=success; showNotice=
+Host: 192.168.246.16:8080
+Referer: http://192.168.246.16:8080/
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62
+X-XSRF-TOKEN: 0e8e6398-9231-4b2b-859a-a0ace85c9f12
+
+
+HTTP/1.1 200
+X-Application-Context: portalApplication:swagger,dev:8085
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+Content-Type: application/json;charset=UTF-8
+Content-Length: 2126
+Date: Tue, 25 Nov 2025 12:21:51 GMT
+Keep-Alive: timeout=20
+Connection: keep-alive
+
+
+{
+ "id" : "001-90010001-000001-6000009",
+ "registeredOn" : "2021-07-19T07:25:53.000+0000",
+ "registeredBy" : "001-90010009-000000-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "재팬리미트파이낸스㈜",
+ "state" : "01",
+ "englishName" : null,
+ "description" : "기업회원-재팬리미트파이낸스㈜",
+ "type" : {
+ "id" : "001-90010001-000001-0000001",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "제휴 기업",
+ "alias" : null,
+ "description" : "제휴 기업",
+ "parent" : {
+ "id" : "001-90010001-000000-0000011",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "고객 타입",
+ "alias" : null,
+ "description" : "고객 타입",
+ "parent" : {
+ "id" : "001-90010001-000000-0000010",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "파티",
+ "alias" : null,
+ "description" : "파티 최상위 타입 자체",
+ "parent" : null
+ }
+ }
+ },
+ "partnerId" : null,
+ "status" : "01",
+ "individualCorporateDivision" : "02",
+ "informationOfferLevel" : "10",
+ "signedOnDay" : "20170726",
+ "signedPath" : "31",
+ "statusReason" : null,
+ "addresses" : [ ],
+ "contacts" : [ ],
+ "communityInfo" : null,
+ "individualAgreements" : [ ],
+ "signup" : null,
+ "accountabilities" : null,
+ "businessManRegistrationNo" : "1838700854",
+ "corporateRegistrationNo" : "1101116464905",
+ "closeDivision" : "09",
+ "incorporatedOnDay" : "20170726",
+ "industryClassification" : null,
+ "representativeName" : "호세인사로와",
+ "representativeBirthday" : null,
+ "typeOfBusiness" : null,
+ "businessCondition" : null,
+ "partnerCode" : "000014-01",
+ "corporateEmployee" : [ ],
+ "defaultAddress" : null,
+ "defaultContact" : null
+}
\ No newline at end of file
diff --git a/kjb-docs/obp/obp-query-partner/organizationFromObp.md b/kjb-docs/obp/obp-query-partner/organizationFromObp.md
new file mode 100644
index 0000000..d0bef76
--- /dev/null
+++ b/kjb-docs/obp/obp-query-partner/organizationFromObp.md
@@ -0,0 +1,70 @@
+```json
+{
+ "id" : "001-90010001-000001-6000009",
+ "registeredOn" : "2021-07-19T07:25:53.000+0000",
+ "registeredBy" : "001-90010009-000000-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "재팬리미트파이낸스㈜",
+ "state" : "01",
+ "englishName" : null,
+ "description" : "기업회원-재팬리미트파이낸스㈜",
+ "type" : {
+ "id" : "001-90010001-000001-0000001",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "제휴 기업",
+ "alias" : null,
+ "description" : "제휴 기업",
+ "parent" : {
+ "id" : "001-90010001-000000-0000011",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "고객 타입",
+ "alias" : null,
+ "description" : "고객 타입",
+ "parent" : {
+ "id" : "001-90010001-000000-0000010",
+ "registeredOn" : "2016-09-06T01:51:18.000+0000",
+ "registeredBy" : "001-20160721-163701-0000001",
+ "modifiedOn" : null,
+ "modifiedBy" : null,
+ "name" : "파티",
+ "alias" : null,
+ "description" : "파티 최상위 타입 자체",
+ "parent" : null
+ }
+ }
+ },
+ "partnerId" : null,
+ "status" : "01",
+ "individualCorporateDivision" : "02",
+ "informationOfferLevel" : "10",
+ "signedOnDay" : "20170726",
+ "signedPath" : "31",
+ "statusReason" : null,
+ "addresses" : [ ],
+ "contacts" : [ ],
+ "communityInfo" : null,
+ "individualAgreements" : [ ],
+ "signup" : null,
+ "accountabilities" : null,
+ "businessManRegistrationNo" : "1838700854",
+ "corporateRegistrationNo" : "1101116464905",
+ "closeDivision" : "09",
+ "incorporatedOnDay" : "20170726",
+ "industryClassification" : null,
+ "representativeName" : "호세인사로와",
+ "representativeBirthday" : null,
+ "typeOfBusiness" : null,
+ "businessCondition" : null,
+ "partnerCode" : "000014-01",
+ "corporateEmployee" : [ ],
+ "defaultAddress" : null,
+ "defaultContact" : null
+}
+```
\ No newline at end of file
diff --git a/kjb-docs/obp/organization-commission/commission.http b/kjb-docs/obp/organization-commission/commission.http
new file mode 100644
index 0000000..dfb4d85
--- /dev/null
+++ b/kjb-docs/obp/organization-commission/commission.http
@@ -0,0 +1,151 @@
+POST /api/admin/commission?cacheBuster=1764308323034 HTTP/1.1
+Accept: application/json, text/plain, */*
+Accept-Encoding: gzip, deflate
+Accept-Language: ko,en;q=0.9,en-US;q=0.8
+Connection: keep-alive
+Content-Length: 235
+Content-Type: application/json;charset=UTF-8
+Cookie: XSRF-TOKEN=a8bfa79d-f739-480a-a3d8-ca524efe8928; NG_TRANSLATE_LANG_KEY=%22ko%22; JSESSIONID_APIPORTAL=F53D8E820FAA78EBE1FF7CD197F67650; loginStatus=success; showNotice=
+Host: 192.168.246.16:8080
+Origin: http://192.168.246.16:8080
+Referer: http://192.168.246.16:8080/
+User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62
+X-XSRF-TOKEN: a8bfa79d-f739-480a-a3d8-ca524efe8928
+
+{"provider":{"id":15,"name":"OBP","description":"OBP","type":"PROVIDER","state":"ACTIVE"},"organization":{"id":70,"name":"글로벌머니익스프레스","description":null,"type":"PARTNER","state":"ACTIVE"},"year":"2025","month":"01"}
+
+
+HTTP/1.1 200
+X-Application-Context: portalApplication:swagger,dev:8085
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+Content-Type: application/json;charset=UTF-8
+Transfer-Encoding: chunked
+Date: Fri, 28 Nov 2025 05:38:43 GMT
+Keep-Alive: timeout=20
+Connection: keep-alive
+
+{
+"status" : "01",
+"apiTotalValueSum" : "0.00",
+"apiSum" : "0.00",
+"apiTotalValueSum2" : "0.00",
+"apiSum2" : "0.00",
+"productTotalValueCountSum" : "7",
+"productTotalValueAmountSum" : "0",
+"productSum" : "200000.00",
+"productTotalValueCountSum2" : "7",
+"productTotalValueAmountSum2" : "0",
+"productSum2" : "200000.00",
+"totalValueCountSum" : "7.00",
+"totalValueCountSum2" : "7.00",
+"totalValueAmountSum" : "0",
+"totalValueAmountSum2" : "0",
+"sum" : "200000.00",
+"sum2" : "200000.00",
+"list" : [ {
+"paymentTargetName" : "예금주성명조회 API",
+"paymentTargetType" : "01",
+"value" : "100.00",
+"minimumAmount" : "0.00",
+"maximumAmount" : null,
+"totalCount" : "0",
+"totalCount2" : "0",
+"totalValue" : "0.00",
+"totalValue2" : "0.00",
+"minimumAmount2" : "0.00",
+"maximumAmount2" : null,
+"paymentBaseName" : "건",
+"paymentTimingName" : "사후부과",
+"valueDivisionName" : "금액",
+"sum" : "0.00",
+"sum2" : "0.00",
+"detailFeeTypeName" : null,
+"value2" : "100.00",
+"changeReason2" : "system billing"
+}, {
+"paymentTargetName" : "[계좌] 실지명의조회 API",
+"paymentTargetType" : "01",
+"value" : "100.00",
+"minimumAmount" : "0.00",
+"maximumAmount" : null,
+"totalCount" : "0",
+"totalCount2" : "0",
+"totalValue" : "0.00",
+"totalValue2" : "0.00",
+"minimumAmount2" : "0.00",
+"maximumAmount2" : null,
+"paymentBaseName" : "건",
+"paymentTimingName" : "사후부과",
+"valueDivisionName" : "금액",
+"sum" : "0.00",
+"sum2" : "0.00",
+"detailFeeTypeName" : null,
+"value2" : "100.00",
+"changeReason2" : "system billing"
+}, {
+"paymentTargetName" : "가상계좌 API",
+"paymentTargetType" : "02",
+"value" : "400.00",
+"minimumAmount" : "100000.00",
+"maximumAmount" : null,
+"totalCount" : "7",
+"totalCount2" : "7",
+"totalValue" : "7.00",
+"totalValue2" : "7.00",
+"minimumAmount2" : "100000.00",
+"maximumAmount2" : null,
+"paymentBaseName" : "건",
+"paymentTimingName" : "사후부과",
+"valueDivisionName" : "금액",
+"sum" : "100000.00",
+"sum2" : "100000.00",
+"detailFeeTypeName" : "API",
+"value2" : "400.00",
+"changeReason2" : "system billing"
+}, {
+"paymentTargetName" : "입금이체(당행) API",
+"paymentTargetType" : "02",
+"value" : "100.00",
+"minimumAmount" : "0.00",
+"maximumAmount" : null,
+"totalCount" : "0",
+"totalCount2" : "0",
+"totalValue" : "0.00",
+"totalValue2" : "0.00",
+"minimumAmount2" : "0.00",
+"maximumAmount2" : null,
+"paymentBaseName" : "건",
+"paymentTimingName" : "사후부과",
+"valueDivisionName" : "금액",
+"sum" : "0.00",
+"sum2" : "0.00",
+"detailFeeTypeName" : "API",
+"value2" : "100.00",
+"changeReason2" : "system billing"
+}, {
+"paymentTargetName" : "입금이체(타행) API",
+"paymentTargetType" : "02",
+"value" : "400.00",
+"minimumAmount" : "100000.00",
+"maximumAmount" : null,
+"totalCount" : "0",
+"totalCount2" : "0",
+"totalValue" : "0.00",
+"totalValue2" : "0.00",
+"minimumAmount2" : "100000.00",
+"maximumAmount2" : null,
+"paymentBaseName" : "건",
+"paymentTimingName" : "사후부과",
+"valueDivisionName" : "금액",
+"sum" : "100000.00",
+"sum2" : "100000.00",
+"detailFeeTypeName" : "API",
+"value2" : "400.00",
+"changeReason2" : "system billing"
+} ],
+"documentFormatNo" : null
+}
\ No newline at end of file
diff --git a/kjb-docs/sso/WEB-INF/lib/INICrypto_v4.0.12.jar b/kjb-docs/sso/WEB-INF/lib/INICrypto_v4.0.12.jar
new file mode 100644
index 0000000..09636c9
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/INICrypto_v4.0.12.jar differ
diff --git a/kjb-docs/sso/WEB-INF/lib/external_v4.2.0.02.jar b/kjb-docs/sso/WEB-INF/lib/external_v4.2.0.02.jar
new file mode 100644
index 0000000..99afc6c
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/external_v4.2.0.02.jar differ
diff --git a/kjb-docs/sso/WEB-INF/lib/jdom.jar b/kjb-docs/sso/WEB-INF/lib/jdom.jar
new file mode 100644
index 0000000..4736a4e
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/jdom.jar differ
diff --git a/kjb-docs/sso/WEB-INF/lib/log4j-1.2.16.jar b/kjb-docs/sso/WEB-INF/lib/log4j-1.2.16.jar
new file mode 100644
index 0000000..3f9d847
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/log4j-1.2.16.jar differ
diff --git a/kjb-docs/sso/WEB-INF/lib/poolit.jar b/kjb-docs/sso/WEB-INF/lib/poolit.jar
new file mode 100644
index 0000000..272db37
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/poolit.jar differ
diff --git a/kjb-docs/sso/WEB-INF/lib/xercesImpl.jar b/kjb-docs/sso/WEB-INF/lib/xercesImpl.jar
new file mode 100644
index 0000000..33990e8
Binary files /dev/null and b/kjb-docs/sso/WEB-INF/lib/xercesImpl.jar differ
diff --git a/kjb-docs/sso/app01.jsp b/kjb-docs/sso/app01.jsp
new file mode 100644
index 0000000..0a8e495
--- /dev/null
+++ b/kjb-docs/sso/app01.jsp
@@ -0,0 +1,48 @@
+<%@ page language="java" contentType="text/html; charset=EUC-KR"
+ pageEncoding="EUC-KR"%>
+
+
+<%
+ String user_id = (String)session.getAttribute("SSO_ID");
+ String app_id = (String)session.getAttribute("APP_ID");
+ String ext_info = (String)session.getAttribute("EXT_INFO");
+ String retCode = (String)session.getAttribute("RET_CODE");
+
+%>
+
+
+
+Insert title here
+
+
+ Single Sign On [<%=user_id %>]
+
+ [ 업무시스템 - (SAMPLE_APP)]
+
+
+
+
+ | * 사용자ID |
+ <%=user_id%> |
+
+
+
+
+ | * SERVICE_NAME |
+ <%=app_id%> |
+
+
+
+ | * 확장정보 |
+ <%=ext_info%> |
+
+
+
+ | * 인증토큰 상태 |
+ retCode = [<%=retCode%>] (1000:누락, 1001:시간, 1002:비정상) |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kjb-docs/sso/config.jsp b/kjb-docs/sso/config.jsp
new file mode 100644
index 0000000..653d772
--- /dev/null
+++ b/kjb-docs/sso/config.jsp
@@ -0,0 +1,295 @@
+<%@ page
+ import="sun.misc.*,
+ java.util.*,
+ java.math.*,
+ java.net.*,
+ com.initech.eam.nls.*,
+ com.initech.eam.api.*,
+ com.initech.eam.base.*,
+ com.initech.eam.nls.command.*,
+ com.initech.eam.smartenforcer.*,
+ examples.api.*"
+%>
+<%@page import="com.initech.eam.nls.CookieManager"%>
+<%!
+/**[INISAFE NEXESS JAVA AGENT]**********************************************************************
+* 업무시스템 설정 사항 (업무 환경에 맞게 변경)
+***************************************************************************************************/
+
+
+/***[SERVICE CONFIGURATION]***********************************************************************/
+ private String SERVICE_NAME = "Web";
+ private String SERVER_URL = "http://test.kjbank.com";
+ private String SERVER_PORT = "8080";
+ private String ASCP_URL = SERVER_URL + ":" + SERVER_PORT + "/se/login_exec.jsp";
+/*************************************************************************************************/
+
+
+/***[SSO CONFIGURATION]**]***********************************************************************/
+ private String NLS_URL = "http://sso.kjbank.com";
+ private String NLS_PORT = "13890";
+ private String NLS_LOGIN_URL = NLS_URL + ":" + NLS_PORT + "/nls3/clientLogin.jsp";
+ private String NLS_LOGOUT_URL= NLS_URL + ":" + NLS_PORT + "/nls3/NCLogout.jsp";
+ private String NLS_ERROR_URL = NLS_URL + ":" + NLS_PORT + "/nls3/error.jsp";
+
+/***[SSO ND LIST 운영]**]***********************************************************************/
+ private static String ND_URL1 = "http://sso.kjbank.com:5480";
+
+
+ private static Vector PROVIDER_LIST = new Vector();
+
+ private static final int COOKIE_SESSTION_TIME_OUT = 3000000;
+
+ // 인증 타입 (ID/PW 방식 : 1, 인증서 : 3)
+ private String TOA = "1";
+ private String SSO_DOMAIN = ".kjbank.com";
+
+ private static final int timeout = 15000;
+ private static NXContext context = null;
+ static{
+ List serverurlList = new ArrayList();
+ serverurlList.add(ND_URL1);
+
+ context = new NXContext(serverurlList,timeout);
+ CookieManager.setEncStatus(true);
+
+ PROVIDER_LIST.add("sso.kjbank.com");
+ SECode.setCookiePadding("_V42");
+ }
+
+ // by Kang
+ public NXContext getContext()
+ {
+ NXContext context = null;
+ try
+ {
+ List serverurlList = new ArrayList();
+ serverurlList.add(ND_URL1);
+ context = new NXContext(serverurlList);
+ CookieManager.setEncStatus(true);
+
+ PROVIDER_LIST.add("sso.kjbank.com");
+ //NLS3 web.xml의 CookiePadding 값과 같아야 한다. 안그럼 검증 페일남
+ SECode.setCookiePadding("_V42");
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ return context;
+ }
+
+ // 통합 SSO ID 조회
+ public String getSsoId(HttpServletRequest request) {
+ String sso_id = null;
+ sso_id = CookieManager.getCookieValue(SECode.USER_ID, request);
+ return sso_id;
+ }
+
+ public String getSystemAccount(String sso_id) {
+ NXContext context = null;
+ NXUserAPI userAPI = null;
+ String retValue = null;
+
+ try {
+ context = getContext();
+ userAPI = new NXUserAPI(context);
+ System.out.println("##############################################");
+ System.out.println("sso_id 2: "+sso_id);
+ System.out.println("SERVICE_NAME : "+SERVICE_NAME);
+ System.out.println("##############################################");
+ NXAccount account = userAPI.getUserAccount(sso_id, SERVICE_NAME);
+ System.out.println(account.toString());
+ retValue = account.getAccountName();
+ System.out.println("retValue = " + retValue);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return retValue;
+ }
+
+ // 통합 SSO 로그인페이지 이동
+ public void goLoginPage(HttpServletResponse response, String uurl)
+ throws Exception {
+ com.initech.eam.nls.CookieManager.addCookie(SECode.USER_URL, uurl, SSO_DOMAIN, response);
+ com.initech.eam.nls.CookieManager.addCookie(SECode.R_TOA, TOA, SSO_DOMAIN, response);
+ response.sendRedirect(NLS_LOGIN_URL + "?UURL=" + uurl);
+ }
+
+ // 통합 SSO 로그인페이지 이동
+ public void goLoginPage(HttpServletResponse response)throws Exception {
+ CookieManager.addCookie(SECode.USER_URL, ASCP_URL, SSO_DOMAIN, response);
+ CookieManager.addCookie(SECode.R_TOA, TOA, SSO_DOMAIN, response);
+ response.sendRedirect(NLS_LOGIN_URL);
+ }
+
+ // 통합인증 세션을 체크 하기 위하여 사용되는 API
+ public String getEamSessionCheck(HttpServletRequest request,HttpServletResponse response){
+ String retCode = "";
+ try {
+ retCode = CookieManager.verifyNexessCookie(request, response, 10, COOKIE_SESSTION_TIME_OUT,PROVIDER_LIST);
+ } catch(Exception npe) {
+ npe.printStackTrace();
+ }
+ return retCode;
+ }
+
+ //ND API를 사용해서 쿠키검증하는것(현재 표준에서는 사용안함, 근데 해도 되기는 함)
+ public String getEamSessionCheck2(HttpServletRequest request,HttpServletResponse response)
+ {
+ String retCode = "";
+ try {
+ NXNLSAPI nxNLSAPI = new NXNLSAPI(context);
+ retCode = nxNLSAPI.readNexessCookie(request, response, 0, 0);
+ } catch(Exception npe) {
+ npe.printStackTrace();
+ }
+ return retCode;
+ }
+
+ // SSO 에러페이지 URL
+ public void goErrorPage(HttpServletResponse response, int error_code)throws Exception {
+ CookieManager.removeNexessCookie(SSO_DOMAIN, response);
+ CookieManager.addCookie(SECode.USER_URL, ASCP_URL, SSO_DOMAIN, response);
+ response.sendRedirect(NLS_ERROR_URL + "?errorCode=" + error_code);
+ }
+
+ /**
+ * 사용자 기본정보 가져오기.
+ * @param String userid
+ * @return Properties
+ */
+ public Properties getUserInfos(String userid) throws Exception {
+
+ NXContext context = null;
+ context = getContext();
+
+ NXUserAPI userAPI = new NXUserAPI(context);
+ Properties propt = null;
+
+ if (userid==null || userid.length() <= 0)
+ return propt;
+
+ try {
+ NXUserInfo userInfo = userAPI.getUserInfo(userid);
+ propt = new Properties();
+ //System.out.println("userInfo.getName()=["+userInfo.getName()+"]");
+ propt.setProperty("USERID", userInfo.getUserId());
+ propt.setProperty("EMAIL", userInfo.getEmail());
+ propt.setProperty("ENABLE", String.valueOf(userInfo.getEnable()));
+ propt.setProperty("STARTVALID", userInfo.getStartValid());
+ propt.setProperty("ENDVALID", userInfo.getEndValid());
+ propt.setProperty("NAME", userInfo.getName());
+ propt.setProperty("ENCPASSWD", userInfo.getEncpasswd());
+ propt.setProperty("LASTPASSWDCHANGE", userInfo.getLastpasswdchange());
+ propt.setProperty("LastLoginIP", userInfo.getLastLoginIp());
+ propt.setProperty("LastLoginTime", userInfo.getLastLoginTime());
+ propt.setProperty("LastLoginAuthLevel", userInfo.getLastLoginAuthLevel());
+
+ } catch (EmptyResultException e) {
+ e.printStackTrace();
+ } catch (APIException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return propt;
+ }
+
+ /**
+ * 사용자의 기본정보 중에서 입력된 키 값만 가지고 온다.
+ * @param String userid
+ * @param String key
+ * @return String
+ */
+ public String getUserInfo(String userid, String key) throws Exception {
+ if(userid==null||userid.length()<1) return null;
+
+ String userInfo = "";
+
+ try{
+ Properties propt = getUserInfos(userid);
+ userInfo = propt.getProperty(key);
+ } catch (Exception e) {
+ //throw e;
+ e.printStackTrace();
+ }
+ return userInfo;
+ }
+
+ /**
+ * 사용자 확장정보 조회
+ * return : Properties
+ * 사용자가 존재하지 않거나 확장필드가 없다면 return null
+ */
+ public Properties getUserExFields(String userid)
+ throws Exception {
+ NXContext context = getContext();
+ NXUserAPI userAPI = new NXUserAPI(context);
+ Properties prop = null;
+ NXExternalFieldSet nxefs = null;
+
+ try {
+ nxefs = userAPI.getUserExternalFields(userid);
+ prop = new Properties();
+
+ if (nxefs != null) {
+ Iterator iter = nxefs.iterator();
+ while(iter.hasNext()) {
+ NXExternalField nxef = (NXExternalField) iter.next();
+ if(nxef.getName().equals("PASSINIT")){
+ String sPassintVal = (String) nxef.getValue();
+ prop.setProperty(nxef.getName(), sPassintVal.equals("") ? "0" : sPassintVal);
+ }
+ else
+ prop.setProperty(nxef.getName(), (String) nxef.getValue());
+ //System.out.println("확장 필드 " + nxef.getName() + " : " + nxef.getValue());
+ }
+ } else {
+ // prop is null
+ }
+ } catch (EmptyResultException e) {
+ // 사용자가 존재하지 않거나 확장필드가 없음
+ } catch (APIException e) {
+ throw e;
+ //e.printStackTrace();
+ }
+ //System.out.println("getUserExFields:[" + prop + "]");
+ return prop;
+ }
+
+ /**
+ * 사용자의 특정 확장 필드 정보 조회
+ * return : String
+ * 사용자가 존재하지 않거나 확장필드가 없다면 return null
+ */
+ public String getUserExField(String userid, String exName)
+ throws Exception {
+ NXContext context = getContext();
+ NXUserAPI userAPI = new NXUserAPI(context);
+ NXExternalField nxef = null;
+ String returnValue = null;
+
+ try {
+
+System.out.println("getUserExField==="+ userid ) ;
+ nxef = userAPI.getUserExternalField(userid, exName);
+System.out.println("nxef==="+ (String) nxef.getValue() ) ;
+ returnValue = (String) nxef.getValue();
+ } catch (EmptyResultException e) {
+ // 사용자가 존재하지 않거나 확장필드가 없음
+ } catch (APIException e) {
+ throw e;
+ //e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ throw e;
+ //e.printStackTrace();
+ }
+ /*
+ System.out.println("getUserExField:[" + userid + ":"
+ + exName + ":" + returnValue+ "]");
+ */
+ return returnValue;
+ }
+
+%>
diff --git a/kjb-docs/sso/login_exec.jsp b/kjb-docs/sso/login_exec.jsp
new file mode 100644
index 0000000..2d2b41f
--- /dev/null
+++ b/kjb-docs/sso/login_exec.jsp
@@ -0,0 +1,69 @@
+<%@ page language="java" contentType="text/html;charset=EUC-KR" %>
+<%@ include file="./config.jsp" %>
+<%
+
+ String uurl = null;
+ String sso_id = null;
+ String service_id = null;
+ String ext_info = null;
+ String retCode = null;
+ //http://test.kjbank.com:8080/se/login_exec.jsp : 꼭 도메인으로 호출해야 된다.
+ //1.SSO ID 수신
+ sso_id = getSsoId(request);
+
+ // 2. UURL 수신
+ uurl = request.getParameter("UURL");
+
+ if (sso_id == null) {
+ //uurl 이 없다면, 근데 여기서 uurl 은 뭘 체크하는거지? uurl 에 test.kjbank.com:8080/se/login_exec.jsp 를 넣는구나.
+ if (uurl == null) uurl = ASCP_URL;
+
+ // 3. SSO 인증 정보가 없으면 통합 로그인 페이지로 이동
+ goLoginPage(response, uurl);
+ return;
+ } else {
+
+ //4.쿠키 유효성 확인 :0(정상)
+ retCode = getEamSessionCheck(request,response);
+
+ if(!retCode.equals("0")){
+ goErrorPage(response, Integer.parseInt(retCode));
+ return;
+ }
+ /***********************************************************************************
+ * < TO-DO >
+ * SSO 통합 인증이 완료되었으므로, 업무 시스템 로그인은 건너뛰도록 코드를 수정합니다.
+ * SSO 에서 제공한 사번은 sso_id 변수에 저장되어 있습니다.
+ * 시스템에서 필요로 하는 세션 정보를 세팅 코드를 여기에 작성합니다.
+ ************************************************************************************/
+
+ //5 확장 정보 조회
+ ext_info = getUserExField(sso_id, "CELLPHONENO");
+ service_id = SERVICE_NAME;
+ System.out.println ("SERVICE_NAME : " + service_id);
+ //
+ //6.업무시스템에 읽을 사용자 아이디를 세션으로 생성
+ /* 변경 전
+ String EAM_ID = (String)session.getAttribute("SSO_ID");
+ if(EAM_ID == null || EAM_ID.equals("")) {
+ session.setAttribute("SSO_ID", sso_id);
+ session.setAttribute("APP_ID", service_id);
+ session.setAttribute("EXT_INFO", ext_info);
+ session.setAttribute("RET_CODE", retCode);
+ }
+ */
+
+
+ /*변경 후*/
+ if(!sso_id.replace(" ", "").equals("")) {
+ session.setAttribute("SSO_ID", sso_id);
+ session.setAttribute("APP_ID", service_id);
+ session.setAttribute("EXT_INFO", ext_info);
+ session.setAttribute("RET_CODE", retCode);
+ }
+
+ //7.업무시스템 페이지 호출(세션 페이지 또는 메인페이지 지정) --> 업무시스템에 맞게 URL 수정!
+ response.sendRedirect(SERVER_URL + ":" + SERVER_PORT + "/se/app01.jsp");
+ }
+%>
+
diff --git a/kjb-docs/sso/광주은행_SSO연동가이드.zip b/kjb-docs/sso/광주은행_SSO연동가이드.zip
new file mode 100644
index 0000000..6e533e9
Binary files /dev/null and b/kjb-docs/sso/광주은행_SSO연동가이드.zip differ
diff --git a/obp.http b/obp.http
new file mode 100644
index 0000000..ce3ab3e
--- /dev/null
+++ b/obp.http
@@ -0,0 +1,463 @@
+POST http://192.168.246.15:8181/api/billing/findBillingForCondition
+Content-Type: application/json; charset=UTF-8
+x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=
+x-obp-partnercode: 100001-01
+
+{
+ "partnerCode": "000002-01",
+ "targetOnMonth": "202503"
+}
+
+
+###
+
+ curl -X POST --location "http://192.168.246.15:8181/api/billing/billing/findBillingForCondition" \
+ -H "Content-Type: application/json; charset=UTF-8" \
+ -H "x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=" \
+ -H "x-obp-partnercode: 100001-01" \
+ -d '{
+ "partnerCode": "000002-01",
+ "targetOnMonth": "202510"
+ }'
+POST http://192.168.246.15:8181/api/billing/findBillingForCondition
+x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=
+x-obp-partnercode: 100001-01
+Content-Type: application/json; charset=UTF-8
+
+{
+ "partnerCode": "000002-01",
+ "targetOnMonth": "202503"
+}
+
+###
+
+
+
+
+{
+ "billingId" : "001-20250401-060001-0001467",
+ "status" : "01",
+ "apiTotalValueSum" : 0.00,
+ "apiSum" : 100000.00,
+ "apiTotalValueSum2" : 0.00,
+ "apiSum2" : 100000.00,
+ "productTotalValueCountSum" : 0,
+ "productTotalValueAmountSum" : 0.00,
+ "productSum" : 600000.00,
+ "productTotalValueCountSum2" : 0,
+ "productTotalValueAmountSum2" : 0.00,
+ "productSum2" : 600000.00,
+ "totalValueCountSum" : 0.00,
+ "totalValueCountSum2" : 0.00,
+ "totalValueAmountSum" : 0.00,
+ "totalValueAmountSum2" : 0.00,
+ "sum" : 700000.00,
+ "sum2" : 700000.00,
+ "list" : [ {
+ "id" : "001-20250401-060001-0001469",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "[계좌] 실지명의조회",
+ "paymentTargetType" : "01",
+ "paymentTargetId" : "001-20180918-085325-0000353",
+ "detailFeeType" : "11",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "001-20201012-154617-0000969",
+ "feeDetailId" : "001-20221115-105403-0011851",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 100.00,
+ "minimumAmount" : 0.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 0.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 100.00,
+ "minimumAmount2" : 0.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 0.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : null,
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 0.00,
+ "sum2" : 0.00
+ }, {
+ "id" : "001-20250401-060001-0001468",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "예금주성명조회 API",
+ "paymentTargetType" : "01",
+ "paymentTargetId" : "001-20180419-200001-0000100",
+ "detailFeeType" : "11",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131549-0085992",
+ "feeDetailId" : "101-20180516-131549-0085993",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 100.00,
+ "minimumAmount" : 100000.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 100000.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 100.00,
+ "minimumAmount2" : 100000.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 100000.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : null,
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 100000.00,
+ "sum2" : 100000.00
+ }, {
+ "id" : "001-20250401-060002-0001472",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "P2P 가상계좌(투자자용) API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000003",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0085994",
+ "feeDetailId" : "101-20180516-131550-0085995",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 300.00,
+ "minimumAmount" : 200000.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 200000.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 300.00,
+ "minimumAmount2" : 200000.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 200000.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 200000.00,
+ "sum2" : 200000.00
+ }, {
+ "id" : "001-20250401-060002-0001476",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "P2P 지급이체(당행_프로모션) API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000007",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0085996",
+ "feeDetailId" : "101-20180516-131550-0085997",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 0.00,
+ "minimumAmount" : 0.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 0.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 0.00,
+ "minimumAmount2" : 0.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 0.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 0.00,
+ "sum2" : 0.00
+ }, {
+ "id" : "001-20250401-060002-0001474",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "P2P 지급이체 API(타행)",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000005",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0085998",
+ "feeDetailId" : "101-20180516-131550-0085999",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 300.00,
+ "minimumAmount" : 200000.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 200000.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 300.00,
+ "minimumAmount2" : 200000.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 200000.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 200000.00,
+ "sum2" : 200000.00
+ }, {
+ "id" : "001-20250401-060002-0001473",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "P2P 지급이체(대출실행집금용) API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000004",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0086000",
+ "feeDetailId" : "101-20180516-131550-0086001",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 50.00,
+ "minimumAmount" : 0.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 0.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 50.00,
+ "minimumAmount2" : 0.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 0.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 0.00,
+ "sum2" : 0.00
+ }, {
+ "id" : "001-20250401-060002-0001475",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "P2P 투자금 관리 API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000006",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0086002",
+ "feeDetailId" : "001-20231128-161411-0007408",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "01",
+ "value" : 0.05,
+ "minimumAmount" : 200000.00,
+ "maximumAmount" : null,
+ "paymentBase" : "02",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 200000.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 0.05,
+ "minimumAmount2" : 200000.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 200000.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "금액",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "율",
+ "sum" : 200000.00,
+ "sum2" : 200000.00
+ }, {
+ "id" : "001-20250401-060001-0001470",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "입금이체(당행) API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000001",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0086004",
+ "feeDetailId" : "101-20180516-131550-0086005",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 50.00,
+ "minimumAmount" : 0.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 0.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 50.00,
+ "minimumAmount2" : 0.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 0.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 0.00,
+ "sum2" : 0.00
+ }, {
+ "id" : "001-20250401-060001-0001471",
+ "billingId" : "001-20250401-060001-0001467",
+ "paymentTargetName" : "입금이체(타행) API",
+ "paymentTargetType" : "02",
+ "paymentTargetId" : "001-20171227-110000-0000002",
+ "detailFeeType" : "21",
+ "revision" : 0,
+ "contractAccountId" : "001-20180430-104631-0798825",
+ "feeId" : "101-20180516-131550-0086006",
+ "feeDetailId" : "101-20180516-131550-0086007",
+ "bank" : "034",
+ "accountNo" : "1107617597186",
+ "valueDivision" : "02",
+ "value" : 300.00,
+ "minimumAmount" : 0.00,
+ "maximumAmount" : null,
+ "paymentBase" : "01",
+ "paymentBaseValue" : 1.00,
+ "paymentTiming" : "02",
+ "totalValue" : 0.00,
+ "withdrawalAmount" : 0.00,
+ "totalCount" : 0,
+ "revision2" : 0,
+ "totalValue2" : 0.00,
+ "value2" : 300.00,
+ "minimumAmount2" : 0.00,
+ "maximumAmount2" : null,
+ "changeReason2" : "system billing",
+ "withdrawalAmount2" : 0.00,
+ "totalCount2" : 0,
+ "detailFeeTypeName" : "상품수수료",
+ "paymentBaseName" : "건",
+ "paymentTimingName" : "사후부과",
+ "valueDivisionName" : "금액",
+ "sum" : 0.00,
+ "sum2" : 0.00
+ } ]
+}
+
+
+
+
+
+$ curl -X POST --location "http://192.168.246.15:8181/api/billing/billing/findBillingForCondition/print" -H "Content-Type: application/json; charset=UTF-8" -H "x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=" -H "x-obp-partnercode: 100001-01" -d '{
+ "partnerCode": "000014-01",
+ "targetOnMonth": "202503"
+ }'
+{
+ "billingId" : "001-20250401-060004-0001512",
+ "status" : "01",
+ "apiTotalValueSum" : 0,
+ "apiSum" : 0,
+ "apiTotalValueSum2" : 0,
+ "apiSum2" : 0,
+ "productTotalValueCountSum" : 0,
+ "productTotalValueAmountSum" : 0,
+ "productSum" : 0,
+ "productTotalValueCountSum2" : 0,
+ "productTotalValueAmountSum2" : 0,
+ "productSum2" : 0,
+ "totalValueCountSum" : 0,
+ "totalValueCountSum2" : 0,
+ "totalValueAmountSum" : 0,
+ "totalValueAmountSum2" : 0,
+ "sum" : 0,
+ "sum2" : 0,
+ "list" : [ ]
+}
+
+
+
+
+
+curl -X POST --location "http://192.168.246.15:8181/api/billing/billing/findBillingForCondition/print" -H "Content-Type: application/json; charset=UTF-8" -H "x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=" -H "x-obp-partnercode: 100001-01" -d '{
+> "partnerCode": "000014-01",
+> "targetOnMonth": "202503"
+> }'
+{
+ "error" : {
+ "code" : "E.BILLING.NOT_PRINT_BEFORE_CONFIRM",
+ "message" : "확정이전에는 출력이 안됩니다."
+ }
+}
+
+
+
+
+
+
+weblogic@apigwd01(DEV-eAPIM-GW#1):[/weblogic]
+$ curl -X POST --location "http://192.168.246.15:8181/api/billing/billing/findBillingForCondition/print" -H "Content-Type: application/json; charset=UTF-8" -H "x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=" -H "x-obp-partnercode: 100001-01" -d '{
+ "partnerCode": "000002-02",
+ "targetOnMonth": "201803"
+ }'
+{
+ "error" : {
+ "code" : "E.BILLING.NOT_PRINT_BEFORE_CONFIRM",
+ "message" : "확정이전에는 출력이 안됩니다."
+ }
+}
+weblogic@apigwd01(DEV-eAPIM-GW#1):[/weblogic]
+$ curl -X POST --location "http://192.168.246.15:8181/api/billing/billing/findBillingForCondition/print" -H "Content-Type: application/json; charset=UTF-8" -H "x-obp-trust-system: APIMPT-0002-QVBJTVBULTAwMDI=" -H "x-obp-partnercode: 100001-01" -d '{
+ "partnerCode": "000002-02",
+ "targetOnMonth": "199603"
+ }'
+{
+ "error" : {
+ "code" : "E.BILLING.NOT_PRINT_BEFORE_CONFIRM",
+ "message" : "확정이전에는 출력이 안됩니다."
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbObpException.java b/src/main/java/com/eactive/ext/kjb/common/KjbObpException.java
new file mode 100644
index 0000000..93d9e8b
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/common/KjbObpException.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java b/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java
index f848d93..554da7b 100644
--- a/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java
+++ b/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java
@@ -3,6 +3,9 @@ package com.eactive.ext.kjb.common;
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Pattern;
@@ -29,17 +32,17 @@ public class KjbProperty {
@NotEmpty
private String eaiBatchCharset = "euc-kr";
- @KjbPropertyValue(key = "kjb.ums.eai_batch.send_dir")
+ @KjbPropertyValue(key = "kjb.ums.eai_batch.send_dir", defaultValue = "/Data/eapim/portal/sendmail/snd")
@NotEmpty(message = "kjb.ums.eai_batch.send_dir 는 필수값입니다.")
- private String umsEaiBatchSendDir;
+ private String umsEaiBatchSendDir = "/Data/eapim/portal/sendmail/snd";
- @KjbPropertyValue(key = "kjb.ums.eai_batch.backup_dir")
+ @KjbPropertyValue(key = "kjb.ums.eai_batch.backup_dir", defaultValue = "/Data/eapim/portal/sendmail/bak")
@NotEmpty(message = "kjb.ums.eai_batch.backup_dir 는 필수값입니다.")
- private String umsEaiBatchBackupDir;
+ private String umsEaiBatchBackupDir = "/Data/eapim/portal/sendmail/bak";
- @KjbPropertyValue(key = "kjb.ums.eai_batch.error_dir")
+ @KjbPropertyValue(key = "kjb.ums.eai_batch.error_dir", defaultValue = "/Data/eapim/portal/sendmail/bak")
@NotEmpty(message = "kjb.ums.eai_batch.error_dir 는 필수값입니다.")
- private String umsEaiBatchErrorDir;
+ private String umsEaiBatchErrorDir = "/Data/eapim/portal/sendmail/bak";
@KjbPropertyValue(key = "kjb.ums.eai_batch.interface_id")
@Pattern(regexp = "^[a-zA-Z0-9]{13}$", message = "kjb.ums.eai_batch.interface_id 는 12자리 영문대소문자 및 숫자 조합이어야 합니다.")
@@ -69,4 +72,189 @@ public class KjbProperty {
@KjbPropertyValue(key = "kjb.ums.timeout", defaultValue = "10")
@Min(1) @Max(30)
private int umsTimeout = 10;
+
+
+ @KjbPropertyValue(key = "kjb.obp.url")
+ @Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.obp.url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api")
+ private String obpUrl;
+
+ @KjbPropertyValue(key = "kjb.obp.trust_system.key", defaultValue = "x-obp-trust-system")
+ @NotEmpty
+ private String obpTrustSystemKey = "x-obp-trust-system";
+
+ @KjbPropertyValue(key = "kjb.obp.trust_system.value", defaultValue = "APIMPT-0002-QVBJTVBULTAwMDI=")
+ @NotEmpty
+ private String obpTrustSystemValue = "APIMPT-0002-QVBJTVBULTAwMDI=";
+
+ @KjbPropertyValue(key = "kjb.obp.partner_code.key", defaultValue = "x-obp-partnercode")
+ @NotEmpty
+ private String obpPartnerCodeKey = "x-obp-partnercode";
+
+ @KjbPropertyValue(key = "kjb.obp.partner_code.value", defaultValue = "100001-01")
+ @NotEmpty
+ 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")
+ @NotEmpty
+ private String obpUrlCommission = "/api/billing/billing/findBillingForCondition";
+
+ @KjbPropertyValue(key = "kjb.obp.url.commission_print", defaultValue = "/api/billing/billing/findBillingForCondition/print")
+ @NotEmpty
+ private String obpUrlCommissionPrint = "/api/billing/billing/findBillingForCondition/print";
+
+ @KjbPropertyValue(key = "kjb.obp.url.organization_fmt", defaultValue = "/api/customer/findCustomerByBusinessManRegistrationNo/%s")
+ @NotEmpty
+ private String obpUrlOrganizationFmt = "/api/customer/findCustomerByBusinessManRegistrationNo/%s";
+
+
+ // 광주은행 SSO
+
+ @KjbPropertyValue(key = "kjb.url", defaultValue = "https://manage-eapim.kjbank.com")
+ @NotEmpty
+ private String url = "https://manage-eapim.kjbank.com";
+
+ @KjbPropertyValue(key = "kjb.sso.service_name", defaultValue = "Web")
+ @NotEmpty
+ private String ssoServiceName = "Web";
+
+ @KjbPropertyValue(key = "kjb.sso.ascp_url", defaultValue = "/monitoring/sso/login.do")
+ @NotEmpty
+ private String ssoAscpUri = "/monitoring/sso/login.do";
+
+ @KjbPropertyValue(key = "kjb.sso.domain", defaultValue = "sso.kjbank.com")
+ @NotEmpty
+ private String ssoDomain = "sso.kjbank.com";
+
+ @KjbPropertyValue(key = "kjb.sso.nls_port", defaultValue = "13890")
+ @Min(1024) @Max(65535)
+ private int ssoNlsPort = 13890;
+
+ @KjbPropertyValue(key = "kjb.sso.ignore_validation", defaultValue = "false")
+ private boolean ssoIgnoreValidation = false;
+
+ @KjbPropertyValue(
+ key = "kjb.sso.sync_info",
+ defaultValue = "true",
+ description = "SSO 인증시 사용자 정보 동기화 여부(false 시 이름과 ID를 동일하게 처리. 나머지 정보는 무시)")
+ private boolean ssoSyncInfo = true;
+
+
+ // SMS 2차 인증
+
+ @KjbPropertyValue(
+ key = "kjb.sms_auth.enabled",
+ defaultValue = "true",
+ description = "SMS 2차 인증 사용 여부. true 시 휴대폰번호 없는 사용자는 ID/PW 로그인 불가")
+ private boolean smsAuthEnabled = false;
+
+ @KjbPropertyValue(
+ key = "kjb.sms_auth.mode",
+ defaultValue = "real",
+ description = "인증번호 생성 모드: real(랜덤), hhmm00(현재시각+00), fixed(고정값)")
+ @Pattern(regexp = "^(real|hhmm00|fixed)$", message = "kjb.sms_auth.mode는 real, hhmm00, fixed 중 하나여야 합니다.")
+ private String smsAuthMode = "real";
+
+ @KjbPropertyValue(
+ key = "kjb.sms_auth.fixed_value",
+ defaultValue = "654321",
+ description = "mode가 fixed일 때 사용할 고정 인증번호 (6자리 숫자)")
+ @Pattern(regexp = "^\\d{6}$", message = "kjb.sms_auth.fixed_value는 6자리 숫자여야 합니다.")
+ private String smsAuthFixedValue = "654321";
+
+
+ @KjbPropertyValue(
+ key = "kjb.api.allow_ip_list",
+ description = "API 접근 허용 IP 목록 (콤마(,)로 구분된 IP들, 예: 192.168.0.1, 192.168.1.*,127.*.*.*",
+ defaultValue = "10.15.106.*, 10.14.8.*, 10.15.8.*, 192.168.240.*, 127.0.0.1, 192.168.132.*, 192.168.131.*"
+ )
+ private String apiAllowIpList = "10.15.106.*, 10.14.8.*, 10.15.8.*, 192.168.240.*, 127.0.0.1, 192.168.132.*, 192.168.131.*";
+
+
+ @KjbPropertyValue(
+ key = "kjb.api_gw_metric.page_size",
+ description = "API GW Metric 조회시 페이지 크기 설정 (Integer, 0 = 무한, 기본값 10000)",
+ defaultValue = "10000"
+ )
+ private Integer apiGwMetricPageSize = 10000;
+
+
+ public String toJsonString(boolean isPretty) {
+ return isPretty
+ ? new GsonBuilder().setPrettyPrinting().create().toJson(this)
+ : new Gson().toJson(this);
+ }
+
+
+ // 기본 프로퍼티 목록을 콘솔로 출력 (가이드용)
+ public static void main(String[] args) {
+ System.out.println("| Key | Default Value | Description |");
+ System.out.println("|-----|---------------|-------------|");
+
+ for (java.lang.reflect.Field field : KjbProperty.class.getDeclaredFields()) {
+ KjbPropertyValue annotation = field.getAnnotation(KjbPropertyValue.class);
+ if (annotation != null) {
+ String key = annotation.key();
+ String defaultValue = annotation.defaultValue();
+ String description = annotation.description();
+ System.out.printf("| %s | %s | %s |%n", key, defaultValue, description);
+ }
+ }
+ }
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbPropertyHolder.java b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyHolder.java
new file mode 100644
index 0000000..16acbd5
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyHolder.java
@@ -0,0 +1,92 @@
+package com.eactive.ext.kjb.common;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.function.Function;
+
+/**
+ * KjbProperty 싱글톤 홀더.
+ *
+ * 모든 곳에서 {@code KjbPropertyHolder.get()}으로 현재 KjbProperty에 접근합니다.
+ * eapim-admin에서 초기화 시 {@code initialize()}를 호출하고,
+ * MonitoringContext.refresh() 시 {@code reload()}를 호출하여 값을 갱신합니다.
+ *
+ * 사용 예시:
+ *
+ * // 값 접근
+ * String url = KjbPropertyHolder.get().getUmsHostUrl();
+ *
+ * // 초기화 (eapim-admin에서)
+ * KjbPropertyInjector injector = new KjbPropertyInjector(service, "Monitoring");
+ * KjbPropertyHolder.initialize(injector::inject);
+ *
+ * // reload (MonitoringContext.refresh() 시)
+ * KjbPropertyHolder.reload();
+ *
+ * // 테스트용
+ * KjbPropertyHolder.setForTest(mockProperty);
+ *
+ */
+@Slf4j
+public class KjbPropertyHolder {
+
+ private static volatile KjbProperty instance = new KjbProperty();
+ private static volatile Function injector;
+
+ private KjbPropertyHolder() {
+ // 유틸리티 클래스
+ }
+
+ /**
+ * 현재 KjbProperty 인스턴스를 반환합니다.
+ */
+ public static KjbProperty get() {
+ return instance;
+ }
+
+ /**
+ * 초기화 함수를 등록하고 최초 로딩을 수행합니다.
+ * eapim-admin 부팅 시 MonitoringContextImpl.init()에서 호출됩니다.
+ *
+ * @param injectorFn KjbProperty를 DB에서 로딩하는 함수 (KjbPropertyInjector::inject)
+ */
+ public static void initialize(Function injectorFn) {
+ injector = injectorFn;
+ reload();
+ log.info("KjbPropertyHolder 초기화 완료");
+ }
+
+ /**
+ * DB에서 프로퍼티를 다시 로딩합니다.
+ * MonitoringContext.refresh() 호출 시 함께 호출됩니다.
+ */
+ public static void reload() {
+ if (injector != null) {
+ try {
+ instance = injector.apply(new KjbProperty());
+ log.info("KjbProperty reload 완료");
+ } catch (Exception e) {
+ log.error("KjbProperty reload 실패: {}", e.getMessage(), e);
+ }
+ } else {
+ log.debug("KjbPropertyHolder: injector가 설정되지 않음 (테스트 환경일 수 있음)");
+ }
+ }
+
+ /**
+ * 테스트용 - 직접 인스턴스를 설정합니다.
+ */
+ public static void setForTest(KjbProperty testProperty) {
+ instance = testProperty;
+ log.debug("KjbProperty 테스트 인스턴스 설정");
+ }
+
+ /**
+ * 테스트용 - 기본값으로 초기화합니다.
+ */
+ public static void resetForTest() {
+ instance = new KjbProperty();
+ injector = null;
+ log.debug("KjbPropertyHolder 테스트 초기화");
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java
index 0522f7d..6b75459 100644
--- a/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java
+++ b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java
@@ -10,4 +10,5 @@ import java.lang.annotation.Target;
public @interface KjbPropertyValue {
String key();
String defaultValue() default "";
+ String description() default "";
}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/KjbObpModule.java b/src/main/java/com/eactive/ext/kjb/obp/KjbObpModule.java
new file mode 100644
index 0000000..21e5aff
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/KjbObpModule.java
@@ -0,0 +1,85 @@
+package com.eactive.ext.kjb.obp;
+
+import com.eactive.ext.kjb.common.KjbObpException;
+import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+@Slf4j
+public class KjbObpModule {
+ private static final String FAKE_PARTNER_CODE = "999999-99";
+
+ private static volatile KjbObpModule instance;
+
+ private KjbObpModule() {
+ // 싱글톤
+ }
+
+ public static KjbObpModule getInstance() {
+ if (instance == null) {
+ synchronized (KjbObpModule.class) {
+ if (instance == null) {
+ instance = new KjbObpModule();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private static KjbProperty getProperty() {
+ return KjbPropertyHolder.get();
+ }
+
+ private ObpClient getClient() {
+ return ObpClient.getInstance();
+ }
+
+ /**
+ * 사업자등록번호로 partnerCode 조회
+ * @param bizNo 사업자등록번호 (숫자 외 문자 자동 제거)
+ * @return partnerCode
+ * @throws KjbObpException 조회 실패 시
+ */
+ public String queryPartnerCode(String bizNo) throws KjbObpException {
+ bizNo = ObpUtil.onlyDigit(bizNo);
+
+ if (StringUtils.isEmpty(bizNo)) {
+ throw new IllegalArgumentException("BizNo is empty.");
+ }
+
+ if (bizNo.length() != 10) {
+ throw new IllegalArgumentException("BizNo must be 10 digits. (input: " + bizNo + ")");
+ }
+
+ ObpClient client = getClient();
+
+ // 가상모드: URL 미설정 시 가상 파트너코드 반환
+ if (!client.isRealMode()) {
+ log.info("[OBP] 가상모드 - 가상 partnerCode 반환: {}", FAKE_PARTNER_CODE);
+ return FAKE_PARTNER_CODE;
+ }
+
+ String path = String.format(getProperty().getObpUrlOrganizationFmt(), bizNo);
+ log.debug("[OBP] queryPartnerCode - bizNo: {}, path: {}", bizNo, path);
+
+ ObpResponse response = client.get(path);
+
+ 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();
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/ObpClient.java b/src/main/java/com/eactive/ext/kjb/obp/ObpClient.java
new file mode 100644
index 0000000..bb6745a
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/ObpClient.java
@@ -0,0 +1,178 @@
+package com.eactive.ext.kjb.obp;
+
+import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
+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 volatile ObpClient instance;
+
+ private ObpClient() {
+ // 싱글톤
+ }
+
+ public static ObpClient getInstance() {
+ if (instance == null) {
+ synchronized (ObpClient.class) {
+ if (instance == null) {
+ instance = new ObpClient();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private static KjbProperty getProperty() {
+ return KjbPropertyHolder.get();
+ }
+
+ /**
+ * 실제 모드 여부 (URL이 설정되어 있는지 확인)
+ */
+ public boolean isRealMode() {
+ return StringUtils.isNotEmpty(getProperty().getObpUrl());
+ }
+
+ private RequestConfig createRequestConfig() {
+ KjbProperty prop = getProperty();
+ return RequestConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(prop.getObpConnectionTimeout()))
+ .setResponseTimeout(Timeout.ofSeconds(prop.getObpResponseTimeout()))
+ .build();
+ }
+
+ /**
+ * 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 additionalHeaders) {
+ String url = getProperty().getObpUrl() + path;
+ HttpGet httpGet = new HttpGet(url);
+ applyDefaultHeaders(httpGet);
+ applyAdditionalHeaders(httpGet, additionalHeaders);
+ httpGet.setConfig(createRequestConfig());
+
+ 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 additionalHeaders) {
+ String url = getProperty().getObpUrl() + path;
+ HttpPost httpPost = new HttpPost(url);
+ applyDefaultHeaders(httpPost);
+ applyAdditionalHeaders(httpPost, additionalHeaders);
+ httpPost.setConfig(createRequestConfig());
+
+ if (StringUtils.isNotEmpty(jsonBody)) {
+ httpPost.setEntity(EntityBuilder.create()
+ .setText(jsonBody)
+ .setContentType(ContentType.APPLICATION_JSON)
+ .build());
+ }
+
+ return execute(httpPost);
+ }
+
+ private void applyDefaultHeaders(HttpUriRequestBase request) {
+ KjbProperty prop = getProperty();
+ request.setHeader("Content-Type", "application/json; charset=utf-8");
+ request.setHeader(prop.getObpTrustSystemKey(), prop.getObpTrustSystemValue());
+ request.setHeader(prop.getObpPartnerCodeKey(), prop.getObpPartnerCodeValue());
+ }
+
+ private void applyAdditionalHeaders(HttpUriRequestBase request, Map 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 (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ log.debug("[OBP] 요청: {} {}", request.getMethod(), request.getRequestUri());
+
+ Future 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(getProperty().getObpTimeout(), TimeUnit.SECONDS);
+
+ if (log.isDebugEnabled()) {
+ log.debug("[OBP] 응답: status={}, body={}", response.getStatus(), response.getBody());
+ }
+
+ return response;
+
+ } catch (InterruptedException | ExecutionException | TimeoutException | java.io.IOException 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();
+ }
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/ObpOrganization.java b/src/main/java/com/eactive/ext/kjb/obp/ObpOrganization.java
new file mode 100644
index 0000000..13625e4
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/ObpOrganization.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/ObpRequest.java b/src/main/java/com/eactive/ext/kjb/obp/ObpRequest.java
new file mode 100644
index 0000000..287e4f3
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/ObpRequest.java
@@ -0,0 +1,4 @@
+package com.eactive.ext.kjb.obp;
+
+public class ObpRequest {
+}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/ObpResponse.java b/src/main/java/com/eactive/ext/kjb/obp/ObpResponse.java
new file mode 100644
index 0000000..ce32e47
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/ObpResponse.java
@@ -0,0 +1,13 @@
+package com.eactive.ext.kjb.obp;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class ObpResponse {
+ private String body;
+ private Map headers = new HashMap<>();
+ private int status;
+}
diff --git a/src/main/java/com/eactive/ext/kjb/obp/ObpUtil.java b/src/main/java/com/eactive/ext/kjb/obp/ObpUtil.java
new file mode 100644
index 0000000..e8cefde
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/obp/ObpUtil.java
@@ -0,0 +1,10 @@
+package com.eactive.ext.kjb.obp;
+
+import java.util.stream.Collectors;
+
+class ObpUtil {
+ static String onlyDigit(String str) {
+ return str == null ? "" :
+ str.chars().filter(Character::isDigit).mapToObj(c -> String.valueOf((char)c)).collect(Collectors.joining());
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/sso/ExtendedInfo.java b/src/main/java/com/eactive/ext/kjb/sso/ExtendedInfo.java
new file mode 100644
index 0000000..686012f
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/sso/ExtendedInfo.java
@@ -0,0 +1,38 @@
+package com.eactive.ext.kjb.sso;
+
+import lombok.Getter;
+
+public enum ExtendedInfo {
+ DATEOFBIRTH("생년월일 + 성별"),
+ CELLPHONENO("핸드폰"),
+ UPPERTCODE("모점점번코드"),
+ PARTCODE("소속부점코드"),
+ LEGACYDEPTCODE("인사부서코드"),
+ LEGACYDEPTNAME("인사부서명"),
+ LEGACYPOSITIONCODE("인사직위코드"),
+ LEGACYPOSITIONNAME("인사직위명"),
+ LEGACYFUNCTIONCODE("인사직책코드"),
+ LEGACYFUNCTIONNAME("인사직책명"),
+ LEGACYROLLCODE("인사직무코드"),
+ LEGACYROLLNAME("인사직무명"),
+ LEGACYOFCLEVELCODE("인사직급코드"),
+ LEGACYOFCLEVELNAME("인사직급명"),
+ PART_GBN("본부(1), 영업점(2)"),
+ STATUS("재직(1),휴가(2),퇴직(9)"),
+ CEF_APPOINTDATE("최종방령일"),
+ OTHERDEPTCODE("겸직/파견 부서코드"),
+ OTHERDEPTNAME("겸직/파견 부서명"),
+ OTHERPOSITIONCODE("겸직/파견 직위코드"),
+ OTHERFIUNCTIONCODE("겸직/파견 직책코드"),
+ OTHERROLLCODE("겸직/파견 직무코드"),
+ OTHEROFCLEVELCODE("겸직/파견 직급코드")
+ ;
+
+
+ @Getter
+ private final String description;
+
+ ExtendedInfo(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/sso/KjbSsoModule.java b/src/main/java/com/eactive/ext/kjb/sso/KjbSsoModule.java
new file mode 100644
index 0000000..687c45e
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/sso/KjbSsoModule.java
@@ -0,0 +1,350 @@
+package com.eactive.ext.kjb.sso;
+
+import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
+import com.initech.eam.api.*;
+import com.initech.eam.base.APIException;
+import com.initech.eam.base.EmptyResultException;
+import com.initech.eam.nls.CookieManager;
+import com.initech.eam.smartenforcer.SECode;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.Vector;
+
+@Slf4j
+public class KjbSsoModule {
+ private static volatile KjbSsoModule instance;
+
+ private final Vector PROVIDER_LIST = new Vector();
+ private final int COOKIE_SESSTION_TIME_OUT = 3000000;
+
+ // 인증 타입 (ID/PW 방식 : 1, 인증서 : 3)
+ private final String TOA = "1";
+ private final String SSO_DOMAIN = ".kjbank.com";
+ private final String COOKIE_PADDING = "_V42";
+
+ private static final int timeout = 15000;
+
+ private KjbSsoModule() {
+ CookieManager.setEncStatus(true);
+ SECode.setCookiePadding(COOKIE_PADDING);
+ }
+
+ public static KjbSsoModule getInstance() {
+ if (instance == null) {
+ synchronized (KjbSsoModule.class) {
+ if (instance == null) {
+ instance = new KjbSsoModule();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private static KjbProperty getProperty() {
+ return KjbPropertyHolder.get();
+ }
+
+ private String getServiceName() {
+ return getProperty().getSsoServiceName();
+ }
+
+ public String getAscpUrl() {
+ KjbProperty prop = getProperty();
+ return prop.getUrl() + prop.getSsoAscpUri();
+ }
+
+ private String getNlsUrl() {
+ return "http://" + getProperty().getSsoDomain();
+ }
+
+ private String getNlsPort() {
+ return String.valueOf(getProperty().getSsoNlsPort());
+ }
+
+ private String getNlsLoginUrl() {
+ return getNlsUrl() + ":" + getNlsPort() + "/nls3/clientLogin.jsp";
+ }
+
+ private String getNlsLogoutUrl() {
+ return getNlsUrl() + ":" + getNlsPort() + "/nls3/NCLogout.jsp";
+ }
+
+ private String getNlsErrorUrl() {
+ return getNlsUrl() + ":" + getNlsPort() + "/nls3/error.jsp";
+ }
+
+ private String getNdUrl1() {
+ return "http://" + getProperty().getSsoDomain() + ":5480";
+ }
+
+ public Vector getProviderList() {
+ Vector list = new Vector<>();
+ list.add(getProperty().getSsoDomain());
+ return list;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public NXContext getContext() {
+ NXContext context = null;
+ try {
+ List serverurlList = new ArrayList<>();
+ serverurlList.add(getNdUrl1());
+ context = new NXContext(serverurlList);
+ CookieManager.setEncStatus(true);
+ SECode.setCookiePadding(COOKIE_PADDING);
+ }
+ catch (Exception e)
+ {
+ log.warn("getContext", e);
+ }
+ return context;
+ }
+
+
+ /**
+ * 통합 SSO ID 조회
+ * @param request
+ * @return
+ */
+ public String getSsoId(HttpServletRequest request) {
+ String sso_id = null;
+ sso_id = CookieManager.getCookieValue(SECode.USER_ID, request);
+ return sso_id;
+ }
+
+ public String getSystemAccount(String sso_id) {
+ NXContext context = null;
+ NXUserAPI userAPI = null;
+ String retValue = null;
+
+ try {
+ context = getContext();
+ userAPI = new NXUserAPI(context);
+ String serviceName = getServiceName();
+ log.info("##############################################");
+ log.info("sso_id 2: "+sso_id);
+ log.info("SERVICE_NAME : "+ serviceName);
+ log.info("##############################################");
+ NXAccount account = userAPI.getUserAccount(sso_id, serviceName);
+ log.info(account.toString());
+ retValue = account.getAccountName();
+ log.info("retValue = " + retValue);
+ } catch (Exception e) {
+ log.error("KjbSsoModule Exception", e);
+ }
+ return retValue;
+ }
+
+ /**
+ * 통합 SSO 로그인페이지 이동
+ * @param response
+ * @param uurl
+ * @throws Exception
+ */
+ public void goLoginPage(HttpServletResponse response, String uurl)
+ throws Exception {
+ CookieManager.addCookie(SECode.USER_URL, uurl, SSO_DOMAIN, response);
+ CookieManager.addCookie(SECode.R_TOA, TOA, SSO_DOMAIN, response);
+ response.sendRedirect(getNlsLoginUrl() + "?UURL=" + uurl);
+ }
+
+ /**
+ * 통합 SSO 로그인페이지 이동
+ * @param response
+ * @throws Exception
+ */
+ public void goLoginPage(HttpServletResponse response) throws Exception {
+ CookieManager.addCookie(SECode.USER_URL, getAscpUrl(), SSO_DOMAIN, response);
+ CookieManager.addCookie(SECode.R_TOA, TOA, SSO_DOMAIN, response);
+ response.sendRedirect(getNlsLoginUrl());
+ }
+
+ /**
+ * 통합인증 세션을 체크 하기 위하여 사용되는 API
+ * @param request
+ * @param response
+ * @return
+ */
+ public String getEamSessionCheck(HttpServletRequest request, HttpServletResponse response) {
+ String retCode = "";
+ try {
+ retCode = CookieManager.verifyNexessCookie(request, response, 10, COOKIE_SESSTION_TIME_OUT, getProviderList());
+ } catch(Exception npe) {
+ log.warn("getEamSessionCheck", npe);
+ }
+ return retCode;
+ }
+
+ /**
+ * ND API를 사용해서 쿠키검증하는것(현재 표준에서는 사용안함, 근데 해도 되기는 함)
+ * @param request
+ * @param response
+ * @return
+ */
+ public String getEamSessionCheck2(HttpServletRequest request, HttpServletResponse response) {
+ String retCode = "";
+ try {
+ NXContext ctx = getContext();
+ NXNLSAPI nxNLSAPI = new NXNLSAPI(ctx);
+ retCode = nxNLSAPI.readNexessCookie(request, response, 0, 0);
+ } catch(Exception npe) {
+ log.error("KjbSsoModule-getEamSessionCheck2 Exception", npe);
+ }
+ return retCode;
+ }
+
+ /**
+ * SSO 에러페이지 URL
+ * @param response
+ * @param error_code
+ * @throws Exception
+ */
+ public void goErrorPage(HttpServletResponse response, int error_code) throws Exception {
+ CookieManager.removeNexessCookie(SSO_DOMAIN, response);
+ CookieManager.addCookie(SECode.USER_URL, getAscpUrl(), SSO_DOMAIN, response);
+ response.sendRedirect(getNlsErrorUrl() + "?errorCode=" + error_code);
+ }
+
+ /**
+ * 사용자 기본정보 가져오기.
+ * @param userid
+ * @return Properties
+ */
+ public Properties getUserInfos(String userid) throws Exception {
+ NXContext context = null;
+ context = getContext();
+
+ NXUserAPI userAPI = new NXUserAPI(context);
+ Properties propt = null;
+
+ if (userid==null || userid.length() <= 0)
+ return propt;
+
+ try {
+ NXUserInfo userInfo = userAPI.getUserInfo(userid);
+ propt = new Properties();
+ propt.setProperty("USERID", userInfo.getUserId());
+ propt.setProperty("EMAIL", userInfo.getEmail());
+ propt.setProperty("ENABLE", String.valueOf(userInfo.getEnable()));
+ propt.setProperty("STARTVALID", userInfo.getStartValid());
+ propt.setProperty("ENDVALID", userInfo.getEndValid());
+ propt.setProperty("NAME", userInfo.getName());
+ propt.setProperty("ENCPASSWD", userInfo.getEncpasswd());
+ propt.setProperty("LASTPASSWDCHANGE", userInfo.getLastpasswdchange());
+ propt.setProperty("LastLoginIP", userInfo.getLastLoginIp());
+ propt.setProperty("LastLoginTime", userInfo.getLastLoginTime());
+ propt.setProperty("LastLoginAuthLevel", userInfo.getLastLoginAuthLevel());
+
+ } catch (Exception e) {
+ log.error("KjbSsoModule-getUserInfos Exception", e);
+ throw e;
+ }
+
+ return propt;
+ }
+
+ /**
+ * 사용자의 기본정보 중에서 입력된 키 값만 가지고 온다.
+ * @param userid
+ * @param key
+ * @return
+ * @throws Exception
+ */
+ public String getUserInfo(String userid, String key) throws Exception {
+ if(userid==null|| userid.isEmpty()) return null;
+
+ String userInfo = "";
+
+ try {
+ Properties propt = getUserInfos(userid);
+ userInfo = propt.getProperty(key);
+ } catch (Exception e) {
+ log.error("KjbSsoModule-getUserInfos Exception", e);
+ throw e;
+ }
+ return userInfo;
+ }
+
+ /**
+ * 사용자 확장정보 조회
+ * return : Properties
+ * 사용자가 존재하지 않거나 확장필드가 없다면 return null
+ */
+ @SuppressWarnings("rawtypes")
+ public Properties getUserExFields(String userid) throws Exception {
+ NXContext context = getContext();
+ NXUserAPI userAPI = new NXUserAPI(context);
+ Properties prop = null;
+ NXExternalFieldSet nxefs = null;
+
+ try {
+ nxefs = userAPI.getUserExternalFields(userid);
+ prop = new Properties();
+
+ if (nxefs != null) {
+ Iterator> iter = nxefs.iterator();
+ while(iter.hasNext()) {
+ NXExternalField nxef = (NXExternalField) iter.next();
+ if(nxef.getName().equals("PASSINIT")){
+ String sPassintVal = (String) nxef.getValue();
+ prop.setProperty(nxef.getName(), sPassintVal.equals("") ? "0" : sPassintVal);
+ }
+ else
+ prop.setProperty(nxef.getName(), (String) nxef.getValue());
+ //System.out.println("확장 필드 " + nxef.getName() + " : " + nxef.getValue());
+ }
+ } else {
+ // prop is null
+ log.debug("KjbSsoModule-getUserExFields property is null");
+ }
+ } catch (EmptyResultException e) {
+ // 사용자가 존재하지 않거나 확장필드가 없음
+ log.warn("사용자가 존재하지 않거나 확장필드가 없음", e);
+ } catch (APIException e) {
+ log.warn("SSO-APIException", e);
+ throw e;
+ //e.printStackTrace();
+ }
+ //System.out.println("getUserExFields:[" + prop + "]");
+ return prop;
+ }
+
+ /**
+ * 사용자의 특정 확장 필드 정보 조회
+ * return : String
+ * 사용자가 존재하지 않거나 확장필드가 없다면 return null
+ */
+ public String getUserExField(String userid, String exName)
+ throws Exception {
+ NXContext context = getContext();
+ NXUserAPI userAPI = new NXUserAPI(context);
+ NXExternalField nxef = null;
+ String returnValue = null;
+
+ try {
+ log.debug("KjbSsoModule-getUserExField userid : {}", userid);
+ nxef = userAPI.getUserExternalField(userid, exName);
+ log.debug("KjbSsoModule-getUserExField nxef === {}", nxef.getValue());
+ returnValue = (String) nxef.getValue();
+ } catch (EmptyResultException e) {
+ // 사용자가 존재하지 않거나 확장필드가 없음
+ log.warn("사용자가 존재하지 않거나 확장필드가 없음", e);
+ } catch (Exception e) {
+ log.error("KjbSsoModule-getUserExField Exception", e);
+ throw e;
+ }
+
+ return returnValue;
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java b/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java
index 7a38cde..07a7b09 100644
--- a/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java
+++ b/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java
@@ -8,9 +8,10 @@ import com.eactive.ext.kjb.common.KjbProperty;
import com.eactive.ext.kjb.eai.EaiBatchInterface;
import com.eactive.ext.kjb.eai.EaiBatchMessage;
import com.eactive.ext.kjb.eai.KjbEaiBatchModule;
-import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
+//import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -50,7 +51,8 @@ public class EmailJob {
this.messageRequest = messageRequest;
this.prop = prop;
- String filename = messageRequest.getServiceId() + "_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(now) + ".dat";
+// String filename = messageRequest.getServiceId() + "_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(now) + ".dat";
+ String filename = messageRequest.getServiceId() + "_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(now) + ".dat";
this.eaiFilePath = Paths.get(prop.getUmsEaiBatchSendDir(), filename);
this.backupFilePath = Paths.get(prop.getUmsEaiBatchBackupDir(), filename);
this.failedFilePath = Paths.get(prop.getUmsEaiBatchBackupDir(), filename + ".failed");
@@ -228,30 +230,39 @@ public class EmailJob {
public String generateJSON(boolean isPretty) {
validateMessageRequest(messageRequest, true, true);
- KjbSafedbWrapper safedb = KjbSafedbWrapper.getInstance();
+// KjbSafedbWrapper safedb = KjbSafedbWrapper.getInstance();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("MAKE_DATE", DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now()));
jsonObject.addProperty("MAKE_SEQNO", DateTimeFormatter.ofPattern("HHmmssSSS").format(LocalDateTime.now()));
jsonObject.addProperty("CAMP_ID", messageRequest.getServiceId()); // 전자금융 고객이메일 발송 업무ID (SERVICE_ID)
jsonObject.addProperty("ID", messageRequest.getUmsUid()); // 고객 ID
- jsonObject.addProperty("EMAIL", safedb.encryptNotRnnoString(messageRequest.getEmail()));
+// jsonObject.addProperty("EMAIL", safedb.encryptNotRnnoString(messageRequest.getEmail()));
+ jsonObject.addProperty("EMAIL", messageRequest.getEmail());
jsonObject.addProperty("NAME", messageRequest.getUsername());
jsonObject.addProperty("ENCKEY", ""); // 보안메일용으로 기입 불필요
Gson gson = new Gson();
- log.debug("Template string to json: {}", messageRequest.getMessage());
- JsonObject contentJSon = gson.fromJson(messageRequest.getMessage(), JsonObject.class);
- contentJSon.entrySet().forEach(entry -> {
- jsonObject.add(entry.getKey(), entry.getValue());
- });
+ String message = messageRequest.getMessage();
+ JsonArray jsonArray = new JsonArray();
+ log.debug("Template string to json: {}", message);
+ if (StringUtils.isNotEmpty(message)) {
+ JsonObject contentJSon = gson.fromJson(message, JsonObject.class);
+ contentJSon.entrySet().forEach(entry -> {
+ jsonObject.add(entry.getKey(), entry.getValue());
+ });
+ }
+
+ jsonArray.add(jsonObject);
// jsonObject.addProperty("SUBJECT", messageRequest.getSubject());
// jsonObject.addProperty("MESSAGE", messageRequest.getMessage());
String jsonString;
if (isPretty) {
- jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(jsonObject);
+ // pretty print 제외
+// jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(jsonArray);
+ jsonString = new GsonBuilder().create().toJson(jsonArray);
} else {
jsonString = jsonObject.toString();
}
diff --git a/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java b/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java
index 159f554..ce83a77 100644
--- a/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java
+++ b/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java
@@ -5,6 +5,7 @@ import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.kjb.common.KjbEaiBatchException;
import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
import com.eactive.ext.kjb.utils.ValidationUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -18,24 +19,35 @@ import java.util.function.BiConsumer;
public class KjbEmailSendModule {
public final static int[] ALLOW_CHANNELS = { MessageCode.Channels.EMAIL.getValue() };
- private static KjbEmailSendModule instance = null;
- private static KjbProperty prop;
+ private static volatile KjbEmailSendModule instance;
+ private MessageRequestRepository repo;
- private final MessageRequestRepository repo;
-
-
- public static synchronized KjbEmailSendModule getInstance(KjbProperty property,
- MessageRequestRepository messageRequestRepository) {
-
- ValidationUtil.validateOrThrow(property);
+ private KjbEmailSendModule() {
+ // 싱글톤
+ }
+ public static KjbEmailSendModule getInstance() {
if (instance == null) {
- instance = new KjbEmailSendModule(property, messageRequestRepository);
+ synchronized (KjbEmailSendModule.class) {
+ if (instance == null) {
+ instance = new KjbEmailSendModule();
+ }
+ }
}
-
return instance;
}
+ /**
+ * MessageRequestRepository 설정 (eapim-admin 초기화 시 호출)
+ */
+ public void setRepository(MessageRequestRepository repository) {
+ this.repo = repository;
+ }
+
+ private static KjbProperty getProperty() {
+ return KjbPropertyHolder.get();
+ }
+
public static boolean isAllowChannel(MessageRequest message) {
for (int channel : ALLOW_CHANNELS) {
if ((message.getMessageCode().getChannels() & channel) == channel) {
@@ -47,6 +59,8 @@ public class KjbEmailSendModule {
public static void environmentCheck() {
+ KjbProperty prop = getProperty();
+
if (StringUtils.isEmpty(prop.getEaiBatchUrl())) {
log.warn("EAI_BATCH_URL 가 설정되지 않음. 실제 EAI 배치 연동 없이 동작함");
}
@@ -101,24 +115,13 @@ public class KjbEmailSendModule {
log.info("KjbEmailSendModule - Environment check passed.");
}
-
-
- private KjbEmailSendModule(KjbProperty property,
- MessageRequestRepository repo) {
- this.repo = repo;
-
- if (property != null && prop == null) {
- prop = property;
- }
-
- environmentCheck();
- }
-
public void sendEmail(MessageRequest messageRequest) throws KjbEaiBatchException {
String serviceId = "UNKNOWN";
- serviceId = messageRequest.getMessageCode().getServiceId();
- messageRequest.setServiceId(serviceId);
+ if (StringUtils.isEmpty(messageRequest.getServiceId())) {
+ serviceId = messageRequest.getMessageCode().getServiceId();
+ messageRequest.setServiceId(serviceId);
+ }
if (StringUtils.isEmpty(serviceId)) {
throw new IllegalArgumentException("MessageRequest MessageCode serviceId is required");
@@ -133,7 +136,7 @@ public class KjbEmailSendModule {
repo.save(messageRequest);
}
- EmailJob job = EmailJob.create(prop, messageRequest, repo);
+ EmailJob job = EmailJob.create(getProperty(), messageRequest, repo);
job.sendEmail();
log.debug("JSON : {}", job.generateJSON(true));
diff --git a/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java b/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java
index c849023..883675f 100644
--- a/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java
+++ b/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java
@@ -3,13 +3,13 @@ package com.eactive.ext.kjb.ums;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
import com.eactive.ext.kjb.common.KjbUmsException;
-import com.eactive.ext.kjb.ums.gson.IUmsGsonObject;
-import com.eactive.ext.kjb.ums.gson.UmsRequestPayload;
-import com.eactive.ext.kjb.ums.gson.VerifyPhoneUmsTemplate;
+import com.eactive.ext.kjb.ums.gson.*;
import com.eactive.ext.kjb.utils.JsonUtil;
import com.eactive.ext.kjb.utils.ValidationUtil;
import com.google.gson.Gson;
+import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -39,34 +39,42 @@ public class KjbUmsService {
private final static String CONTENT_TYPE = "application/json; charset=utf-8";
private final static String AUTHORIZATION_FMT = "Bearer %s";
- private final KjbProperty property;
- private final boolean isRealMode;
- private final CloseableHttpClient httpClient;
- private final RequestConfig requestConfig;
+ private static volatile KjbUmsService instance;
+ private KjbUmsService() {
+ // 싱글톤
+ }
- public KjbUmsService(KjbProperty property) {
- if (property == null) {
- log.error("[KJB-UMS] 파라미터 설정 되지 않음");
- throw new IllegalArgumentException("property is null");
+ public static KjbUmsService getInstance() {
+ if (instance == null) {
+ synchronized (KjbUmsService.class) {
+ if (instance == null) {
+ instance = new KjbUmsService();
+ }
+ }
}
+ return instance;
+ }
- isRealMode = property.getUmsHostUrl() != null && StringUtils.isNotEmpty(property.getUmsHostUrl().trim());
+ private KjbProperty getProperty() {
+ return KjbPropertyHolder.get();
+ }
- if (isRealMode) {
- ValidationUtil.validateOrThrow(property);
+ private boolean isRealMode() {
+ String url = getProperty().getUmsHostUrl();
+ return url != null && StringUtils.isNotEmpty(url.trim());
+ }
- httpClient = HttpClients.createDefault();
- requestConfig = RequestConfig.custom()
- .setConnectTimeout(Timeout.ofSeconds(property.getEaiBatchConnectionTimeout()))
- .setResponseTimeout(Timeout.ofSeconds(property.getEaiBatchResponseTimeout()))
- .build();
- } else {
- httpClient = HttpClients.createDefault();
- requestConfig = null;
- }
+ private CloseableHttpClient createHttpClient() {
+ return HttpClients.createDefault();
+ }
- this.property = property;
+ private RequestConfig createRequestConfig() {
+ KjbProperty prop = getProperty();
+ return RequestConfig.custom()
+ .setConnectTimeout(Timeout.ofSeconds(prop.getUmsConnectionTimeout()))
+ .setResponseTimeout(Timeout.ofSeconds(prop.getUmsResponseTimeout()))
+ .build();
}
@@ -90,13 +98,10 @@ public class KjbUmsService {
throw new IllegalArgumentException("지원하지 않는 발송 채널 - " + messageRequest.getMessageCode().toString());
}
- IUmsGsonObject content = null;
- content = VerifyPhoneUmsTemplate.builder().authNumber(messageRequest.getMessage()).build();
-
String umsMessageId = this.sendKakaoAlimTalk(messageRequest.getId(),
UmsBizWorkCode.VERIFY_PHONE,
messageRequest.getPhone(),
- content
+ GsonUtil.toJsonObject(messageRequest.getMessage())
);
log.info("MessageRequest sent - {}", umsMessageId);
@@ -104,7 +109,13 @@ public class KjbUmsService {
return true;
}
- public String sendKakaoAlimTalk(String guid, UmsBizWorkCode bizWorkCode, String cpno, IUmsGsonObject content) throws KjbUmsException {
+ public String sendObpMonitoring(String cpno, String message) throws KjbUmsException {
+ log.info("OBP 모니터링 용 메세지 발송 - message: {}", message);
+
+ return this.sendKakaoAlimTalk(null, UmsBizWorkCode.MONITORING, cpno, GsonUtil.toJsonObject(ObpMonitoringUmsTemplate.builder().message(message).build()));
+ }
+
+ public String sendKakaoAlimTalk(String guid, UmsBizWorkCode bizWorkCode, String cpno, JsonObject content) throws KjbUmsException {
if (content == null) {
throw new IllegalArgumentException("content is null");
}
@@ -137,13 +148,14 @@ public class KjbUmsService {
}
private UmsCallResult call(String url, UmsRequestPayload requestPayload) throws KjbUmsException {
+ KjbProperty prop = getProperty();
ExecutorService executor = Executors.newSingleThreadExecutor();
Gson gson = new Gson();
String json = gson.toJson(requestPayload);
- HttpPost httpPost = new HttpPost(property.getUmsHostUrl() + url);
+ HttpPost httpPost = new HttpPost(prop.getUmsHostUrl() + url);
httpPost.setHeader("Content-Type", CONTENT_TYPE);
- httpPost.setHeader("Authorization", String.format(AUTHORIZATION_FMT, property.getUmsApiKey()));
+ httpPost.setHeader("Authorization", String.format(AUTHORIZATION_FMT, prop.getUmsApiKey()));
httpPost.setEntity(EntityBuilder.create().setText(json).setContentType(ContentType.APPLICATION_JSON).build());
if (log.isDebugEnabled()) {
@@ -156,52 +168,54 @@ public class KjbUmsService {
}
}
- httpPost.setConfig(requestConfig);
+ httpPost.setConfig(createRequestConfig());
- if (isRealMode) {
- Future future = executor.submit(() -> {
- return httpClient.execute(httpPost, res -> {
- try {
- UmsCallResult result;
- String body = EntityUtils.toString(res.getEntity());
- if (log.isDebugEnabled()) {
- log.debug("Response - {}", body);
- }
- result = gson.fromJson(body, UmsCallResult.class);
- result.setUmsMessageId(requestPayload.getMessageIdentityNo());
- result.setSuccess(true);
- return result;
- } catch (JsonSyntaxException e) {
- log.error("UMS Call error(JSON Syntax)", e);
- return UmsCallResult.builder()
- .umsMessageId(requestPayload.getMessageIdentityNo())
- .isSuccess(false)
- .throwable(e)
- .build();
- } catch (Exception e) {
- log.error("UMS Call error", e);
- return UmsCallResult.builder()
- .umsMessageId(requestPayload.getMessageIdentityNo())
- .isSuccess(false)
- .throwable(e)
- .build();
- } finally {
- log.debug("Response status - {}", res.getCode());
+ if (isRealMode()) {
+ try (CloseableHttpClient httpClient = createHttpClient()) {
+ Future future = executor.submit(() -> {
+ return httpClient.execute(httpPost, res -> {
+ try {
+ UmsCallResult result;
+ String body = EntityUtils.toString(res.getEntity());
+ if (log.isDebugEnabled()) {
+ log.debug("Response - {}", body);
+ }
+ result = gson.fromJson(body, UmsCallResult.class);
+ result.setUmsMessageId(requestPayload.getMessageIdentityNo());
+ result.setSuccess(true);
+ return result;
+ } catch (JsonSyntaxException e) {
+ log.error("UMS Call error(JSON Syntax)", e);
+ return UmsCallResult.builder()
+ .umsMessageId(requestPayload.getMessageIdentityNo())
+ .isSuccess(false)
+ .throwable(e)
+ .build();
+ } catch (Exception e) {
+ log.error("UMS Call error", e);
+ return UmsCallResult.builder()
+ .umsMessageId(requestPayload.getMessageIdentityNo())
+ .isSuccess(false)
+ .throwable(e)
+ .build();
+ } finally {
+ log.debug("Response status - {}", res.getCode());
- if (res.getCode() != 200) {
- Arrays.asList(res.getHeaders()).forEach(o -> {
- log.warn("[Header] {}: {}", o.getName(), o.getValue());
- });
+ if (res.getCode() != 200) {
+ Arrays.asList(res.getHeaders()).forEach(o -> {
+ log.warn("[Header] {}: {}", o.getName(), o.getValue());
+ });
+ }
}
- }
+ });
});
- });
- try {
- return future.get(property.getUmsTimeout(), TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- log.error("EAI Batch call exception", e);
- throw new KjbUmsException("EAI Ums call failed", e);
+ return future.get(prop.getUmsTimeout(), TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException | IOException e) {
+ log.error("UMS call exception", e);
+ throw new KjbUmsException("UMS call failed", e);
+ } finally {
+ executor.shutdown();
}
} else {
log.warn("UMS URL 미지정으로 실제 호출을 진행하지 않음.");
diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/GsonUtil.java b/src/main/java/com/eactive/ext/kjb/ums/gson/GsonUtil.java
new file mode 100644
index 0000000..3731e47
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/ums/gson/GsonUtil.java
@@ -0,0 +1,131 @@
+package com.eactive.ext.kjb.ums.gson;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+
+public class GsonUtil {
+ /** 한 번만 생성해 두고 재사용 */
+ private static final Gson GSON = new GsonBuilder()
+ .setPrettyPrinting() // 필요 시 포맷 옵션
+ .create();
+
+ private GsonUtil() {
+ /* 인스턴스 생성 금지 */
+ }
+
+ /* ------------------------------------------------------------------ *
+ * 1. 객체 ↔ JSON 문자열
+ * ------------------------------------------------------------------ */
+ /** 객체 → JSON 문자열 */
+ public static String toJson(Object src) {
+ return GSON.toJson(src);
+ }
+
+ /** JSON 문자열 → 객체 */
+ public static T fromJson(String json, Class clazz) {
+ return GSON.fromJson(json, clazz);
+ }
+
+ /* ------------------------------------------------------------------ *
+ * 2. 객체 ↔ JsonElement
+ * ------------------------------------------------------------------ */
+ /** 객체 → JsonElement (Gson 내부 표현) */
+ public static JsonElement toJsonElement(Object src) {
+ return GSON.toJsonTree(src);
+ }
+
+ /** JsonElement → 객체 */
+ public static T fromJsonElement(JsonElement json, Class clazz) {
+ return GSON.fromJson(json, clazz);
+ }
+
+ /* ------------------------------------------------------------------ *
+ * 3. JSON 문자열 ↔ JsonObject
+ * ------------------------------------------------------------------ */
+ /** JSON 문자열 → JsonObject (정상적인 객체만 허용) */
+ public static JsonObject fromJsonObject(String json) {
+ JsonElement elem = GSON.fromJson(json, JsonElement.class);
+ if (!elem.isJsonObject()) {
+ throw new IllegalArgumentException("JSON 문자열이 JsonObject 가 아닙니다: " + json);
+ }
+ return elem.getAsJsonObject();
+ }
+
+ /** JsonObject → JSON 문자열 (정렬 옵션 없이) */
+ public static String toJsonString(JsonObject jsonObj) {
+ return GSON.toJson(jsonObj);
+ }
+
+ /* ------------------------------------------------------------------ *
+ * 4. JSON 문자열 ↔ List
+ * ------------------------------------------------------------------ */
+ /** JSON 문자열 → List<T> */
+ public static List fromJsonList(String json, Class clazz) {
+ // ParameterizedType 으로 List 를 명시
+ Type listType = new ParameterizedType() {
+ @Override public Type[] getActualTypeArguments() { return new Type[]{clazz}; }
+ @Override public Type getRawType() { return List.class; }
+ @Override public Type getOwnerType() { return null; }
+ };
+ return GSON.fromJson(json, listType);
+ }
+
+ /* ------------------------------------------------------------------ *
+ * 5. 기타 (예시: JsonArray ↔ List)
+ * ------------------------------------------------------------------ */
+ /** JsonArray → List<T> */
+ public static List fromJsonArray(JsonArray array, Class clazz) {
+ return GSON.fromJson(array, new TypeToken>(){}.getType());
+ }
+
+ /** List<T> → JsonArray */
+ public static JsonArray toJsonArray(List list) {
+ return GSON.toJsonTree(list).getAsJsonArray();
+ }
+
+ /**
+ * JSON 문자열을 JsonObject 로 변환
+ *
+ * @param json 유효한 JSON 객체 문자열 (예: {"name":"John","age":30})
+ * @return JsonObject 인스턴스
+ * @throws IllegalArgumentException JSON 문자열이 객체가 아닐 경우
+ */
+ public static JsonObject toJsonObject(String json) {
+ // 먼저 JsonElement 로 파싱
+ JsonElement element = GSON.fromJson(json, JsonElement.class);
+
+ // 객체인지 검사
+ if (!element.isJsonObject()) {
+ throw new IllegalArgumentException(
+ "Provided JSON string is not a JsonObject: " + json);
+ }
+
+ // JsonObject 로 캐스팅 후 반환
+ return element.getAsJsonObject();
+ }
+
+ /**
+ * 일반 Java 객체 → JsonObject
+ *
+ * @param src 변환할 객체 (Map, POJO 등)
+ * @return JsonObject 인스턴스
+ * @throws IllegalArgumentException 객체가 JsonObject 로 변환될 수 없을 때
+ */
+ public static JsonObject toJsonObject(Object src) {
+ // 객체를 JsonElement 로 변환
+ JsonElement element = GSON.toJsonTree(src);
+
+ // 객체인지 검사 (Map, POJO 등은 JsonObject 가 됨)
+ if (!element.isJsonObject()) {
+ throw new IllegalArgumentException(
+ "Provided object cannot be represented as a JsonObject: "
+ + src.getClass().getName());
+ }
+
+ return element.getAsJsonObject();
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java b/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java
index b384155..57d0f9f 100644
--- a/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java
+++ b/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java
@@ -1,8 +1,12 @@
package com.eactive.ext.kjb.ums.gson;
import com.google.gson.annotations.SerializedName;
+import lombok.Builder;
+import lombok.Data;
+@Data
+@Builder
public class ObpMonitoringUmsTemplate implements IUmsGsonObject {
- @SerializedName("AUTH_NO")
- private String authNumber;
+ @SerializedName("MOTR_EVAL_CNTN")
+ private String message;
}
diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java b/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java
index 9dade0e..0bc99b1 100644
--- a/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java
+++ b/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java
@@ -1,5 +1,7 @@
package com.eactive.ext.kjb.ums.gson;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import lombok.Builder;
import lombok.Data;
@@ -34,7 +36,7 @@ public class UmsRequestPayload {
// MSG_BZWK_PARAM_CTNT object 메시지업무파라미터내용 4000 메시지업무코드의 템플릿에서 사용하는 변수와 변수값
@NotNull
@SerializedName("MSG_BZWK_PARAM_CTNT")
- private IUmsGsonObject content;
+ private JsonObject content;
// 발송예약일시 / 14 / 실 발송 일자(yyyyMMddHHmmss)
// SND_RESR_DTTM string 발송예약일시 14 20240418111500 O 실 발송 일자(현재시간)
diff --git a/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java b/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java
index 39f836a..4fb4359 100644
--- a/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java
+++ b/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java
@@ -11,11 +11,16 @@ public class KjbPropertyUtil {
Class> clazz = KjbProperty.class;
StringBuilder sb = new StringBuilder();
+ StringBuilder sbDelete = new StringBuilder();
for (Field field : clazz.getDeclaredFields()) {
KjbPropertyValue anno = field.getAnnotation(KjbPropertyValue.class);
if (anno != null) {
+ sbDelete.append("DELETE FROM EMSADM.TSEAIRM24 WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = '");
+ sbDelete.append(anno.key());
+ sbDelete.append("';\n");
+
sb.append("insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', ");
String key = anno.key();
@@ -27,6 +32,9 @@ public class KjbPropertyUtil {
}
System.out.println(sb);
+ System.out.println();
+ System.out.println();
+ System.out.println(sbDelete);
}
private static String addQuote(String str) {
diff --git a/src/test/java/com/eactive/ext/kjb/ums/test/KjbPropertyTest.java b/src/test/java/com/eactive/ext/kjb/ums/test/KjbPropertyTest.java
new file mode 100644
index 0000000..9bf6d78
--- /dev/null
+++ b/src/test/java/com/eactive/ext/kjb/ums/test/KjbPropertyTest.java
@@ -0,0 +1,28 @@
+package com.eactive.ext.kjb.ums.test;
+
+import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.utils.ValidationUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+@Slf4j
+public class KjbPropertyTest {
+
+ @Test
+ void testKjbProperty() {
+ Assumptions.assumeTrue(
+ "true".equalsIgnoreCase(System.getenv("KJB_EAPIM_TEST_ENABLED")),
+ "OS 환경변수 내 KJB_EAPIM_TEST_ENABLED=TRUE 설정되어 있어야 동작함");
+
+
+ log.info("KjbProperty 테스트 시작");
+
+
+ log.info("기본값 Validation 테스트");
+ KjbProperty prop = new KjbProperty();
+ ValidationUtil.validateOrThrow(prop);
+
+ log.info("KjbProperty 테스트 완료");
+ }
+}
diff --git a/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java b/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java
index 5015ef9..d0cfe69 100644
--- a/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java
+++ b/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.ext.kjb.common.KjbEaiBatchException;
import com.eactive.ext.kjb.common.KjbProperty;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
import com.eactive.ext.kjb.common.KjbUmsException;
import com.eactive.ext.kjb.ums.KjbEmailSendModule;
import com.eactive.ext.kjb.ums.KjbUmsService;
@@ -14,6 +15,8 @@ import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
@@ -23,14 +26,16 @@ public class UmsTest {
@BeforeAll
public static void init() {
- prop = new KjbProperty();
+ prop = new KjbProperty();
prop.setEaiBatchUrl("http://172.31.32.111:10230/BATAppWeb/EaiBatCall");
- prop.setUmsEaiBatchInterfaceId("UAGFF00001UIE");
+ prop.setUmsEaiBatchInterfaceId("UIEFF00001UAG");
prop.setUmsHostUrl("http://172.31.35.144:9021");
prop.setUmsApiKey("7cca6d58-e4f7-474d-9edd-525806bc99ff");
if (OsUtil.isWindows()) {
+ prop.setEaiBatchUrl("");
+ prop.setUmsHostUrl("");
prop.setUmsEaiBatchSendDir("c:\\kjb-logs\\sendmail\\snd");
prop.setUmsEaiBatchBackupDir("c:\\kjb-logs\\sendmail\\bak");
prop.setUmsEaiBatchErrorDir("c:\\kjb-logs\\sendmail\\error");
@@ -41,6 +46,8 @@ public class UmsTest {
prop.setUmsEaiBatchBackupDir("/Data/eapim/portal/sendmail/bak");
prop.setUmsEaiBatchErrorDir("/Data/eapim/portal/sendmail/error");
}
+
+ KjbPropertyHolder.setForTest(prop);
}
@Test
@@ -58,7 +65,7 @@ public class UmsTest {
messageRequest.setSubject("제목 - test");
messageRequest.setMessage("{\"ATHN_NO\"; \"369369\"}");
- KjbUmsService umsService = new KjbUmsService(prop);
+ KjbUmsService umsService = KjbUmsService.getInstance();
try {
boolean result = umsService.send(messageRequest);
@@ -71,6 +78,9 @@ public class UmsTest {
log.info("done");
}
+ /**
+ * :eapim-admin-kjb:test --tests "com.eactive.ext.kjb.ums.test.UmsTest.testEmail"
+ */
@Test
public void testEmail() {
Assumptions.assumeTrue(
@@ -82,25 +92,71 @@ public class UmsTest {
log.info("{}: {}", key, System.getProperty((String) key));
});
- KjbEmailSendModule service = KjbEmailSendModule.getInstance(prop, null);
- MessageRequest messageRequest = new MessageRequest();
- messageRequest.setId(UUID.randomUUID().toString());
- messageRequest.setMessageCode(MessageCode.USER_VERIFICATION_EMAIL);
- messageRequest.setEmail("dearmai@dearmai.net");
- messageRequest.setUmsUid(UUID.randomUUID().toString());
- messageRequest.setUsername("김광은");
- messageRequest.setSubject("제목 - test");
- messageRequest.setMessage("{\"ATHN_NO\": \"543216\"}");
- messageRequest.setServiceId("FM_APIM001");
+ KjbEmailSendModule service = KjbEmailSendModule.getInstance();
+
+ List requestList = new ArrayList<>();
+
+
+ log.info("#1 요청 생성 - 이메일 인증");
+ requestList.add(createMessageRequest(MessageCode.USER_VERIFICATION_EMAIL, "{\"ATHN_NO\": \"543216\"}"));
+
+ log.info("#2 요청 생성 - 비밀번호 초기화");
+ requestList.add(createMessageRequest(MessageCode.USER_PASSWORD_RESET, "{\"ATHN_NO\": \"new-password\"}"));
+
+ log.info("#3 요청 생성 - 비밀번호 변경 완료");
+ requestList.add(createMessageRequest(MessageCode.USER_PASSWORD_CHANGED, ""));
+
+ log.info("#4 요청 생성 - 로그인 제한");
+ requestList.add(createMessageRequest(MessageCode.USER_ACCOUNT_LOCKED, "{\"RJCT_RSN\": \"reason\"}"));
+
+ log.info("#5 요청 생성 - 이메일 초대");
+ requestList.add(createMessageRequest(MessageCode.USER_INVITATION,
+ "{\"CRPT_NM\":\"corpName\",\"MNGR_NM\":\"managerName\",\"CUST_ID\":\"loginId\",\"RCMD_CD\":\"321654\",\"URL_ADDR\":\"https://www.naver.com/\"}"));
+
+ log.info("#6 요청 생성 - 이메일 초대 취소");
+ requestList.add(createMessageRequest(MessageCode.USER_INVITATION_CANCELED,
+ "{\"CRPT_NM\":\"corpName\",\"MNGR_NM\":\"managerName\"}"));
+
+ log.info("#7 요청 생성 - 법인관리자 및 법인 등록 승인");
+ requestList.add(createMessageRequest(MessageCode.MANAGER_WITH_ORG_REGISTER_APPROVED, ""));
+
+ log.info("#8 요청 생성 - 법인관리자 및 법인 등록 거절");
+ requestList.add(createMessageRequest(MessageCode.MANAGER_WITH_ORG_REGISTER_REJECTED, "{\"RJCT_RSN\": \"reason\"}"));
+
+ log.info("#9 요청 생성 - 앱 승인");
+ requestList.add(createMessageRequest(MessageCode.APP_REGISTER_APPROVED, "{\"APP_IDNT_NM\": \"appName\"}"));
+
+ log.info("#10 요청 생성 - 앱 승인 거절");
+ requestList.add(createMessageRequest(MessageCode.APP_REGISTER_REJECTED, "{\"RJCT_RSN\": \"reason\"}"));
+
- log.info("MessageRequest - {}", messageRequest);
try {
- service.sendEmail(messageRequest);
+ for (int i = 0; i < requestList.size(); i++) {
+ MessageRequest request = requestList.get(i);
+ log.info("#{} - 메일 전송 시도 - {}", i + 1, request.getServiceId());
+ service.sendEmail(request);
+ }
} catch (KjbEaiBatchException e) {
throw new RuntimeException(e);
}
log.info("done");
}
+
+ private MessageRequest createMessageRequest(MessageCode messageCode, String message) {
+ MessageRequest messageRequest = new MessageRequest();
+ messageRequest.setId(UUID.randomUUID().toString());
+ messageRequest.setMessageCode(messageCode);
+ messageRequest.setEmail(System.getenv().getOrDefault("KJB_TEST_EMAIL", "25W0064@kjbank.com"));
+ messageRequest.setUmsUid(UUID.randomUUID().toString());
+ messageRequest.setUsername("김광은");
+ messageRequest.setSubject("제목 - test");
+ messageRequest.setMessage(message);
+// messageRequest.setServiceId(serviceId);
+
+ log.info("MessageRequest - {}", messageRequest);
+
+ return messageRequest;
+ }
}