로그인 시 이메일 인증 절차 페이지 완료
This commit is contained in:
+139
@@ -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 참조
|
||||||
Binary file not shown.
@@ -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<PortalUserDto> toDtoList(List<PortalUser> 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<PortalUser, Long> {
|
||||||
|
Optional<PortalUser> findByEmail(String email);
|
||||||
|
|
||||||
|
@Query("SELECT u FROM PortalUser u WHERE u.status = :status")
|
||||||
|
List<PortalUser> 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/`
|
||||||
@@ -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)
|
||||||
@@ -22,10 +22,10 @@ public class AuthNumberServiceImpl implements AuthNumberService {
|
|||||||
private final AuthNumberGenerator generator;
|
private final AuthNumberGenerator generator;
|
||||||
private final MessageSender messageSender;
|
private final MessageSender messageSender;
|
||||||
|
|
||||||
@Value("${auth-ttl:300}")
|
@Value("${portal.auth-ttl:300}")
|
||||||
private int authNumberExpirationTime;
|
private int authNumberExpirationTime;
|
||||||
|
|
||||||
@Value("${auth.resend.limit.seconds:30}")
|
@Value("${portal.auth.resend_limit_seconds:30}")
|
||||||
private int resendLimitSeconds;
|
private int resendLimitSeconds;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public class AccountController {
|
|||||||
private final UserFacade userFacade;
|
private final UserFacade userFacade;
|
||||||
private final OrgRegisterFacade orgRegisterFacade;
|
private final OrgRegisterFacade orgRegisterFacade;
|
||||||
private final AgreementsFacade agreementsFacade;
|
private final AgreementsFacade agreementsFacade;
|
||||||
|
private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/confirm_password")
|
@PostMapping("/confirm_password")
|
||||||
@@ -326,4 +327,115 @@ public class AccountController {
|
|||||||
return "redirect:/mypage";
|
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<ValidationResponse> sendVerificationCode(
|
||||||
|
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> 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<ValidationResponse> verifyEmailCode(
|
||||||
|
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ public interface UserFacade {
|
|||||||
void updateCorporateManager(PortalUserDTO portalUserDTO);
|
void updateCorporateManager(PortalUserDTO portalUserDTO);
|
||||||
|
|
||||||
void withdrawUser(String userId);
|
void withdrawUser(String userId);
|
||||||
|
|
||||||
|
void activateUserByEmail(String email);
|
||||||
}
|
}
|
||||||
@@ -198,4 +198,23 @@ public class UserFacadeImpl implements UserFacade {
|
|||||||
throw new IllegalArgumentException("새 비밀번호와 확인 비밀번호가 일치하지 않습니다.");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ public class PortalUserService {
|
|||||||
return portalUserRepository.findByLoginId(loginId);
|
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) {
|
public boolean existsByLoginId(String loginId) {
|
||||||
return portalUserRepository.existsByLoginId(loginId);
|
return portalUserRepository.existsByLoginId(loginId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ public class PortalAuthenticationManager implements AuthenticationManager {
|
|||||||
throw new DisabledException("사용자 승인 대기중입니다. ");
|
throw new DisabledException("사용자 승인 대기중입니다. ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
|
// 25.11.13 - 이메일 인증 로그인은 허용
|
||||||
throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
|
// if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
|
||||||
}
|
// throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
|
||||||
|
// }
|
||||||
|
|
||||||
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.ADMINBLOCK)) {
|
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.ADMINBLOCK)) {
|
||||||
throw new DisabledException("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다.");
|
throw new DisabledException("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다.");
|
||||||
|
|||||||
+9
-1
@@ -55,7 +55,11 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
HttpSession session = request.getSession();
|
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("success", "90일 이상 미접속하여 계정이 잠금 처리되었습니다. 본인인증 후 이용해주세요.");
|
||||||
session.setAttribute("dormantAccount", true);
|
session.setAttribute("dormantAccount", true);
|
||||||
session.setAttribute("dormantLoginId", username);
|
session.setAttribute("dormantLoginId", username);
|
||||||
@@ -114,4 +118,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus());
|
return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isEmailVerificationRequired(PortalUser user) {
|
||||||
|
return PortalUserEnums.UserStatus.READY.equals(user.getUserStatus());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ portal:
|
|||||||
log-path: /Log/App/eapim/
|
log-path: /Log/App/eapim/
|
||||||
|
|
||||||
auth-ttl: 300
|
auth-ttl: 300
|
||||||
|
auth:
|
||||||
|
resend_limit_seconds: 30
|
||||||
|
|
||||||
user-approval: true
|
user-approval: true
|
||||||
password-expiration-days: 90
|
password-expiration-days: 90
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<logger name="com.eactive.apim" level="DEBUG" />
|
<logger name="com.eactive.apim" level="DEBUG" />
|
||||||
|
<logger name="org.thymeleaf.templateparser" level="DEBUG" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Hibernate SQL Logging -->
|
<!-- Hibernate SQL Logging -->
|
||||||
|
|||||||
@@ -145,10 +145,10 @@
|
|||||||
}
|
}
|
||||||
if (document.loginForm.message && document.loginForm.message.value) {
|
if (document.loginForm.message && document.loginForm.message.value) {
|
||||||
if (document.loginForm.message.value.includes("이메일 인증이 완료되지 않았습니다")) {
|
if (document.loginForm.message.value.includes("이메일 인증이 완료되지 않았습니다")) {
|
||||||
console.log("이메일 재발송 팝업 트리거");
|
console.log("이메일 인증 페이지 이동");
|
||||||
let loginId = $('#loginId').val();
|
location.href = '/mypage/verification-email'
|
||||||
console.log("Retrieved loginId:", loginId);
|
// console.log("Retrieved loginId:", loginId);
|
||||||
customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
|
// customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
|
||||||
} else {
|
} else {
|
||||||
customPopups.showAlert(document.loginForm.message.value);
|
customPopups.showAlert(document.loginForm.message.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,23 @@
|
|||||||
customPopups.showAlert(successMsg);
|
customPopups.showAlert(successMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이메일 인증 체크
|
||||||
|
const emailVerificationRequired = [[${session.emailVerificationRequired}]];
|
||||||
|
if (emailVerificationRequired) {
|
||||||
|
const emailVerifyMsg = /*[[${session.success}]]*/ '';
|
||||||
|
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
|
||||||
|
|
||||||
|
console.log('emailVerifyMsg:', emailVerifyMsg);
|
||||||
|
console.log('redirectUrl:', redirectUrl);
|
||||||
|
|
||||||
|
customPopups.showAlert(emailVerifyMsg);
|
||||||
|
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
|
||||||
|
customPopups.hideAlert('#customAlert');
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
});
|
||||||
|
return; // 다른 체크 중단
|
||||||
|
}
|
||||||
|
|
||||||
// 장기미사용 계정 체크
|
// 장기미사용 계정 체크
|
||||||
const dormantAccount = [[${session.dormantAccount}]];
|
const dormantAccount = [[${session.dormantAccount}]];
|
||||||
if (dormantAccount) {
|
if (dormantAccount) {
|
||||||
@@ -428,7 +445,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
G
|
||||||
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
|
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
|
||||||
const searchBox = document.getElementById('search-box');
|
const searchBox = document.getElementById('search-box');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
|
||||||
|
<body>
|
||||||
|
<section layout:fragment="contentFragment" class="content">
|
||||||
|
<div class="content_wrap">
|
||||||
|
<!-- 타이틀 -->
|
||||||
|
<div class="sub_title2" id="title-verification-email">
|
||||||
|
<h2 class="title add5">이메일 인증</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inner i_cs10 h_inner2">
|
||||||
|
|
||||||
|
<div class="form_type">
|
||||||
|
<form id="verificationEmailForm" role="form" name="verificationEmailForm" method="post">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||||
|
<input type="hidden" id="userId" name="userId" th:value="${userId}">
|
||||||
|
|
||||||
|
<!-- 이메일 주소 표시 (readonly) -->
|
||||||
|
<div class="info1 p_top2">
|
||||||
|
<p class="title w_tit">
|
||||||
|
<span class="dot">이메일 주소</span>
|
||||||
|
</p>
|
||||||
|
<div class="info_line">
|
||||||
|
<div class="info_box1 w_inp4">
|
||||||
|
<input type="email" id="email" name="email" class="common_input_type_1"
|
||||||
|
th:value="${email}" readonly style="background-color: #f5f5f5;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="info_txt" style="margin-top: 10px; color: #666;">
|
||||||
|
위 이메일 주소로 인증코드가 발송됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 인증코드 받기 버튼 -->
|
||||||
|
<div class="info1 pt28">
|
||||||
|
<div class="btn_check" style="width: 100%;">
|
||||||
|
<a class="common_btn_type_2 cbt btn_send_code" style="width: 100%;"><span>인증코드 받기</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 인증번호 입력 -->
|
||||||
|
<div class="info1 pt28 auth-code-container" id="authCodeContainer" style="display: none;">
|
||||||
|
<p class="title w_tit">
|
||||||
|
<span class="dot">인증코드 입력</span>
|
||||||
|
</p>
|
||||||
|
<div class="info_line">
|
||||||
|
<div class="info_box1 w_inp5">
|
||||||
|
<div class="certify_box">
|
||||||
|
<input type="text" id="authCode" class="common_input_type_1 w_input"
|
||||||
|
placeholder="이메일로 받은 인증코드를 입력하세요" maxlength="6" required>
|
||||||
|
<span class="certify_time" id="certify_time">05:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn_check">
|
||||||
|
<a class="common_btn_type_2 cbt btn_verify_code"><span>인증코드 확인</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 제출 버튼 -->
|
||||||
|
<div class="btn_application m_top">
|
||||||
|
<a type="button" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
|
||||||
|
<div class="btn_gap"></div>
|
||||||
|
<button type="button" class="common_btn_type_1 btn_complete" disabled><span>인증 완료</span></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <!– 탭 메뉴 –>-->
|
||||||
|
<!-- <div class="tab-wrap">-->
|
||||||
|
<!-- <div class="tabs">-->
|
||||||
|
<!-- <ul class="tab_nav tabm">-->
|
||||||
|
<!-- <li class="active"><a href="#tab1">이메일 인증</a></li>-->
|
||||||
|
<!-- </ul>-->
|
||||||
|
|
||||||
|
<!-- <!– 이메일 인증 폼 –>-->
|
||||||
|
<!-- <div class="tab active" id="tab1">-->
|
||||||
|
<!-- -->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<th:block layout:fragment="contentScript">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
let timerInterval;
|
||||||
|
let resendTimerInterval;
|
||||||
|
let isVerified = false;
|
||||||
|
|
||||||
|
// CSRF 토큰 설정
|
||||||
|
const csrfHeaderName = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
|
||||||
|
const csrfToken = /*[[${_csrf.token}]]*/ '';
|
||||||
|
|
||||||
|
// 서버 설정값
|
||||||
|
const resendLimitSeconds = /*[[${@environment.getProperty('portal.auth.resend_limit_seconds', '60')}]]*/ 60;
|
||||||
|
|
||||||
|
// 취소 버튼 클릭 시 메인 페이지로 이동
|
||||||
|
$('.btn_cancel').on('click', function() {
|
||||||
|
if (confirm('이메일 인증을 취소하고 메인 페이지로 이동하시겠습니까?')) {
|
||||||
|
location.href = '/';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 타이머 시작 함수 (인증 코드 유효 시간)
|
||||||
|
function startTimer(duration) {
|
||||||
|
let timer = duration;
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
|
||||||
|
timerInterval = setInterval(function () {
|
||||||
|
let minutes = parseInt(timer / 60, 10);
|
||||||
|
let seconds = parseInt(timer % 60, 10);
|
||||||
|
|
||||||
|
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||||
|
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||||
|
|
||||||
|
$('#certify_time').text(minutes + ":" + seconds);
|
||||||
|
|
||||||
|
if (--timer < 0) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
$('#certify_time').text("00:00").css('color', 'red');
|
||||||
|
customPopups.showAlert("인증 시간이 만료되었습니다. 인증코드를 다시 받아주세요.");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 재발송 타이머 시작 함수
|
||||||
|
function startResendTimer() {
|
||||||
|
let resendTimer = resendLimitSeconds;
|
||||||
|
const $button = $('.btn_send_code');
|
||||||
|
|
||||||
|
clearInterval(resendTimerInterval);
|
||||||
|
$button.prop('disabled', true);
|
||||||
|
|
||||||
|
resendTimerInterval = setInterval(function () {
|
||||||
|
if (resendTimer > 0) {
|
||||||
|
$button.find('span').text('재발송 가능 (' + resendTimer + '초)');
|
||||||
|
resendTimer--;
|
||||||
|
} else {
|
||||||
|
clearInterval(resendTimerInterval);
|
||||||
|
$button.prop('disabled', false).find('span').text('인증코드 재발송');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증코드 받기 버튼 클릭
|
||||||
|
$('.btn_send_code').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const email = $('#email').val();
|
||||||
|
const $button = $(this);
|
||||||
|
|
||||||
|
$button.prop('disabled', true).find('span').text('발송 중...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/mypage/send-verification-code',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ email: email }),
|
||||||
|
headers: {
|
||||||
|
[csrfHeaderName]: csrfToken
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
customPopups.showAlert('인증코드가 이메일로 발송되었습니다.');
|
||||||
|
$('#authCodeContainer').show();
|
||||||
|
startTimer(300); // 5분 타이머
|
||||||
|
startResendTimer(); // 재발송 타이머 시작 (1분)
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
const errorMsg = xhr.responseJSON?.error || '인증코드 발송에 실패했습니다.';
|
||||||
|
customPopups.showAlert(errorMsg);
|
||||||
|
$button.prop('disabled', false).find('span').text('인증코드 받기');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 인증코드 확인 버튼 클릭
|
||||||
|
$('.btn_verify_code').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const authCode = $('#authCode').val().trim();
|
||||||
|
const email = $('#email').val();
|
||||||
|
|
||||||
|
if (!authCode) {
|
||||||
|
customPopups.showAlert('인증코드를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authCode.length !== 6) {
|
||||||
|
customPopups.showAlert('6자리 인증코드를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/mypage/verify-email-code',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
code: authCode
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
[csrfHeaderName]: csrfToken
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
isVerified = true;
|
||||||
|
customPopups.showAlert('이메일 인증이 완료되었습니다!');
|
||||||
|
|
||||||
|
// 알림 확인 후 메인 페이지로 이동
|
||||||
|
$('#customAlertOkButton').off('click.emailVerify').on('click.emailVerify', function () {
|
||||||
|
customPopups.hideAlert('#customAlert');
|
||||||
|
location.href = '/';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
const errorMsg = xhr.responseJSON?.error || '인증코드가 일치하지 않습니다.';
|
||||||
|
customPopups.showAlert(errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 인증 완료 버튼 클릭
|
||||||
|
$('.btn_complete').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isVerified) {
|
||||||
|
customPopups.showAlert('이메일 인증을 먼저 완료해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메인 페이지로 이동
|
||||||
|
customPopups.showAlert('이메일 인증이 완료되었습니다. 메인 페이지로 이동합니다.');
|
||||||
|
setTimeout(function() {
|
||||||
|
location.href = '/';
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user