diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..1a4c63c --- /dev/null +++ b/.cursorrules @@ -0,0 +1,139 @@ +# Cursor Rules for EAPIM Portal + +> 이 파일은 CLAUDE.md를 기반으로 합니다. 내용 수정 시 CLAUDE.md와 동기화해주세요. + +## 프로젝트 개요 + +광주은행 EAPIM Portal - 엔터프라이즈 API 포털 관리 시스템 +- Spring Boot 2.7.18, Java 8, Gradle 7.x +- Oracle 12c 이중 데이터베이스 (EMS, Gateway) +- Spring Security, JPA/Hibernate, MapStruct, Lombok + +## 빌드 & 실행 + +```bash +# gradlew 사용 금지 - offline gradle 사용 +gradle bootRun # 개발 환경 실행 +gradle bootWar # 실행 가능 WAR +gradle war # 서버용 WAR (JEUS/WebLogic) +gradle test # 테스트 +``` + +## 아키텍처 핵심 + +### 외부 모듈 의존성 +- elink-online-core-jpa: Gateway 데이터 모델 +- elink-portal-common: 공통 유틸리티 +- kjb-safedb: SafeDB 암호화 라이브러리 + +### 패키지 구조 (기능 모듈 방식) +``` +apps/ +├── user/ # 사용자 관리 +├── app/ # API 키 관리 +├── approval/ # 승인 워크플로우 +├── apis/ # API 카탈로그 +├── proxy/ # API 테스트 프록시 +└── ... +``` + +각 모듈: controller/ service/ repository/ dto/ mapper/ + +### 이중 DB +- EMS (Portal): jdbc/dsOBP_EMS, com.eactive.apim.portal.apps.* +- Gateway: jdbc/dsOBP_AGW, com.eactive.eai.data.entity.onl.* +- Atomikos JTA로 분산 트랜잭션 + +### 프로파일 +- dev: 개발 (192.168.240.177:1599, SQL 로깅) +- stage: 스테이징 (JNDI, proxy to inter-dapiwas01) +- prod: 운영 (JNDI, proxy to inter-apiwas00, 캐싱) + +## 개발 패턴 + +### 새 기능 모듈 +1. apps/ 하위에 패키지 생성 +2. controller/, service/, repository/, dto/, mapper/ 구조 +3. Entity는 AbstractAuditingEntity 확장 +4. DTO는 Lombok @Data +5. Mapper는 MapStruct +6. Service는 @Transactional +7. Thymeleaf 템플릿 추가 +8. application.yml의 pages 섹션에 라우트 설정 (또는 @Controller 직접 매핑) + +### 보안 +- config/PortalConfigSecurity.java +- PortalAuthenticationManager (커스텀 인증) +- CSRF, XSS 방어, 세션 고정 공격 방어 +- 역할 계층: ROLE_USER < ROLE_CORP_USER < ROLE_CORP_MANAGER + +### 트랜잭션 +- 기본: @Transactional (REQUIRED) +- 읽기 전용: @Transactional(readOnly = true) +- 다중 DB는 자동 처리 (Atomikos JTA) + +### 로깅 +- SLF4J with Lombok @Slf4j +- 로그 경로: /Log/App/eapim/ +- 개발 환경만 SQL 로깅 + +### 쿼리 +- 우선: Spring Data JPA 메서드 +- 복잡한 쿼리: QueryDSL + +## 주요 기능 + +### 사용자 관리 (apps/user/) +- 내부 사용자 (Staff), 기업 사용자 (Enterprise) +- 가입 → 이메일 인증 → 관리자 승인 → 활성화 +- 비밀번호 정책: 90일 만료, 복잡도 규칙 + +### API 키 관리 (apps/app/) +- 승인 워크플로우 +- 개발/운영 환경 지원 +- eLink Gateway ClientID/ClientSecret 연동 + +### 감사 추적 +- AbstractAuditingEntity (createdBy, createdDate, lastModifiedBy, lastModifiedDate) +- @Audited로 변경 이력 +- Spring Data Envers로 조회 + +## 중요 사항 + +### 한글 지원 +- Thymeleaf 템플릿 한글 +- DB 데이터 UTF-8 +- messages_ko.properties + +### Forward Proxy +- Dev: http://localhost:10000 +- Stage: http://inter-dapiwas01:10000 +- Prod: http://inter-apiwas00:10000 + +### 비밀번호 정책 (PortalUserValidator) +- 최소 8자, 대소문자+숫자+특수문자 +- 연속 문자 불가, 최근 3개 재사용 불가 +- 90일 만료 + +### 세션 +- 15분 타임아웃 +- 단일 세션 강제 +- Redis/Ehcache 클러스터링 (stage/prod) + +### 파일 업로드 +- 최대 8MB +- /Log/App/eapim/files/ +- XSS 스캐닝 + +## 배포 +- JEUS, WebLogic, GlassFish 6.3, Tomcat 지원 +- JNDI 설정 필수 (stage/prod) +- Docker 지원 + +## 관련 프로젝트 +- ../eapim-admin/ +- ../eapim-online/ +- ../kjb-eapim-sql/ + +--- +상세 내용은 CLAUDE.md 참조 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..35270b0 Binary files /dev/null and b/.github/copilot-instructions.md differ diff --git a/.gitignore b/.gitignore index 17e8cca..2813ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,4 @@ TODO.txt *.lck *.log +.claude \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fd952b8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,456 @@ +# CLAUDE.md + +이 파일은 Claude Code(claude.ai/code)가 이 저장소의 코드를 작업할 때 참고하는 지침을 제공합니다. + +## 프로젝트 개요 + +**EAPIM Portal**은 광주은행을 위한 엔터프라이즈 API 포털 관리 시스템입니다. API 서비스 관리, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다. + +**기술 스택:** +- Spring Boot 2.7.18 with Spring MVC and Thymeleaf +- Java 8 (targetCompatibility: 1.8) +- Gradle 8.7 빌드 시스템 +- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스) +- Spring Security (커스텀 인증) +- JPA/Hibernate 5.6.15 with Envers (감사 추적) +- MapStruct (DTO 매핑), Lombok (보일러플레이트 제거) + +## 빌드 명령어 + +### 애플리케이션 실행 +- gradlew 사용 금지 - offline gradle 단독 실행 +- msi-gf63 컴퓨터에서는 /c/eactive/workspaces/shell-scripts/kjb-gradle.sh 로 gradle 실행 (jdk, gradle 등 환경 변수 세팅 후 gradle 실행하는 스크립트) + +```bash +# dev 프로파일로 실행 (기본값) +gradle bootRun + +# 특정 프로파일로 실행 +gradle bootRun --args='--spring.profiles.active=stage' +``` + +### 빌드 + +```bash +# Spring Boot 실행 가능 WAR 빌드 +gradle bootWar +# 출력: build/libs/eapim-portal-boot.war + +# 애플리케이션 서버용 표준 WAR 빌드 (JEUS/WebLogic) +gradle war +# 출력: build/libs/eapim-portal.war + +# 클린 후 빌드 +gradle clean build +``` + +### 테스트 + +```bash +# 모든 테스트 실행 +gradle test + +# 커버리지와 함께 테스트 실행 +gradle test --info + +# 특정 테스트 클래스 실행 +gradle test --tests "com.eactive.apim.portal.apps.user.AccountControllerTest" + +# 패턴에 매칭되는 테스트 실행 +gradle test --tests "*Controller*" +``` + +### 개발 + +```bash +# 현재 프로파일 확인 +gradle printProfile + +# 소스 세트 설정 확인 +gradle printSourceSets + +# Docker 이미지 빌드 +./build_docker.sh +``` + +## 아키텍처 + +### 멀티 모듈 구조 + +이 프로젝트는 Gradle composite build를 통해 3개의 외부 모듈에 의존합니다: + +1. **elink-online-core-jpa** (`../eapim-online/elink-online-core-jpa`) + - Gateway 데이터 모델 및 JPA 엔티티 + - API 명세 엔티티 및 리포지토리 제공 + - 패키지: `com.eactive.eai.data.entity.onl.*` + +2. **elink-portal-common** (`../elink-portal-common`) + - 공유 포털 유틸리티 및 공통 컴포넌트 + - 기본 리포지토리 구현, QueryDSL 지원 + - 공통 예외 핸들러 및 보안 유틸리티 + +3. **kjb-safedb** (`../kjb-safedb`) + - 광주은행 SafeDB 암호화 라이브러리 + - 데이터베이스 레벨 민감 데이터 암호화 + - 암호화 컬럼(사용자 비밀번호, API 키 등)에 필수 + +### 패키지 구조 + +코드는 기술 계층이 아닌 **기능 모듈**(수직 분할) 방식으로 구성됩니다: + +``` +com.eactive.apim.portal/ +├── apps/ # 기능 모듈 +│ ├── agreements/ # API 약관 +│ ├── apis/ # API 카탈로그 & 문서 +│ ├── apiservice/ # API 서비스 그룹핑 +│ ├── app/ # API 키 관리 +│ ├── approval/ # 승인 워크플로우 +│ ├── auth/ # 인증 +│ ├── community/ # FAQ, 공지사항, Q&A, 제휴문의 +│ ├── dashboard/ # 통계 & 분석 +│ ├── file/ # 파일 업로드/다운로드 +│ ├── login/ # 로그인/로그아웃 +│ ├── proxy/ # API 테스트용 Forward Proxy +│ ├── sample/ # 샘플 코드 생성 +│ └── user/ # 사용자 관리 +├── common/ # 공통 관심사 +├── config/ # Spring 설정 +└── gateway/ # Gateway DB 직접 접근 + └── data/ # Gateway 리포지토리 +``` + +각 기능 모듈은 일반적으로 다음을 포함합니다: +- `controller/` - Spring MVC 컨트롤러 (@Controller) +- `service/` - 비즈니스 로직 (@Service, @Transactional) +- `repository/` - Spring Data JPA 리포지토리 +- `dto/` - 데이터 전송 객체 (Lombok @Data) +- `mapper/` - MapStruct 인터페이스 (entity ↔ DTO 변환) + +### 이중 데이터베이스 아키텍처 + +애플리케이션은 두 개의 Oracle 데이터베이스를 사용합니다: + +1. **EMS Database** (Portal/Admin) + - JNDI: `jdbc/dsOBP_EMS` + - Schema: `EMSADM` + - Entities: `com.eactive.apim.portal.apps.*` + - 목적: 포털 사용자, 조직, API 키, 승인 + +2. **Gateway Database** (API Specs) + - JNDI: `jdbc/dsOBP_AGW` + - Schema: `AGWADM` + - Entities: `com.eactive.eai.data.entity.onl.*` + - 목적: API 명세, 서비스, 메시지 + +설정: `config/PortalDatasourceConfiguration.java` +- 각 데이터베이스별 별도 EntityManager +- Atomikos JTA를 통한 분산 트랜잭션 +- 트랜잭션 로그: `/Log/eapim/portal/` + +### 설정 프로파일 + +환경별 설정 파일: `src/main/resources/application-{profile}.yml` + +- **dev**: 개발 환경 (Oracle at 192.168.240.177:1599, DevTools 활성화, SQL 로깅) +- **stage**: 스테이징 환경 (JNDI 데이터소스, proxy to inter-dapiwas01) +- **prod**: 운영 환경 (JNDI 데이터소스, proxy to inter-apiwas00, 캐싱 활성화) +- **gf63**: 개인 개발 환경 +- **kjb_rinjae**: 개인 개발 환경 + +주요 설정: +- 세션 타임아웃: 15분 +- 파일 업로드 최대: 8MB +- 비밀번호 만료: 90일 +- 인증 토큰 TTL: 5분 (300초) +- 사용자 승인 필수: true + +## 주요 기능 및 비즈니스 로직 + +### 사용자 관리 (`apps/user/`) + +두 가지 사용자 유형: +1. **내부 사용자 (Staff)**: 포털 관리자 및 운영자 +2. **기업 사용자 (Enterprise)**: 외부 API 소비자 + +사용자 라이프사이클: +- 가입 → 이메일 인증 → 관리자 승인 → 활성화 +- 비밀번호 정책 적용 (90일 만료, 복잡도 규칙) +- 로그인 실패 시 계정 잠금 +- 휴면 계정 관리 + +### API 키 관리 (`apps/app/`) + +API 키는 승인 워크플로우와 함께 관리됩니다: +- 기업 관리자가 `AppRequestController`를 통해 키 요청 +- 관리자가 `ApprovalController`를 통해 승인/반려 +- 키는 개발/운영 환경 지원 +- 키는 eLink Gateway ClientID/ClientSecret과 연결 + +### 승인 워크플로우 (`apps/approval/`) + +다음 항목에 대한 범용 승인 시스템: +- 사용자 등록 승인 +- API 키 생성/수정 +- 제휴 신청 + +엔티티: `Approval`, `Approver`, `ApprovalStatus` (WAITING, APPROVED, REJECTED) + +### API 테스트 (`apps/proxy/`, `apps/sample/`) + +- Gateway 서버로의 Forward Proxy +- 요청/응답 로깅이 있는 API 테스터 +- 여러 언어의 샘플 코드 생성 +- API 문서를 위한 Swagger UI 통합 + +### 감사 추적 (Hibernate Envers) + +모든 엔티티는 자동 감사를 위해 `AbstractAuditingEntity`를 확장합니다: +- `createdBy`, `createdDate` +- `lastModifiedBy`, `lastModifiedDate` +- `@Audited` 어노테이션으로 변경 이력 관리 +- Spring Data Envers를 통한 이력 조회 + +## 공통 개발 패턴 + +### 새 기능 모듈 추가하기 + +1. `apps/` 하위에 패키지 생성 (예: `apps/newfeature/`) +2. `controller/`, `service/`, `repository/`, `dto/`, `mapper/`로 구조화 +3. `repository/entity/`에 JPA 엔티티 생성 (`AbstractAuditingEntity` 확장) +4. `dto/`에 Lombok `@Data`를 사용한 DTO 생성 +5. `mapper/`에 MapStruct 매퍼 인터페이스 생성 +6. `@Service`와 `@Transactional`을 사용한 서비스 계층 구현 +7. `@Controller`와 `@RequestMapping`을 사용한 컨트롤러 생성 +8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가 +9. `application.yml`의 `pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑) + +### MapStruct DTO 매핑 + +타입 안전한 DTO 변환을 위해 MapStruct 사용: + +```java +@Mapper(componentModel = "spring") +public interface UserMapper { + PortalUserDto toDto(PortalUser entity); + PortalUser toEntity(PortalUserDto dto); + List toDtoList(List entities); +} +``` + +생성된 구현체는 `build/generated/sources/annotationProcessor/java/main/`에 위치 + +### 보안 및 인증 + +보안 설정: `config/PortalConfigSecurity.java` +- 커스텀 인증 관리자: `PortalAuthenticationManager` +- `changeSessionId()`를 통한 세션 고정 공격 방어 +- 쿠키 기반 토큰을 사용한 CSRF 방어 +- Lucy 필터를 통한 XSS 방어 + +역할 계층: +``` +ROLE_USER → [ROLE_INQUIRY, ROLE_ACCOUNT] +ROLE_CORP_USER → [ROLE_INQUIRY, ROLE_APP, ROLE_ACCOUNT] +ROLE_CORP_MANAGER → [ROLE_API_KEY_REQUEST, ROLE_INQUIRY, ROLE_APP, + ROLE_ACCOUNT, ROLE_CORP_API, ROLE_DASHBOARD, + ROLE_USER_MANAGER] +``` + +### 데이터베이스 쿼리 + +Spring Data JPA 리포지토리 메서드 사용 권장: + +```java +public interface PortalUserRepository extends JpaRepository { + Optional findByEmail(String email); + + @Query("SELECT u FROM PortalUser u WHERE u.status = :status") + List findByStatus(@Param("status") UserStatus status); +} +``` + +복잡한 쿼리는 QueryDSL 사용: +```java +QPortalUser user = QPortalUser.portalUser; +return queryFactory.selectFrom(user) + .where(user.email.eq(email) + .and(user.status.eq(UserStatus.ACTIVE))) + .fetchOne(); +``` + +### 트랜잭션 관리 + +적절한 propagation과 함께 `@Transactional` 사용: +- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성) +- 읽기 전용 작업: 최적화를 위해 `@Transactional(readOnly = true)` +- 다중 데이터베이스: Atomikos JTA가 자동으로 분산 트랜잭션 처리 + +### 에러 처리 + +`common/exception/`의 글로벌 예외 처리: +- `@ControllerAdvice`가 적용된 `GlobalExceptionHandler` +- 커스텀 예외는 `RuntimeException` 확장 +- Thymeleaf 에러 뷰 또는 AJAX 요청에는 JSON 반환 + +### 로깅 + +`application.yml`의 로깅 설정: +```yaml +logging: + file: + path: /Log/App/eapim/ + level: + com.eactive.apim.portal: DEBUG + org.hibernate.SQL: DEBUG (개발 환경만) +``` + +Lombok의 `@Slf4j`와 함께 SLF4J 사용: +```java +@Slf4j +@Service +public class UserService { + public void someMethod() { + log.debug("디버그 메시지"); + log.info("정보 메시지"); + log.error("에러 메시지", exception); + } +} +``` + +## 테스트 가이드 + +### 테스트 구조 + +`src/test/java/com/eactive/apim/portal/`의 테스트: +- `BaseWebTest` - Spring Boot 테스트 컨텍스트를 가진 기본 클래스 +- 컨트롤러 테스트는 `BaseWebTest` 확장 +- 통합 테스트는 `@SpringBootTest` 사용 +- 독립적인 컨트롤러 테스트는 `@WebMvcTest` 사용 + +### 일반적인 테스트 패턴 + +```java +@SpringBootTest +@AutoConfigureMockMvc +class UserControllerTest extends BaseWebTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Test + void testUserRegistration() throws Exception { + mockMvc.perform(post("/user/register") + .param("email", "test@example.com") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("user/register-success")); + } +} +``` + +### SafeDB 테스트 + +테스트에서는 기본적으로 SafeDB 암호화가 비활성화됩니다. 필요한 경우: +```bash +gradle test -Dsafedb=/path/to/safedb +``` + +## 중요 사항 + +### 한글 지원 + +이 포털은 한글을 광범위하게 사용합니다: +- Thymeleaf 템플릿은 한글 레이블 사용 +- 데이터베이스는 한글 데이터 포함 (UTF-8) +- 한글 메시지 프로퍼티 (`messages_ko.properties`) +- 한글 사용자 가이드 (`개발자포탈.md`) + +### Forward Proxy 설정 + +API 테스트는 Gateway로의 Forward Proxy 사용: +- Dev: `http://localhost:10000` +- Stage: `http://inter-dapiwas01:10000` +- Prod: `http://inter-apiwas00:10000` + +설정: `apps/proxy/service/ProxyService.java` + +### 비밀번호 정책 + +`PortalUserValidator`에서 적용: +- 최소 8자 +- 대문자, 소문자, 숫자, 특수문자 포함 필수 +- 연속된 문자 사용 불가 +- 최근 3개 비밀번호 재사용 불가 +- 90일 후 만료 + +### 세션 관리 + +- 사용자당 단일 세션 강제 +- 세션 타임아웃: 15분 +- 로그인 시 세션 고정 공격 방어 +- Redis/Ehcache를 통한 세션 클러스터링 (스테이징/운영) + +### 파일 업로드 제한 + +- 최대 파일 크기: 8MB +- `FileService`에서 허용 확장자 설정 +- 파일 저장 위치: `/Log/App/eapim/files/` +- 업로드 시 XSS 스캐닝 + +## 배포 + +### 애플리케이션 서버 지원 + +WAR 파일 호환 서버: +- **JEUS** (`jeus-web-dd.xml` 필요) +- **WebLogic** (`weblogic.xml` 필요) +- **Tomcat** (Spring Boot 내장) + +### JNDI 설정 필수 + +스테이징/운영 환경에서 JNDI 리소스 설정: +- `jdbc/dsOBP_EMS` → Portal 데이터베이스 +- `jdbc/dsOBP_AGW` → Gateway 데이터베이스 + +### 환경 변수 + +```bash +# 선택사항: 기본 설정 오버라이드 +JAVA_OPTS="-Xmx2g -Xms1g -Dspring.profiles.active=prod" +``` + +### Docker 배포 + +```bash +# WAR 빌드 +gradle bootWar + +# Docker 이미지 빌드 +./build_docker.sh + +# 컨테이너 실행 +docker run -p 8080:8080 \ + -e JAVA_OPTS="-Dspring.profiles.active=prod" \ + -v /Log:/Log \ + eapim-portal:latest +``` + +## 관련 모듈 + +다음 항목에 영향을 주는 변경 시: +- **Gateway API 명세**: `elink-online-core-jpa` 모듈 확인 +- **공통 유틸리티**: `elink-portal-common` 모듈 확인 +- **암호화**: `kjb-safedb` 모듈 확인 +- **Admin 포털**: `../eapim-admin/` 관련 프로젝트 +- **Online 포털**: `../eapim-online/` 관련 프로젝트 + +## 추가 자료 + +- 사용자 가이드 (한글): `개발자포탈.md` +- 빌드 스크립트: `build-gf63.sh`, `deploy_portal.sh`, `deploy_portal2.sh` +- Docker: `Dockerfile`, `build_docker.sh` +- SQL 스크립트: `../kjb-eapim-sql/` diff --git a/README.md b/README.md new file mode 100644 index 0000000..64b2e56 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# EAPIM Portal + +광주은행 엔터프라이즈 API 포털 관리 시스템 + +## 프로젝트 개요 + +**EAPIM Portal**은 광주은행의 API 서비스를 관리하고, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다. + +## 기술 스택 + +- Spring Boot 2.7.18 +- Java 8 +- Gradle 8.7 +- Oracle 19c +- Spring Security +- JPA/Hibernate 5.6.15 with Envers +- MapStruct, Lombok + +## 빌드 및 실행 + +```bash +# 개발 환경 실행 +gradle bootRun + +# WAR 빌드 +gradle bootWar + +# 테스트 +gradle test +``` + +**주의**: gradlew 사용 금지 - offline gradle 단독 실행 + +## 프로젝트 구조 + +``` +src/main/java/com/eactive/apim/portal/ +├── apps/ # 기능 모듈 (user, app, approval, apis, etc.) +├── common/ # 공통 관심사 +├── config/ # Spring 설정 +└── gateway/ # Gateway DB 접근 +``` + +## 외부 모듈 + +이 프로젝트는 다음 모듈에 의존합니다: +- `elink-online-core-jpa`: Gateway 데이터 모델 +- `elink-portal-common`: 공통 유틸리티 +- `kjb-safedb`: SafeDB 암호화 라이브러리 + +## AI 코딩 어시스턴트 지침 파일 + +이 프로젝트는 여러 AI 코딩 어시스턴트를 지원합니다. 각 도구는 다음 파일을 참조합니다: + +### 지침 파일 위치 + +| AI 도구 | 지침 파일 경로 | 용도 | +|---------|---------------|------| +| **Claude Code** | `CLAUDE.md` | 마스터 지침 파일 (한글) | +| **GitHub Copilot** | `.github/copilot-instructions.md` | Copilot용 지침 | +| **Cursor** | `.cursorrules` | Cursor용 간결한 규칙 | + +### 지침 파일 관리 방법 + +1. **마스터 파일**: `CLAUDE.md` + - 가장 상세한 프로젝트 지침을 포함 + - 한글로 작성되어 한국 개발팀이 이해하기 쉬움 + - 수정 시 이 파일을 먼저 업데이트 + +2. **동기화**: + ```bash + # CLAUDE.md 수정 후 다른 파일들 동기화 + cp CLAUDE.md .github/copilot-instructions.md + # .cursorrules는 간결한 버전이므로 필요시 수동 업데이트 + ``` + +3. **새로운 AI 도구 지원 추가**: + - Windsurf: `.windsurfrules` 파일 생성 + - 기타 도구: 해당 도구의 규칙 파일명 확인 후 생성 + +### 지침 파일에 포함된 내용 + +- 프로젝트 개요 및 기술 스택 +- 빌드 명령어 및 개발 환경 설정 +- 아키텍처 구조 (멀티 모듈, 이중 DB) +- 주요 기능 및 비즈니스 로직 +- 개발 패턴 및 코딩 컨벤션 +- 보안, 트랜잭션, 로깅 가이드 +- 테스트 패턴 +- 배포 방법 + +## 환경 설정 + +### 프로파일 + +- **dev**: 개발 환경 +- **stage**: 스테이징 환경 +- **prod**: 운영 환경 +- **gf63**: 개인 개발 환경 +- **kjb_rinjae**: 개인 개발 환경 + +### 데이터베이스 + +- **EMS Database**: Portal 사용자, 조직, API 키 +- **Gateway Database**: API 명세, 서비스 + +## 주요 기능 + +- 사용자 관리 (내부/기업) +- API 키 관리 및 승인 워크플로우 +- API 카탈로그 및 문서화 +- API 테스트 프록시 +- 감사 추적 (Hibernate Envers) + +## 배포 + +### 지원 서버 +- JEUS +- WebLogic +- Tomcat (내장) + +### Docker +```bash +./build_docker.sh +docker run -p 8080:8080 eapim-portal:latest +``` + +## 관련 프로젝트 + +- `../eapim-admin/` - Admin 포털 +- `../eapim-online/` - Online 포털 +- `../kjb-eapim-sql/` - SQL 스크립트 + +## 문서 + +- **프로젝트 상세 지침**: `CLAUDE.md` (한글) +- **사용자 가이드**: `개발자포탈.md` (한글) +- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh` + +## 라이선스 + +© 광주은행 (Kwangju Bank) diff --git a/build.gradle b/build.gradle index 27c8ddc..779fd1e 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(':elink-online-core-jpa') implementation project(':elink-portal-common') + implementation project(':kjb-safedb') implementation('org.springframework.boot:spring-boot-starter') implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos' diff --git a/kjb-docs/README.md b/kjb-docs/README.md index 0aa5631..1ecd3c0 100644 --- a/kjb-docs/README.md +++ b/kjb-docs/README.md @@ -8,7 +8,18 @@ ### 디비 접속 방법 Direct -> JNDI 로 변경 - 은행 내부 환경에선 모두 JNDI 사용하기 때문에 서버환경에서 동작하는 profile 내 application.yaml 에서 디비 접속 정보 삭제 -## 긴급 로깅이 필요할 경우 JVM 파라미터 추가 +## 서버 운영 중 긴급 로깅이 필요할 경우 JVM 파라미터 추가 하여 재기동 +- DEBUG 레벨 root 로그 / debug.log 로 파일 작성 ``` --Dlogback.configurationFile=classpath:logback-debug.xml +-Dlogging.config=classpath:logback-debug.xml +``` + +## 참고자료 +### 개발용 파라미터 +``` +-Dlogging.config=classpath:logback-rinjae.xml +-Dinst.Name=devSvr98 +-Duser.language=en +-Duser.country=US +-Dkjb_safedb.mode=fake ``` \ No newline at end of file diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberGenerator.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberGenerator.java index 59c9bca..ca37f32 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberGenerator.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberGenerator.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import javax.annotation.PostConstruct; import java.security.SecureRandom; @Component @@ -18,6 +19,12 @@ public class AuthNumberGenerator { } + @PostConstruct + public void init() { + log.warn("가상 인증코드 활성화 상태 - {}", portalProperties.getAuthVirtualCode()); + } + + public String generateAuthNumber() { if (StringUtils.hasLength(portalProperties.getAuthVirtualCode())) { log.info("가상 인증코드 활성화 상태 - {}", portalProperties.getAuthVirtualCode()); diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java index d873d08..99285ed 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java @@ -22,10 +22,10 @@ public class AuthNumberServiceImpl implements AuthNumberService { private final AuthNumberGenerator generator; private final MessageSender messageSender; - @Value("${auth-ttl:300}") + @Value("${portal.auth-ttl:300}") private int authNumberExpirationTime; - @Value("${auth.resend.limit.seconds:30}") + @Value("${portal.auth.resend_limit_seconds:30}") private int resendLimitSeconds; @Autowired diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/MessageSender.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/MessageSender.java index 10c26d8..d45adec 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/service/MessageSender.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/MessageSender.java @@ -39,7 +39,6 @@ public class MessageSender { } else { throw new IllegalArgumentException("Unsupported message type: " + msgType); } - messageHandlerService.publishEvent(code, recipient, params); } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java index a57a97b..a3e49dc 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java @@ -46,6 +46,7 @@ public class AccountController { private final UserFacade userFacade; private final OrgRegisterFacade orgRegisterFacade; private final AgreementsFacade agreementsFacade; + private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade; @PostMapping("/confirm_password") @@ -326,4 +327,115 @@ public class AccountController { return "redirect:/mypage"; } } + + @GetMapping("/mypage/verification-email") + public String showVerificationEmailPage(Model model) { + try { + // 현재 로그인한 사용자 정보 가져오기 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + + if (currentUser == null) { + return "redirect:/login"; + } + + // 사용자 이메일 주소를 모델에 추가 + model.addAttribute("email", currentUser.getUsername()); + model.addAttribute("userId", currentUser.getId()); + + return "apps/mypage/verificationEmail"; + } catch (Exception e) { + logger.error("이메일 인증 페이지 로드 실패", e); + return "redirect:/"; + } + } + + @PostMapping("/mypage/send-verification-code") + public ResponseEntity sendVerificationCode( + @org.springframework.web.bind.annotation.RequestBody java.util.Map request, + HttpSession session) { + try { + String email = request.get("email"); + + // 현재 로그인한 사용자의 이메일과 일치하는지 확인 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + if (currentUser == null || !currentUser.getUsername().equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("유효하지 않은 요청입니다."); + return ResponseEntity.badRequest().body(response); + } + + // 이전 세션 데이터 정리 + session.removeAttribute("emailVerified"); + + // 이메일 인증 코드 발송 + ValidationResponse response = authFacade.requestAuth(email, "EMAIL"); + + if (response.isValid()) { + session.setAttribute("verificationEmail", email); + } + + return ResponseEntity.ok(response); + + } catch (Exception e) { + logger.error("이메일 인증 코드 발송 실패", e); + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 코드 발송에 실패했습니다."); + return ResponseEntity.status(500).body(response); + } + } + + @PostMapping("/mypage/verify-email-code") + public ResponseEntity verifyEmailCode( + @org.springframework.web.bind.annotation.RequestBody java.util.Map request, + HttpSession session) { + try { + String email = request.get("email"); + String code = request.get("code"); + + // 현재 로그인한 사용자의 이메일과 일치하는지 확인 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + if (currentUser == null || !currentUser.getUsername().equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("유효하지 않은 요청입니다."); + return ResponseEntity.badRequest().body(response); + } + + // 세션에 저장된 이메일과 일치하는지 확인 + String sessionEmail = (String) session.getAttribute("verificationEmail"); + if (sessionEmail == null || !sessionEmail.equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 요청된 이메일과 일치하지 않습니다."); + return ResponseEntity.badRequest().body(response); + } + + // 인증 코드 확인 + ValidationResponse response = authFacade.verifyAuthNumber(email, code); + + if (response.isValid()) { + session.setAttribute("emailVerified", true); + + // 사용자 상태를 ACTIVE로 변경 + userFacade.activateUserByEmail(email); + + // 이메일 인증 관련 세션 정보 제거 + session.removeAttribute("emailVerificationRequired"); + session.removeAttribute("redirectUrl"); + + logger.info("이메일 인증 완료: {}", email); + } + + return ResponseEntity.ok(response); + + } catch (Exception e) { + logger.error("이메일 인증 코드 확인 실패", e); + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 코드 확인에 실패했습니다."); + return ResponseEntity.status(500).body(response); + } + } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/dto/PortalUserRegistrationDTO.java b/src/main/java/com/eactive/apim/portal/apps/user/dto/PortalUserRegistrationDTO.java index b12a450..cf41d11 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/dto/PortalUserRegistrationDTO.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/dto/PortalUserRegistrationDTO.java @@ -3,8 +3,7 @@ package com.eactive.apim.portal.apps.user.dto; import com.eactive.apim.portal.common.validator.AuthNumberMatch; import com.eactive.apim.portal.common.validator.CellPhone; import com.eactive.apim.portal.common.validator.PasswordMatch; -import com.eactive.apim.portal.common.validator.PasswordRule; -import com.eactive.apim.portal.common.validator.PasswordRuleForKbank; +import com.eactive.apim.portal.common.validator.PasswordRuleForKjbank; import lombok.Data; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; @@ -13,7 +12,7 @@ import org.hibernate.validator.constraints.NotEmpty; @AuthNumberMatch(recipient = "loginId", authField = "authNumber") @PasswordMatch(input = "password", confirm = "password2") @Data -@PasswordRuleForKbank(password = "password", loginId = "loginId", mobile = "mobileNumber") +@PasswordRuleForKjbank(password = "password", loginId = "loginId", mobile = "mobileNumber") public class PortalUserRegistrationDTO { /** diff --git a/src/main/java/com/eactive/apim/portal/apps/user/dto/UserRegisterDTO.java b/src/main/java/com/eactive/apim/portal/apps/user/dto/UserRegisterDTO.java index 7340660..c200c9b 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/dto/UserRegisterDTO.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/dto/UserRegisterDTO.java @@ -1,8 +1,7 @@ package com.eactive.apim.portal.apps.user.dto; import com.eactive.apim.portal.common.validator.PasswordMatch; -import com.eactive.apim.portal.common.validator.PasswordRule; -import com.eactive.apim.portal.common.validator.PasswordRuleForKbank; +import com.eactive.apim.portal.common.validator.PasswordRuleForKjbank; import com.eactive.apim.portal.common.validator.UniqueId; import com.eactive.apim.portal.portaluser.entity.UserStatus; import lombok.Data; @@ -14,7 +13,7 @@ import java.io.Serializable; @PasswordMatch(input = "password", confirm = "password2") @Data -@PasswordRuleForKbank(loginId = "userId", password = "password", mobile = "mobilePhone") +@PasswordRuleForKjbank(loginId = "userId", password = "password", mobile = "mobilePhone") public class UserRegisterDTO implements Serializable { diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java index e3e3994..b23c960 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java @@ -19,8 +19,8 @@ import com.eactive.apim.portal.file.service.FileTypeContext; import com.eactive.apim.portal.portalorg.entity.PortalOrg; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; -import com.eactive.apim.portal.portaluser.event.UserVerificationEmailEvent; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; +import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.service.MessageHandlerService; import com.eactive.apim.portal.template.service.MessageRecipient; import lombok.RequiredArgsConstructor; @@ -206,7 +206,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { try { String encToken = encryptionUtil.encrypt(tokenValue); params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8))); - messageHandlerService.publishEvent(UserVerificationEmailEvent.KEY, recipient, params); + messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params); } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { throw new SystemException("암호화 모듈 오류"); } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java index 9ad4952..47cedb2 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java @@ -15,4 +15,6 @@ public interface UserFacade { void updateCorporateManager(PortalUserDTO portalUserDTO); void withdrawUser(String userId); + + void activateUserByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java index 33c8b38..66ce1bf 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java @@ -198,4 +198,23 @@ public class UserFacadeImpl implements UserFacade { throw new IllegalArgumentException("새 비밀번호와 확인 비밀번호가 일치하지 않습니다."); } } + + @Override + @Transactional + public void activateUserByEmail(String email) { + PortalUser user = portalUserService.findByEmailAddr(email); + + if (user == null) { + throw new IllegalArgumentException("사용자를 찾을 수 없습니다."); + } + + if (user.getUserStatus() == PortalUserEnums.UserStatus.ACTIVE) { + throw new IllegalArgumentException("이미 활성화된 계정입니다."); + } + + user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE); + portalUserService.save(user); + + log.info("사용자 이메일 인증 완료 - 활성화: {}", email); + } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java index dc7baa0..961578f 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java @@ -10,6 +10,7 @@ import com.eactive.apim.portal.common.util.SecurityUtil; import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.invitation.entity.UserInvitation; import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus; +import com.eactive.apim.portal.invitation.event.UserInvitationCancelEvent; import com.eactive.apim.portal.invitation.event.UserInvitationEvent; import com.eactive.apim.portal.portaluser.event.UserManagerAssignedEvent; import com.eactive.apim.portal.invitation.repository.UserInvitationRepository; @@ -183,7 +184,29 @@ public class UserManFacade { throw new IllegalStateException("Invitation is not in a cancelable state"); } + // invitationId 로 사용자 이름 조회, 실패시 id 를 이름으로 대체 + String name = portalUserRepository.findById(invitation.getInvitationEmail()) + .map(PortalUser::getUserName) + .orElse(invitation.getInvitationEmail()); + + // name 이 이메일 일 경우 @ 앞부분 만 사용 (안하면 암호화 해야함...) + if (name.contains("@")) { + name = name.substring(0, name.indexOf("@")); + } + invitation.setStatus(InvitationStatus.CANCELED); + + messageHandlerService.publishEvent(UserInvitationCancelEvent.KEY, + MessageRecipient.builder() + .username(name) + .userId(invitationId) + .build(), + new HashMap() {{ + put("corpName", user.getPortalOrg().getOrgName()); + put("managerName", user.getUserName()); + }} + ); + userInvitationRepository.save(invitation); } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java index 4411baa..f28e68c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java @@ -16,8 +16,8 @@ import com.eactive.apim.portal.invitation.entity.UserInvitation; import com.eactive.apim.portal.invitation.entity.UserInvitationEnums; import com.eactive.apim.portal.invitation.repository.UserInvitationRepository; import com.eactive.apim.portal.portaluser.entity.PortalUser; -import com.eactive.apim.portal.portaluser.event.UserVerificationEmailEvent; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; +import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.service.MessageHandlerService; import com.eactive.apim.portal.template.service.MessageRecipient; import lombok.RequiredArgsConstructor; @@ -146,7 +146,8 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { } agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_IND); - sendEmailActivation(newUser); + // 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동 + // sendEmailActivation(newUser); return new ValidationResponse(true,"회원가입이 완료되었습니다."); } @@ -219,7 +220,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { String tokenValue = String.valueOf(authNumberGenerator.generateAuthNumber()); // String tokenValue = registeredUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + registeredUser.getId(); params.put("token", tokenValue); - messageHandlerService.publishEvent(UserVerificationEmailEvent.KEY, recipient, params); + messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params); // try { // String encToken = encryptionUtil.encrypt(tokenValue); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java index c69dc2b..3ec73a5 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java @@ -96,9 +96,10 @@ public class PortalUserAuthService implements UserDetailsService { public void initializePassword(String loginId, String userName, String mobileNumber) { - PortalUser portalUser = portalUserRepository.findByLoginIdAndUserNameAndMobileNumber(loginId, userName, mobileNumber).orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다.")); + PortalUser portalUser = portalUserRepository.findByLoginIdAndUserNameAndMobileNumber(loginId, userName, mobileNumber) + .orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다.")); - String tempPassword = EncryptionUtil.generateNewPassword(); + String tempPassword = EncryptionUtil.generateNewPasswordForKjbank(loginId); portalUser.setPasswordHash(passwordEncoder.encode(tempPassword)); portalUserRepository.save(portalUser); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java index ba95543..d8b17c5 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java @@ -49,6 +49,15 @@ public class PortalUserService { return portalUserRepository.findByLoginId(loginId); } + public PortalUser findByEmailAddr(String emailAddr) { + return portalUserRepository.findPortalUserByEmailAddr(emailAddr) + .orElseThrow(() -> new IllegalArgumentException("해당 이메일 주소의 사용자를 찾을 수 없습니다.")); + } + + public PortalUser save(PortalUser user) { + return portalUserRepository.save(user); + } + public boolean existsByLoginId(String loginId) { return portalUserRepository.existsByLoginId(loginId); } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/validator/PasswordValidator.java b/src/main/java/com/eactive/apim/portal/apps/user/validator/PasswordValidator.java index c670202..6ca9f6b 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/validator/PasswordValidator.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/validator/PasswordValidator.java @@ -1,6 +1,7 @@ package com.eactive.apim.portal.apps.user.validator; import com.eactive.apim.portal.common.validator.PasswordRuleForKbankValidator; +import com.eactive.apim.portal.common.validator.PasswordRuleForKjbankValidator; import org.springframework.stereotype.Component; @Component @@ -16,7 +17,7 @@ public class PasswordValidator { } public boolean isValidPassword(String password, String loginId, String mobileNumber) { - PasswordRuleForKbankValidator validator = new PasswordRuleForKbankValidator(); + PasswordRuleForKjbankValidator validator = new PasswordRuleForKjbankValidator(); return validator.isValid(password, loginId, mobileNumber); } diff --git a/src/main/java/com/eactive/apim/portal/common/user/PortalAuthenticatedUser.java b/src/main/java/com/eactive/apim/portal/common/user/PortalAuthenticatedUser.java index 251d1d2..6d8f3e0 100644 --- a/src/main/java/com/eactive/apim/portal/common/user/PortalAuthenticatedUser.java +++ b/src/main/java/com/eactive/apim/portal/common/user/PortalAuthenticatedUser.java @@ -40,7 +40,7 @@ public class PortalAuthenticatedUser extends PortalUser implements UserDetails { @Override public boolean isAccountNonLocked() { - return true; + return !"Y".equalsIgnoreCase(this.getAccountLockYn()); } @Override diff --git a/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java b/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java index 365514c..bc02ff7 100644 --- a/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java +++ b/src/main/java/com/eactive/apim/portal/common/util/EncryptionUtil.java @@ -30,7 +30,30 @@ public class EncryptionUtil { return newpassword.toString(); } - @Value("${encryption.key:kbank_portal_application_1357902}") + /** + * 광주은행용 임시패스워드 생성 + * - @ + 이메일 주소 앞 5자리 + 랜덤숫자 3자리 + ! + * @return + */ + public static String generateNewPasswordForKjbank(String loginId) { + StringBuilder newpassword = new StringBuilder(); + newpassword.append("@"); + + if (loginId.length() <= 5) { + newpassword.append(loginId); + } else { + newpassword.append(loginId.substring(0, 5)); + } + + for (int i = 1; i <= 3; i++) { + newpassword.append(RandomUtil.getRandomNum(0, 9)); + } + newpassword.append("!"); + + return newpassword.toString(); + } + + @Value("${encryption.key:kjbank_portal_application_1357902}") private String secretKey; // Should be 16, 24, or 32 bytes long for AES-128, AES-192, or AES-256 private static final String ALGORITHM = "AES"; diff --git a/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbank.java b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbank.java new file mode 100644 index 0000000..dab49ad --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbank.java @@ -0,0 +1,24 @@ +package com.eactive.apim.portal.common.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Constraint(validatedBy = PasswordRuleForKjbankValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PasswordRuleForKjbank { + String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String password(); + + String loginId(); + + String mobile(); + +} diff --git a/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbankValidator.java b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbankValidator.java new file mode 100644 index 0000000..35d7201 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleForKjbankValidator.java @@ -0,0 +1,144 @@ +package com.eactive.apim.portal.common.validator; + +import org.apache.commons.beanutils.PropertyUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Sungpil Hyun + */ +public class PasswordRuleForKjbankValidator implements ConstraintValidator { + + // 최소 8자, 최대 20자 상수 선언 + private static final int MIN = 8; + private static final int MAX = 20; + + private String password; + private String loginId; + private String mobileNumber; + + // 3자리 연속 문자 정규식 + private static final String SAMEPT = "(\\w)\\1\\1"; + // 공백 문자 정규식 + private static final String BLANKPT = "(\\s)"; + + @Override + public void initialize(PasswordRuleForKjbank constraintAnnotation) { + this.password = constraintAnnotation.password(); + this.loginId = constraintAnnotation.loginId(); + this.mobileNumber = constraintAnnotation.mobile(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + + String passwordValue = null; + String loginIdValue = null; + String mobileNumberValue = null; + + try { + passwordValue = (String) PropertyUtils.getProperty(value, this.password); + loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId); + mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber); + } catch (Exception e) { + return false; + } + + return isValid(passwordValue, loginIdValue, mobileNumberValue); + } + + public boolean isValid(String password, String loginId, String mobileNumber) { + // 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식 + String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$"; + + // 정규식 검사객체 + Matcher matcher; + + // 공백 체크 + if (password == null || "".equals(password)) { + return false; + } + + // ASCII 문자 비교를 위한 UpperCase + String tmpPw = password.toUpperCase(); + // 문자열 길이 + int strLen = tmpPw.length(); + + // 글자 길이 체크 + if (strLen > 20 || strLen < 8) { + return false; + } + + if (loginId != null && !loginId.isEmpty()) { + String[] loginParts = loginId.split("@"); + if (loginParts.length > 0) { + String username = loginParts[0].toUpperCase(); + if (tmpPw.contains(username)) { + return false; + } + } + } + + // Mobile number validation + if (mobileNumber != null && !mobileNumber.isEmpty()) { + String[] mobileParts = mobileNumber.split("-"); + for (String part : mobileParts) { + if (!part.isEmpty() && tmpPw.contains(part)) { + return false; + } + } + } + + // 공백 체크 + matcher = Pattern.compile(BLANKPT).matcher(tmpPw); + if (matcher.find()) { + return false; + } + + // 비밀번호 정규식 체크 + matcher = Pattern.compile(REGEX).matcher(tmpPw); + if (!matcher.find()) { + return false; + } + + // 동일한 문자 3개 이상 체크 + matcher = Pattern.compile(SAMEPT).matcher(tmpPw); + if (matcher.find()) { + return false; + } + + // 연속된 문자 / 숫자 3개 이상 체크 + // ASCII Char를 담을 배열 선언 + int[] tmpArray = new int[strLen]; + + // Make Array + for (int i = 0; i < strLen; i++) { + tmpArray[i] = tmpPw.charAt(i); + } + + // Validation Array + for (int i = 0; i < strLen - 2; i++) { + if (isContinuous(tmpArray[i], tmpArray[i + 2]) && isContinuous(tmpArray[i], tmpArray[i + 1], tmpArray[i + 2])) { + return false; + } + } + // Validation Complete + return true; + } + + + + static boolean isContinuous(int first, int third) { + // 첫 글자 A-Z / 0-9 + return (first > 47 && third < 58) || (first > 64 && third < 91); + } + + static boolean isContinuous(int first, int second, int third) { + // 배열의 연속된 수 검사 + // 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2 + return Math.abs(third - second) == 1 && Math.abs(third - first) == 2; + } +} diff --git a/src/main/java/com/eactive/apim/portal/config/BaseDatasourceConfiguration.java b/src/main/java/com/eactive/apim/portal/config/BaseDatasourceConfiguration.java index dd17a85..e8100b3 100644 --- a/src/main/java/com/eactive/apim/portal/config/BaseDatasourceConfiguration.java +++ b/src/main/java/com/eactive/apim/portal/config/BaseDatasourceConfiguration.java @@ -73,6 +73,7 @@ public class BaseDatasourceConfiguration { properties.put("hibernate.default_schema", prop.getSchema()); properties.put("javax.persistence.validation.mode", "none"); properties.put("javax.persistence.transactionType", "JTA"); + properties.put("hibernate.format_sql", "true"); if (prop instanceof EmsDatasourceProperty) { persistenceUnit = "portal"; @@ -95,7 +96,7 @@ public class BaseDatasourceConfiguration { } // 개발 환경용 - if (env.matchesProfiles("dev", "local")) { + if (env.matchesProfiles("local")) { properties.put("hibernate.hbm2ddl.auto", "validate"); } diff --git a/src/main/java/com/eactive/apim/portal/config/GatewayDatasourceConfiguration.java b/src/main/java/com/eactive/apim/portal/config/GatewayDatasourceConfiguration.java index 940e5ba..d824550 100644 --- a/src/main/java/com/eactive/apim/portal/config/GatewayDatasourceConfiguration.java +++ b/src/main/java/com/eactive/apim/portal/config/GatewayDatasourceConfiguration.java @@ -1,7 +1,6 @@ package com.eactive.apim.portal.config; import com.eactive.eai.data.jpa.BaseRepositoryImpl; -import com.zaxxer.hikari.HikariDataSource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; @@ -9,20 +8,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; -import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.jndi.JndiObjectFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import javax.annotation.PostConstruct; import javax.naming.NamingException; import javax.sql.DataSource; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; @Configuration @EntityScan(basePackages = {"com.eactive.apim.gateway"}) @@ -78,28 +71,6 @@ public class GatewayDatasourceConfiguration extends BaseDatasourceConfiguration return createDataSource(gatewayProp); } - @Primary - @Bean - public DataSource portalDataSource(EmsDatasourceProperty emsProp) { - // JNDI 이용 - if (StringUtils.isNotEmpty(emsProp.getJndiName())) { - try { - return this.jndiLookup(emsProp.getJndiName()); - } catch (NamingException e) { - log.warn("JNDI lookup failed.", e); - } - } - - log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도"); - - // JNDI 실패시 직접 접속 이용 - if (StringUtils.isEmpty(emsProp.getJdbcUrl())) { - throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음"); - } - - return createDataSource(emsProp); - } - @Bean(name = "gatewayEntityManagerFactory") public LocalContainerEntityManagerFactoryBean gatewayEntityManagerFactory( EntityManagerFactoryBuilder builder, @@ -107,18 +78,4 @@ public class GatewayDatasourceConfiguration extends BaseDatasourceConfiguration GatewayDataSourceProperty gatewayProp) { return createEntityManagerFactory(builder, dataSource, gatewayProp); } - - @Primary - @Bean(name = "entityManagerFactory") - public LocalContainerEntityManagerFactoryBean entityManagerFactory( - EntityManagerFactoryBuilder builder, - @Qualifier("gatewayDataSource") DataSource dataSource, - EmsDatasourceProperty emsProp) { - return createEntityManagerFactory(builder, dataSource, emsProp); - } - - @Bean - public AuditorAware auditorProvider() { - return new AuditorAwareImpl(); - } } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java index e2bb337..c6d6f2e 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java @@ -5,11 +5,10 @@ import com.eactive.apim.portal.apps.user.service.PortalUserLogService; import com.eactive.apim.portal.common.exception.UserNotFoundException; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; -import java.io.IOException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - +import com.eactive.apim.portal.template.entity.MessageCode; +import com.eactive.apim.portal.template.service.MessageHandlerService; +import com.eactive.apim.portal.template.service.MessageRecipient; +import org.apache.groovy.util.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; @@ -20,6 +19,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + /** * Created by Sungpil Hyun */ @@ -30,10 +34,15 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure private static final Logger logger = LoggerFactory.getLogger(PortalAuthenticationFailureHandler.class); private final PortalUserRepository portalUserRepository; private final PortalUserLogService userLogService; + private final MessageHandlerService messageHandlerService; - public PortalAuthenticationFailureHandler(PortalUserRepository portalUserRepository, PortalUserLogService userLogService) { + + public PortalAuthenticationFailureHandler(PortalUserRepository portalUserRepository, + PortalUserLogService userLogService, + MessageHandlerService messageHandlerService) { this.portalUserRepository = portalUserRepository; this.userLogService = userLogService; + this.messageHandlerService = messageHandlerService; } @Override @@ -53,8 +62,17 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure user.setLoginFailureCount(user.getLoginFailureCount() + 1); if (user.getLoginFailureCount() >= 5) { user.setAccountLockYn("Y"); + + // 계정 잠금 알림 + messageHandlerService.publishEvent( + MessageCode.USER_ACCOUNT_LOCKED, + MessageRecipient.of(user), + Maps.of("reason", "5회 이상 로그인 실패로 인한 계정 잠금")) ;; } portalUserRepository.save(user); + + + } catch (UserNotFoundException e) { logger.error("{} login try {}", username, e.getMessage()); } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java index 138effb..b8955a5 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java @@ -5,8 +5,12 @@ import com.eactive.apim.portal.apps.user.service.PortalUserAuthService; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; +import com.eactive.apim.portal.template.entity.MessageCode; +import com.eactive.apim.portal.template.service.MessageHandlerService; +import com.eactive.apim.portal.template.service.MessageRecipient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.groovy.util.Maps; import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -25,6 +29,7 @@ public class PortalAuthenticationManager implements AuthenticationManager { private final PortalUserAuthService portalUserAuthService; private final PasswordEncoder passwordEncoder; + private final MessageHandlerService messageHandlerService; private String decodePassword(String encodedPassword) { try { @@ -46,9 +51,22 @@ public class PortalAuthenticationManager implements AuthenticationManager { PortalAuthenticatedUser user = (PortalAuthenticatedUser) portalUserAuthService.loadUserByUsername(username); + + if (!user.isAccountNonLocked()) { + if (user.getLoginFailureCount() >= 5) { + throw new LockedException("계정이 잠겼습니다. 비밀번호 초기화 또는 관리자에게 문의하세요."); + } + + throw new LockedException("로그인 할 수 없습니다. 비밀번호 초기화 또는 관리자에게 문의하세요."); + } + + // 비밀번호 검증 if (!passwordEncoder.matches(decodedPassword, user.getPassword())) { - throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다."); + // 암호화가 되어 있지 않을 수도 있어 그대로 비교 검증 + if (!user.getPassword().equals(decodedPassword)) { + throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다."); + } } // DORMANT 상태 체크 @@ -64,9 +82,10 @@ public class PortalAuthenticationManager implements AuthenticationManager { throw new DisabledException("사용자 승인 대기중입니다. "); } - if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) { - throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요."); - } + // 25.11.13 - 이메일 인증 로그인은 허용 +// if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) { +// throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요."); +// } if (user.getUserStatus().equals(PortalUserEnums.UserStatus.ADMINBLOCK)) { throw new DisabledException("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다."); @@ -79,9 +98,7 @@ public class PortalAuthenticationManager implements AuthenticationManager { } } - if (!user.isAccountNonLocked()) { - throw new LockedException("로그인할 수 없습니다. 관리자에게 문의하세요."); - } + UsernamePasswordAuthenticationToken authorityToken = new UsernamePasswordAuthenticationToken(user, decodedPassword, user.getAuthorities()); diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java index df91d07..0806640 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java @@ -55,7 +55,11 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess HttpSession session = request.getSession(); // 세션에 상태 저장 - if (isDormantAccount(user)) { + if (isEmailVerificationRequired(user)) { + session.setAttribute("success", "이메일 인증이 완료되지 않았습니다. 이메일을 확인하여 인증을 완료해주세요."); + session.setAttribute("emailVerificationRequired", true); + session.setAttribute("redirectUrl", contextPath + "/mypage/verification-email"); + } else if (isDormantAccount(user)) { session.setAttribute("success", "90일 이상 미접속하여 계정이 잠금 처리되었습니다. 본인인증 후 이용해주세요."); session.setAttribute("dormantAccount", true); session.setAttribute("dormantLoginId", username); @@ -114,4 +118,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus()); } + private boolean isEmailVerificationRequired(PortalUser user) { + return PortalUserEnums.UserStatus.READY.equals(user.getUserStatus()); + } + } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalDatasourceConfiguration.java b/src/main/java/com/eactive/apim/portal/config/PortalDatasourceConfiguration.java index f13c8bf..8e66dd4 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalDatasourceConfiguration.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalDatasourceConfiguration.java @@ -1,5 +1,6 @@ package com.eactive.apim.portal.config; +import com.eactive.apim.portal.spring.DatabaseSessionVerifier; import com.eactive.eai.data.jpa.BaseRepositoryImpl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -57,23 +58,39 @@ public class PortalDatasourceConfiguration extends BaseDatasourceConfiguration { @Primary @Bean public DataSource portalDataSource(EmsDatasourceProperty emsProp) { + DataSource dataSource = null; + + log.debug("Portal datasource initialize."); + // JNDI 이용 if (StringUtils.isNotEmpty(emsProp.getJndiName())) { try { - return this.jndiLookup(emsProp.getJndiName()); + log.debug("Try get JNDI datasource : {}", emsProp.getJndiName()); + dataSource = this.jndiLookup(emsProp.getJndiName()); } catch (NamingException e) { log.warn("JNDI lookup failed.", e); } } - log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도"); + if (dataSource == null) { + log.info("개발환경을 위해 JNDI 대신 직접 DB 접속 시도"); - // JNDI 실패시 직접 접속 이용 - if (StringUtils.isEmpty(emsProp.getJdbcUrl())) { - throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음"); + // JNDI 실패시 직접 접속 이용 + if (StringUtils.isEmpty(emsProp.getJdbcUrl())) { + throw new IllegalArgumentException("DB 접속을 위한 JNDI 또는 서버 접속 정보가 설정되어 있지 않음"); + } + + dataSource = createDataSource(emsProp); } - return createDataSource(emsProp); + if (dataSource != null && env.matchesProfiles("db_check")) { + log.debug("Activated 'db_check' profile."); + DatabaseSessionVerifier verifier = new DatabaseSessionVerifier(dataSource); + verifier.afterSingletonsInstantiated(); + } + + + return dataSource; } @Primary diff --git a/src/main/java/com/eactive/apim/portal/spring/DatabaseSessionVerifier.java b/src/main/java/com/eactive/apim/portal/spring/DatabaseSessionVerifier.java new file mode 100644 index 0000000..b95b22c --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/spring/DatabaseSessionVerifier.java @@ -0,0 +1,100 @@ +package com.eactive.apim.portal.spring; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.SmartInitializingSingleton; + +import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +//@Configuration +//@Profile("db_check") +@Slf4j +public class DatabaseSessionVerifier implements SmartInitializingSingleton { + private final DataSource emsDataSource; + + + public DatabaseSessionVerifier(DataSource emsDataSource) { + this.emsDataSource = emsDataSource; + } + + + @Override + public void afterSingletonsInstantiated() { + log.info("DB 검증 작업 시작 - {}", now()); + log.info("---------------------------------------------------"); + + log.info("1. 커넥션 확인"); + try (Connection conn = emsDataSource.getConnection()) { + try (PreparedStatement statement = conn.prepareStatement("select 1 from dual")) { + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + log.info("DB 커넥션 확인됨"); + } + } + + log.info("\n2. Table 권한 확인 - {}", now()); + Map> grantTables = this.printTablePrivileges(conn); + + + log.info("\n3. PTL_PROPERTY 테이블 확인 - {}", now()); + if (!grantTables.containsKey("PTL_PROPERTY")) { + log.info("PTL_PROPERTY 테이블 확인 안됨. ROLE 재로딩 시도"); + + try (Statement stmt = conn.createStatement()) { + stmt.execute("SET ROLE_RL_EMS_ALL"); + } + + this.printTablePrivileges(conn); + + throw new InterruptedException("DB 검증 실패로 강제 종료"); + } else { + log.info("PTL_PROPERTY 테이블 확인 됨."); + } + + log.info("검증 작업 종료 - {}", now()); + } catch (Exception e) { + log.error("DB 세션 검증 실패", e); + throw new RuntimeException(e); + } + + throw new RuntimeException("db_check profile end."); + } + + + private LinkedHashMap> printTablePrivileges(Connection conn) throws SQLException { + LinkedHashMap> tables = new LinkedHashMap<>(); + + try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM ROLE_TAB_PRIVS WHERE role = ? ORDER BY table_name ASC")) { + stmt.setString(1, "RL_EMS_ALL"); + ResultSet rs = stmt.executeQuery(); + while(rs.next()) { + Set privileges; + if (tables.containsKey(rs.getString("table_name"))) { + privileges = tables.get(rs.getString("table_name")); + } else { + privileges = new LinkedHashSet<>(); + tables.put(rs.getString("table_name").toUpperCase(), privileges); + } + + privileges.add(rs.getString("privilege")); + } + + log.info("| Table | Privileges |"); + log.info("|---|---|"); + tables.forEach((key, value) -> log.info(String.format("| %-20s | %-36s |", key, value))); + } + + return tables; + } + + + private String now() { + return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } +} diff --git a/src/main/java/com/eactive/apim/portal/tools/JpaErrorLoggingAspect.java b/src/main/java/com/eactive/apim/portal/tools/JpaErrorLoggingAspect.java new file mode 100644 index 0000000..bad9ac7 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/tools/JpaErrorLoggingAspect.java @@ -0,0 +1,28 @@ +package com.eactive.apim.portal.tools; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.sql.SQLException; + +@Component +@Aspect +@Slf4j +public class JpaErrorLoggingAspect { + @PersistenceContext + private EntityManager em; + + @AfterThrowing(pointcut = "@annotation(org.springframework.transaction.annotation.Transactional)", throwing = "ex") + public void onError(SQLException ex) { + try { + String user = (String) em.createNativeQuery("select user from dual").getSingleResult(); + log.error("DB USER = {}, SQL ERROR = {}", user, ex.getMessage() ); + } catch (Exception e) { + log.error("Failed to fetch DB User", e); + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 9b9da74..09dae30 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -18,6 +18,23 @@ spring: thymeleaf: cache: false +ems: + datasource: + jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM + driver-class-name: oracle.jdbc.OracleDriver + username: EMSAPP + password: EMSAPP123! + + +gateway: + datasource: + jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM + driver-class-name: oracle.jdbc.OracleDriver + username: AGWAPP + password: AGWAPP123! + portal: auth-virtual-code: 654321 diff --git a/src/main/resources/application-gf63.yml b/src/main/resources/application-gf63.yml index a03f062..d0ad9e3 100644 --- a/src/main/resources/application-gf63.yml +++ b/src/main/resources/application-gf63.yml @@ -19,8 +19,8 @@ spring: ems: datasource: -# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM - jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM + jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM driver-class-name: oracle.jdbc.OracleDriver username: EMSAPP password: EMSAPP123! @@ -28,8 +28,8 @@ ems: gateway: datasource: -# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM - jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM + jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/PAPM driver-class-name: oracle.jdbc.OracleDriver username: AGWAPP password: AGWAPP123! diff --git a/src/main/resources/application-kjb_rinjae.yml b/src/main/resources/application-kjb_rinjae.yml new file mode 100644 index 0000000..17b7fbe --- /dev/null +++ b/src/main/resources/application-kjb_rinjae.yml @@ -0,0 +1,51 @@ +server.port: '30200' + +spring: + jta: + enabled: true + jpa: + properties: + hibernate: + show_sql: false + format_sql: true + use_sql_comments: true + devtools: + restart: + enabled: false + livereload: + enabled: true + thymeleaf: + cache: false + +ems: + datasource: +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM + jdbc-url: jdbc:oracle:thin:@//10.14.8.30:11521/ELINKDB + driver-class-name: oracle.jdbc.OracleDriver + username: EMSAPP + password: EMSAPP123! + + +gateway: + datasource: +# jdbc-url: jdbc:oracle:thin:@//192.168.240.177:1599/DAPM + jdbc-url: jdbc:oracle:thin:@//10.14.8.30:11521/ELINKDB + driver-class-name: oracle.jdbc.OracleDriver + username: AGWAPP + password: AGWAPP123! + +portal: + logging: + log-path: C:\eactive\logs + auth-virtual-code: 654321 + +proxy: + targets: + stg: http://localhost:10000/monitoring + prod: http://localhost:10000/monitoring + + # stg: http://inter-ndapiap01.k-bank.com:7090/monitoring + # prod: http://inter-ndapiap01.k-bank.com:7090/monitoring + timeout: + connect: 5000 + read: 5000 diff --git a/src/main/resources/application-stage.yml b/src/main/resources/application-stage.yml index 0cd16bd..d9a4f86 100644 --- a/src/main/resources/application-stage.yml +++ b/src/main/resources/application-stage.yml @@ -18,6 +18,25 @@ spring: portal: auth-virtual-code: 654321 + +ems: + datasource: + jndi-name: jdbc/dsOBP_EMS + schema: EMSADM + + hibernate-dialect: org.hibernate.dialect.Oracle12cDialect + hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy + entity-package: com.eactive.apim.portal + +gateway: + datasource: + jndi-name: jdbc/dsOBP_AGW + schema: AGWADM + + hibernate-dialect: org.hibernate.dialect.Oracle12cDialect + hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy + entity-package: com.eactive.eai.data.entity.onl + #proxy: # targets: # stg: http://inter-dapiwas01.k-bank.com:7090/monitoring diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 96cace5..1bced55 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,7 @@ server: whitelabel: enabled: false path: /error + forward-headers-strategy: native spring: @@ -74,6 +75,8 @@ portal: log-path: /Log/App/eapim/ auth-ttl: 300 + auth: + resend_limit_seconds: 30 user-approval: true password-expiration-days: 90 diff --git a/src/main/resources/logback-debug.xml b/src/main/resources/logback-debug.xml index d3c786a..3892104 100644 --- a/src/main/resources/logback-debug.xml +++ b/src/main/resources/logback-debug.xml @@ -1,21 +1,82 @@ - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - + + - + + ${PATTERN_DATE} + + + ${LOG_PATH}/debug.log ${LOG_PATH}/backup/debug.%d{yyyy-MM-dd}.log + 30 + + ${PATTERN} + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + ${LOG_PATH}/debug-hibernate.log + + ${LOG_PATH}/backup/debug-hibernate.%d{yyyy-MM-dd}.log 7 - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + ${PATTERN} + + + + ${LOG_PATH}/debug-report.log + + ${LOG_PATH}/backup/debug-report.%d{yyyy-MM-dd}.log + 7 + + + %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback-gf63.xml b/src/main/resources/logback-gf63.xml index a0220d0..27f5050 100644 --- a/src/main/resources/logback-gf63.xml +++ b/src/main/resources/logback-gf63.xml @@ -40,6 +40,7 @@ + diff --git a/src/main/resources/logback-rinjae.xml b/src/main/resources/logback-rinjae.xml new file mode 100644 index 0000000..59120c4 --- /dev/null +++ b/src/main/resources/logback-rinjae.xml @@ -0,0 +1,82 @@ + + + + + + + ${PATTERN_DATE} + + + + ${LOG_PATH}/root.log + + ${LOG_PATH}/backup/root.%d{yyyy-MM-dd}.log + 30 + + ${PATTERN} + + + + ${LOG_PATH}/hibernate.log + + ${LOG_PATH}/backup/hibernate.%d{yyyy-MM-dd}.log + 7 + + + ${PATTERN} + + + + + ${LOG_PATH}/report.log + + ${LOG_PATH}/backup/report.%d{yyyy-MM-dd}.log + 7 + + + %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 47d84e7..5698dc6 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -102,7 +102,7 @@ - > + diff --git a/src/main/resources/static/js/front2.js b/src/main/resources/static/js/front2.js index 8105d33..645acf1 100644 --- a/src/main/resources/static/js/front2.js +++ b/src/main/resources/static/js/front2.js @@ -163,7 +163,9 @@ document.addEventListener('DOMContentLoaded', function() { const sub = document.getElementById('sub'); // 초기에 서브 메뉴 숨기기 - sub.style.display = 'none'; + if (sub == null) { + sub.style.display = 'none'; + } // 마우스가 메인 네비게이션에 진입했을 때 mainNav.addEventListener('mouseenter', function() { diff --git a/src/main/resources/templates/views/apps/main/index.html b/src/main/resources/templates/views/apps/main/index.html index f6aea41..30036cd 100644 --- a/src/main/resources/templates/views/apps/main/index.html +++ b/src/main/resources/templates/views/apps/main/index.html @@ -1,372 +1,476 @@ + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_index_layout}"> - -
-
-
-
-
-
-
-
-
- 🚀 - Wa bank 생태계에 오신 것을 환영합니다! +
+ + +
+
+
+ + 슬라이드 1 +
+

디지털 금융 리더 케이뱅크
새로운 비즈니스 기회를
제공합니다.

-

- Wa한 금융 혁신,
- 여러분의 아이디어로 시작됩니다 -

-

- 광주은행 오픈 API로 새로운 금융 서비스를 만들어보세요.
- 안전하고 빠르게, 그리고 쉽게 연동할 수 있습니다. -

- +
+ 슬라이드 2 +
+

차별화된 API와
편리한 테스트 환경

지원합니다

+
+
+
+ 슬라이드 3 +
+

디지털 금융 혁신
케이뱅크가
함께합니다

-
- - -
-
-
-
-
- - -
-
-
- 추천 검색어: - - +
+
+ 슬라이드 1 +
+

케이뱅크가 새로운 비즈니스
기회를 제공합니다

- -
- 추천 검색어: - #계좌조회 - #송금API - #실시간결제 - #오픈뱅킹 +
+
+ 슬라이드 2 +
+

차별화된 API와
+ 편리한 테스트 환경
지원합니다

+
+
+
+ 슬라이드 3 +
+

디지털 금융 혁신
케이뱅크가
함께합니다

-
+ + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ -
-
-
-

- 인기 API로 바로 시작하세요 -

-

가장 많이 사용되는 API를 확인하고 바로 테스트해보세요

+
+
+
+
+ + +
+
+
+
+ +
+ +
+
+

가장 많이 찾는 API

+

+ 전체보기 +

-
- -
- - -
인기
-
NEW
- - -
- - -
- - -

API 이름

- - -

API 설명

- - -
- GET - 서비스 -
- - - - 테스트하기 - +
+ +
+
+
+ +
+
- -
- -
-
-
-
-
- - 금융 혁신 -
-
+
+

Kbank API PORTAL

+

Kbank API PORTAL

+

Kbank API PORTAL

+

Kbank API PORTAL

+

+ Kbank API Portal은 기업이 금융서비스를
+ 편리하게 개발할 수 있도록 차별화된
+ API와 테스트환경을 제공합니다.
+

+

+ Kbank API Portal은 기업이 금융서비스를 편리하게 개발할 수 있도록 차별화된 API와 테스트환경을 제공합니다. +

+ +
+
-
-

- 광주은행 API Portal은 기업이 금융서비스를 편리하게 개발할 수 있도록
- 차별화된 API와 테스트환경을 제공합니다. +

-
+
- -
-
-
-

- 도움이 필요하신가요? -

-

다양한 방법으로 필요한 정보를 찾아보세요

-
-
- -
- -
-

공지사항

-

최신 업데이트와 중요한 소식을 확인하세요

-
- -
-
- -
- -
-

자주 묻는 질문

-

자주 묻는 질문과 답변을 빠르게 찾아보세요

-
- -
-
- -
- -
-

문의하기

-

추가 도움이 필요하시면 직접 문의해주세요

-
- -
-
+
+
+

우리곁의 수많은 서비스들이
케이뱅크API
함께하고 있습니다

+

우리곁의 수많은 서비스들이
케이뱅크API와 함께하고 있습니다

+
+
    +
  • + 서비스 이용 수 +
    +

    + 서비스 이용 +
    +
  • +
  • + API 이용건수 +
    +

    + API 이용 +
    +
  • +
  • + API 활용기업 +
    +

    + API 활용 기업 +
    +
  • +
-
+
- -
-
-
-

- 우리곁의 수많은 서비스들이
- 광주은행 API와 함께하고 있습니다 -

-
-
-
-
- -
-
-
0
-
서비스 이용 수
-
다양한 분야의 서비스가 활용중
-
-
-
-
- -
-
-
0
-
API 이용 건수
-
일일 평균 API 호출 수
-
-
-
-
- -
-
-
0
-
API 활용 기업
-
혁신적인 파트너사와 함께
-
-
-
-
-
- +
+
- + diff --git a/src/main/resources/templates/views/apps/mypage/verificationEmail.html b/src/main/resources/templates/views/apps/mypage/verificationEmail.html new file mode 100644 index 0000000..89fc23a --- /dev/null +++ b/src/main/resources/templates/views/apps/mypage/verificationEmail.html @@ -0,0 +1,232 @@ + + + +
+
+ +
+

이메일 인증

+
+ +
+ +
+
+ + + + +
+

+ 이메일 주소 +

+
+
+ +
+
+

+ 위 이메일 주소로 인증코드가 발송됩니다. +

+
+ + + + + + + + +
+ 취소 + + +
+
+
+ +
+
+
+ + + + + diff --git a/src/main/resources/weblogic.xml b/src/main/resources/weblogic.xml index 7784cb1..7b95551 100644 --- a/src/main/resources/weblogic.xml +++ b/src/main/resources/weblogic.xml @@ -13,6 +13,7 @@ org.apache.log4j.* org.apache.commons.logging.* +