diff --git a/CLAUDE.md b/CLAUDE.md index 881e98d..e51bbbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,11 +91,13 @@ kjb-gradle.sh tasks --all 1. **IDE 설정 파일 생성 (최초 1회)** ```bash - # IntelliJ IDEA 사용 시 - kjb-gradle.sh idea - - # Eclipse 사용 시 + # Eclipse 사용 시 (기본) kjb-gradle.sh eclipse + + # IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요) + # 1. build.gradle을 build.gradle.eclipse로 백업 + # 2. build.gradle.intellij를 build.gradle로 복사 + # 3. kjb-gradle.sh idea 실행 ``` 2. **QueryDSL 등 소스 코드 생성** @@ -109,7 +111,8 @@ kjb-gradle.sh tasks --all ``` 4. **IDE에서 프로젝트 열기** - - `kjb-gradle.sh idea` 또는 `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. + - Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. + - IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다. 5. **실행 구성(Run Configuration) 설정** - 사용하는 WAS(Tomcat 등)에 `eapim-admin` 프로젝트를 배포하도록 설정합니다. @@ -156,26 +159,24 @@ eapim-admin (root project) - **Lombok**: 애노테이션 프로세싱을 통한 Getters/setters/builders - **MapStruct**: DTO와 엔티티 간의 객체 매퍼 -#### 이중 Q-Class 생성 (Gradle + Eclipse) +#### Q-Class 생성 위치 -이 프로젝트는 **Gradle과 Eclipse 애노테이션 프로세서 모두를 사용**하므로, Q-classes가 **두 위치에 생성**됩니다: - -1. **`build/generated/java/`** - `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 생성 -2. **`WebContent/generated/`** - Eclipse IDE에서 빌드 시 Eclipse APT가 생성 - -**이는 예상된 동작이며 의도적입니다.** 두 디렉토리 모두 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다. +Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다. +- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성 +- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다 #### 생성된 코드 작업하기 - **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다 -- **Eclipse IDE**: `WebContent/generated/`의 Q-classes가 자동으로 classpath에 포함됩니다 -- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하거나 Eclipse 프로젝트를 새로고침하여 Q-classes를 재생성합니다 -- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava` 또는 Eclipse → Project → Clean으로 재생성합니다 +- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다 +- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다 -중복이 문제를 일으키지 않는 이유: -- 두 도구 모두 동일한 JPA 엔티티에서 동일한 Q-classes를 생성합니다 -- 두 출력 디렉토리 모두 gitignore됩니다 -- 각 도구는 자체 생성 클래스를 사용합니다 (classpath 충돌 없음) +#### IDE별 빌드 설정 파일 + +- **`build.gradle`**: Eclipse 전용 (기본, Eclipse APT 플러그인 포함) +- **`build.gradle.intellij`**: IntelliJ IDEA 전용 + +IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체해야 합니다. ## 로컬 개발 환경 설정 @@ -315,6 +316,50 @@ com.eactive.eai 리포지토리는 Spring Data JPA 규칙을 사용하며, 서비스는 `@Service`, 컨트롤러는 `@RestController`/`@Controller`를 사용합니다. +### Multi-tenancy (동적 스키마 전환) + +애플리케이션은 Hibernate Multi-tenancy를 사용하여 **런타임에 스키마를 동적으로 전환**합니다. + +**핵심 컴포넌트:** +- `DataSourceContextHolder` - ThreadLocal로 현재 데이터소스 타입 관리 +- `DataSourceTypeManager` - 데이터소스 타입 상수 정의 (MONITORING, APIGW, EAI, BAT 등) +- `TenantIdentifierResolver` - `DataSourceContextHolder`에서 tenant 식별자 반환 +- `ConfigurableMultiTenantConnectionProvider` - tenant에 따라 `connection.setSchema()` 호출 +- `DataSourceTypeInterceptor` - 요청 파라미터 `serviceType`에서 데이터소스 자동 설정 + +**동작 흐름:** +1. `DataSourceTypeInterceptor.preHandle()` → `request.getParameter("serviceType")` 읽음 +2. `DataSourceContextHolder.setDataSourceType(dataSourceType)` 설정 +3. JPA 쿼리 실행 시 `TenantIdentifierResolver` → `DataSourceContextHolder`에서 tenant 반환 +4. `ConfigurableMultiTenantConnectionProvider` → `connection.setSchema(schema)` 호출 + +**사용 방법:** + +1. **URL 파라미터 방식** (인터셉터가 자동 처리): + ``` + /api/endpoint?serviceType=APIGW + ``` + +2. **Controller에서 명시적 설정** (특정 데이터소스 강제): + ```java + @Controller + public class MyController { + @RequestMapping("/my/api.json") + public String myApi() { + // 서비스 호출 전에 데이터소스 설정 + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); + + return myService.getData(); + } + } + ``` + +**주의사항:** +- `@PersistenceContext`만 사용 (기본 `entityManagerFactory` 사용, Multi-tenancy 적용됨) +- `@PersistenceContext(unitName = "entityManagerFactoryForEMS")`는 **Multi-tenancy 미적용** (MONITORING 스키마 고정) +- Controller에서 `DataSourceContextHolder` 설정 후 Service 메서드를 호출해야 트랜잭션 시작 시점에 올바른 스키마가 적용됨 + ### 주요 초기화 - `AppInitializer` - 시스템 프로퍼티를 설정하는 메인 애플리케이션 시작 빈 - `CustomizingAppInitializer` - 고객사별 초기화 @@ -328,19 +373,7 @@ com.eactive.eai ## IDE 설정 -### Eclipse - -```bash -kjb-gradle.sh eclipse -``` - -다음을 생성합니다: -- `.project` 및 `.classpath` 파일 -- Eclipse WTP 설정 (context path: `/monitoring`) -- UTF-8 인코딩 설정 -- QueryDSL/Lombok을 위한 애노테이션 프로세서 설정 - -### IntelliJ IDEA +### IntelliJ IDEA (권장) ```bash kjb-gradle.sh idea @@ -348,6 +381,30 @@ kjb-gradle.sh idea 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. +기본 `build.gradle`은 IntelliJ 전용으로 설정되어 있습니다. + +### Eclipse + +Eclipse를 사용하려면 Eclipse APT 플러그인이 포함된 별도의 빌드 설정 파일을 사용해야 합니다: + +```bash +# 1. 기존 build.gradle 백업 (필요시) +cp build.gradle build.gradle.intellij + +# 2. Eclipse 전용 설정으로 교체 +cp build.gradle.eclipse build.gradle + +# 3. Eclipse 프로젝트 생성 +kjb-gradle.sh eclipse +``` + +Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다: +- `com.diffplug.eclipse.apt` 플러그인 (Eclipse APT 지원) +- `.project` 및 `.classpath` 파일 생성 +- Eclipse WTP 설정 (context path: `/monitoring`) +- UTF-8 인코딩 설정 +- QueryDSL/Lombok을 위한 애노테이션 프로세서 설정 + ## 테스트 테스트에 사용되는 것들: diff --git a/build.gradle.intellij b/build.gradle.intellij new file mode 100644 index 0000000..0d0dc1e --- /dev/null +++ b/build.gradle.intellij @@ -0,0 +1,244 @@ +plugins { + id 'java-library' + id 'idea' + id 'war' + id 'project-report' + id 'checkstyle' +} + +group 'com.eactive' +version '4.5.7' + +def springVersion = "5.3.27" +def quartzVersion = "2.2.1" +def queryDslVersion = "5.0.0" +def hibernateVersion = "5.6.15.Final" + +/*def nexusUrl = "https://nexus.eactive.synology.me:8090"*/ +//def useOnJboss = false + +def generatedJavaDir = "$buildDir/generated/java" + +/*repositories { + maven { + url "${nexusUrl}/repository/maven-public/" + allowInsecureProtocol = true + } +}*/ + +project.webAppDirName = 'WebContent' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} + +checkstyle { + toolVersion = '8.45.1' +} + +sourceSets { + main { + java { + srcDirs generatedJavaDir + } + } +} + +compileJava { + options.encoding = 'UTF-8' + options.compilerArgs = [ + '-parameters', + '-Aquerydsl.generatedAnnotationClass=com.querydsl.core.annotations.Generated' + ] + options.generatedSourceOutputDirectory = project.file(generatedJavaDir) +} + +war { + def profile = project.findProperty("profile") ?: "tomcat" + + if (profile == "weblogic") { + webXml = file("WebContent/WEB-INF/weblogic-web.xml") + exclude 'WEB-INF/web.xml' + } + + + // rootSpec.exclude '**/persistence.xml' + exclude '**/context.xml' + archiveFileName = "eapim-admin.war" + duplicatesStrategy = DuplicatesStrategy.WARN +} + + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"] +} + + +dependencies { + + annotationProcessor "org.projectlombok:lombok:1.18.28", "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final" + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa", "jakarta.persistence:jakarta.persistence-api:2.2.3", "jakarta.annotation:jakarta.annotation-api:1.3.5" + + implementation project(':elink-online-core') + implementation project(':elink-online-transformer') + implementation project(':elink-online-common') + implementation project(':elink-online-emsclient') + implementation project(':elink-portal-common') + implementation project(':kjb-safedb') + implementation project(":eapim-admin-kjb") + + /* Custom Libs */ + implementation fileTree(dir: 'libs', include: ['*.jar']) + + //implementation "org.dom4j:dom4j:2.1.3" + //implementation "com.rabbitmq:amqp-client:3.6.6" + + implementation "commons-primitives:commons-primitives:1.0" + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation "commons-lang:commons-lang:2.6" + implementation "commons-httpclient:commons-httpclient:3.1" + implementation "commons-cli:commons-cli:1.1" + implementation "commons-pool:commons-pool:1.6" + implementation group: 'commons-io', name: 'commons-io', version: '2.6' + implementation 'net.sf.j8583:j8583:1.18.0' + implementation "commons-beanutils:commons-beanutils:1.6.1" + implementation 'org.apache.commons:commons-compress:1.26.0' +// implementation "log4j:log4j:1.2.17" + + implementation "net.sf.ehcache:ehcache:2.10.5" + implementation "org.apache.poi:poi:3.2-FINAL" + + implementation 'org.codehaus.groovy:groovy:3.0.18' + + // https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api + compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1' + + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1' + //implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1' + + implementation "javax.jms:javax.jms-api:2.0" + implementation "org.snmp4j:snmp4j:1.10.1" + implementation "com.googlecode.json-simple:json-simple:1.1.1" + + implementation "io.netty:netty-all:4.1.0.Final" + + compileOnly "org.apache.mina:mina-filter-ssl:1.1.6" + compileOnly "org.apache.mina:mina-core:1.1.6" + + // https://mvnrepository.com/artifact/org.modelmapper/modelmapper + implementation 'org.modelmapper:modelmapper:2.4.5' + + ////implementation "xalan:xalan:2.6.0" + implementation "com.sun.xml.messaging.saaj:saaj-impl:1.3.28" + + implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726" + implementation "org.jdom:jdom:1.1" + implementation "com.google.code.gson:gson:2.3.1" + implementation "javax.annotation:javax.annotation-api:1.2" + implementation "net.sf.json-lib:json-lib-ext-spring:1.0.2" + implementation "taglibs:standard:1.1.2" + implementation "javax.servlet:jstl:1.1.2" + implementation group: 'com.jcraft', name: 'jsch', version: '0.1.51' + + implementation "org.quartz-scheduler:quartz:${quartzVersion}" + + // https://mvnrepository.com/artifact/org.apache.poi/poi + implementation group: 'org.apache.poi', name: 'poi', version: '3.17' + // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml + implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '3.17' + + implementation group: 'org.aspectj', name: 'aspectjrt', version: '1.9.7' + implementation group: 'org.aspectj', name: 'aspectjtools', version: '1.9.7' + + implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13' + implementation group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.4.15' + implementation group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.13' + + //implementation "com.eactive:ojdbc8:1.0" + //implementation "com.eactive:INISAFECore:v2.2.28" + //implementation "com.eactive:backport-util-concurrent:1.0" + + implementation "org.springframework:spring-web:${springVersion}" + implementation "org.springframework:spring-aop:${springVersion}" + implementation "org.springframework:spring-aspects:${springVersion}" + implementation "org.springframework:spring-orm:${springVersion}" + //implementation "com.eactive:spring-orm-ibatis-4.1.4:1.0" + implementation "org.springframework:spring-webmvc:${springVersion}" + implementation "org.springframework:spring-context:${springVersion}" + implementation "org.springframework:spring-context-support:${springVersion}" + implementation "org.springframework:spring-jdbc:${springVersion}" + implementation 'org.springframework.data:spring-data-jpa:2.5.2' + + implementation 'org.hibernate:hibernate-entitymanager:5.4.33.Final' + implementation 'org.hibernate:hibernate-validator:5.4.3.Final' + implementation 'org.hibernate:hibernate-ehcache:5.4.3.Final' + + implementation 'org.slf4j:jul-to-slf4j:1.7.35' + implementation 'org.slf4j:jcl-over-slf4j:1.7.35' + implementation 'org.slf4j:log4j-over-slf4j:1.7.35' + implementation 'org.slf4j:slf4j-api:1.7.35' + + implementation 'ch.qos.logback:logback-classic:1.2.10' + implementation 'ch.qos.logback:logback-access:1.2.10' + implementation 'ch.qos.logback:logback-core:1.2.10' + + implementation files("libs/eai-bap-client.jar") + + compileOnly 'javax.xml.bind:jaxb-api:2.3.1' + + //implementation 'org.postgresql:postgresql:42.2.23' + + implementation 'backport-util-concurrent:backport-util-concurrent:3.0' + + implementation ('software.amazon.awssdk:s3:2.20.142') { + exclude group: 'org.slf4j', module: 'slf4j-api' + exclude group: 'org.slf4j', module: 'logback-classic' + } + implementation 'software.amazon.awssdk:sso:2.20.142' + implementation 'software.amazon.awssdk:sts:2.20.142' + + implementation ('io.kubernetes:client-java:18.0.1') { + exclude group: 'org.slf4j', module: 'slf4j-api' + exclude group: 'org.slf4j', module: 'logback-classic' + } + + implementation group: 'commons-net', name: 'commons-net', version: '3.5' + + testRuntimeOnly 'com.h2database:h2:2.1.214' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15' +} + +test { + useJUnitPlatform() +} + +configurations.all { + exclude group: 'log4j', module: 'log4j' + exclude group: 'org.apache.activemq' + exclude group: 'org.codehaus.jackson' + resolutionStrategy { + // 10 minute cache of dynamic version navigation + cacheDynamicVersionsFor 10, 'minutes' + // Do not cache changing modules + cacheChangingModulesFor 0, 'seconds' + } +} + + +task initDirs() { + file(generatedJavaDir).mkdirs() +} + + +tasks.withType(Checkstyle) { + reports { + xml.required = false + html.required = true + } +} diff --git a/http/obp-GwMetric.http b/http/obp.http similarity index 76% rename from http/obp-GwMetric.http rename to http/obp.http index 4522ac3..28da692 100644 --- a/http/obp-GwMetric.http +++ b/http/obp.http @@ -4,4 +4,9 @@ GET {{emsUrl}}/kjb/gw-metrics/after-timeslice/2025-12-02_01:00:00.json ### Pretty Print -GET {{emsUrl}}/kjb/gw-metrics/after-timeslice/2025-12-02_01:00:00.json?pretty=true \ No newline at end of file +GET {{emsUrl}}/kjb/gw-metrics/after-timeslice/2025-12-02_01:00:00.json?pretty=true + + +### API 목록 +GET {{emsUrl}}/kjb/apis.json + diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiController.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiController.java new file mode 100644 index 0000000..6faca83 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiController.java @@ -0,0 +1,69 @@ +package com.eactive.eai.rms.onl.apim.obp; + +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.onl.apim.common.BaseRestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.List; + +/** + * OBP API 목록 조회 REST Controller + * + *
TSEAIHE01과 TSEAIHS04를 조인하여 API 정보를 JSON으로 제공
+ * + * @see BaseRestController 세션 체크 우회 및 JSON 유틸리티 제공 + */ +@Controller +public class ObpApiController extends BaseRestController { + + private static final Logger log = LoggerFactory.getLogger(ObpApiController.class); + + private final ObpApiService obpApiService; + + public ObpApiController(ObpApiService obpApiService) { + this.obpApiService = obpApiService; + } + + /** + * 전체 API 목록 조회 + * + * @param pretty true면 JSON 들여쓰기 적용 + * @return JSON Array + */ + @RequestMapping( + value = "/kjb/apis.json", + produces = JSON_CONTENT_TYPE + ) + @ResponseBody + public String getApis( + @RequestParam(value = "pretty", required = false) Boolean pretty) { + log.debug("OBP API 목록 조회 요청: pretty={}", pretty); + long startTime = System.currentTimeMillis(); + + // APIGW 데이터소스 강제 설정 + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); + + try { + ListTSEAIHE01과 TSEAIHS04 테이블을 조인하여 API 정보를 제공
+ */ +@Getter +@Builder +public class ObpApiDTO { + + /** + * API ID (TSEAIHE01.EAISVCNAME) + */ + private final String id; + + /** + * API 이름 (TSEAIHE01.EAISVCDESC) + */ + private final String name; + + /** + * 활성화 여부 (TSEAIHE01.USEYN = '1' -> true) + */ + private final boolean enabled; + + /** + * API URL (TSEAIHS04.APIFULLPATH에서 '|' 이후 부분) + */ + private final String url; + + /** + * HTTP Method (TSEAIHS04.APIFULLPATH에서 '|' 이전 부분) + */ + private final String method; + + /** + * 인증 타입 (TSEAIHE01.AUTHTYPE) + */ + private final String authType; +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiService.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiService.java new file mode 100644 index 0000000..54b8b8e --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpApiService.java @@ -0,0 +1,91 @@ +package com.eactive.eai.rms.onl.apim.obp; + +import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity; +import com.eactive.eai.data.entity.onl.stdmessage.QStandardMessageInfo; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; +import java.util.stream.Collectors; + +/** + * OBP API 목록 조회 Service + * + *APIGW 데이터소스의 TSEAIHE01, TSEAIHS04 테이블에서 API 정보 조회
+ */ +@Service +public class ObpApiService { + + @PersistenceContext + private EntityManager entityManager; + + private JPAQueryFactory getQueryFactory() { + return new JPAQueryFactory(entityManager); + } + + /** + * 전체 API 목록 조회 + * + * @return API 목록 + */ + public List