Merge remote-tracking branch 'origin/devs/ums' into main-design-layout
# Conflicts: # src/main/resources/templates/views/apps/login/login.html
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.
@@ -101,3 +101,4 @@ TODO.txt
|
||||
|
||||
*.lck
|
||||
*.log
|
||||
.claude
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
+13
-2
@@ -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
|
||||
```
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,6 @@ public class MessageSender {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported message type: " + msgType);
|
||||
}
|
||||
|
||||
messageHandlerService.publishEvent(code, recipient, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
|
||||
@@ -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("암호화 모듈 오류");
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ public interface UserFacade {
|
||||
void updateCorporateManager(PortalUserDTO portalUserDTO);
|
||||
|
||||
void withdrawUser(String userId);
|
||||
|
||||
void activateUserByEmail(String email);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object>() {{
|
||||
put("corpName", user.getPortalOrg().getOrgName());
|
||||
put("managerName", user.getUserName());
|
||||
}}
|
||||
);
|
||||
|
||||
userInvitationRepository.save(invitation);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class PortalAuthenticatedUser extends PortalUser implements UserDetails {
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
return !"Y".equalsIgnoreCase(this.getAccountLockYn());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<? extends Payload>[] payload() default {};
|
||||
|
||||
String password();
|
||||
|
||||
String loginId();
|
||||
|
||||
String mobile();
|
||||
|
||||
}
|
||||
+144
@@ -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<PasswordRuleForKjbank, Object> {
|
||||
|
||||
// 최소 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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> auditorProvider() {
|
||||
return new AuditorAwareImpl();
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
+9
-1
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, Set<String>> 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<String, Set<String>> printTablePrivileges(Connection conn) throws SQLException {
|
||||
LinkedHashMap<String, Set<String>> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,21 +1,82 @@
|
||||
<configuration scan="true" scanPeriod="5 seconds">
|
||||
<property name="LOG_PATH" value="/Log/App/eapim//${inst.Name:-devSvrUD}"/>
|
||||
|
||||
<appendar name="FILE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appendar>
|
||||
<property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
|
||||
<property name="PATTERN_DATE" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-15thread] %-5level %logger - %msg%n" />
|
||||
|
||||
|
||||
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder><pattern>${PATTERN_DATE}</pattern></encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/debug.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder><pattern>${PATTERN}</pattern></encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE_HIBERNATE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/debug-hibernate.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/debug-hibernate.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
<pattern>${PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE_REPORT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/debug-report.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/debug-report.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
|
||||
|
||||
<logger name="com.eactive.eapim" level="DEBUG" />
|
||||
<logger name="weblogic" level="INFO" />
|
||||
<logger name="org.springframework" level="info" />
|
||||
<logger name="_org.springframework" level="info" />
|
||||
|
||||
<!-- Hibernate SQL Logging -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.transaction" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.springframework.transaction.interceptor" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.springframework.orm.jpa" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="com.atomikos" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
</configuration>
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
|
||||
<logger name="com.eactive.apim" level="DEBUG" />
|
||||
<logger name="org.thymeleaf.templateparser" level="DEBUG" />
|
||||
|
||||
|
||||
<!-- Hibernate SQL Logging -->
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<configuration scan="true" scanPeriod="5 seconds">
|
||||
<property name="LOG_PATH" value="/eactive/logs/${inst.Name:-devSvr_undefined}"/>
|
||||
<property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
|
||||
<property name="PATTERN_DATE" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" />
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder><pattern>${PATTERN_DATE}</pattern></encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/root.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/root.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder><pattern>${PATTERN}</pattern></encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE_HIBERNATE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/hibernate.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/hibernate.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE_REPORT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/report.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/report.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="FILE"/>
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
|
||||
<logger name="com.eactive.eapim" level="DEBUG" />
|
||||
|
||||
<!-- hibernate 로그 -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.transaction.internal.TransactionImpl" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.Transaction" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.springframework.transaction" level="TRACE" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="com.atomikos" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="FILE_HIBERNATE" />
|
||||
</logger>
|
||||
<logger name="com.eactive.apim.portal.spring.DatabaseSessionVerifier" level="debug">
|
||||
<appender-ref ref="FILE_REPORT" />
|
||||
</logger>
|
||||
|
||||
</configuration>
|
||||
@@ -102,7 +102,7 @@
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
></logger>
|
||||
</logger>
|
||||
|
||||
<logger name="com.eactive.eapim" level="DEBUG"/>
|
||||
<logger name="kjb.safedb" level="DEBUG" additivity="false">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,372 +1,476 @@
|
||||
<!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/kjbank_index_layout}">
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_index_layout}">
|
||||
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="hero-section">
|
||||
<div class="hero-patterns">
|
||||
<div class="pattern-circle pattern-1"></div>
|
||||
<div class="pattern-circle pattern-2"></div>
|
||||
<div class="pattern-dots"></div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="badge-icon">🚀</span>
|
||||
<span>Wa bank 생태계에 오신 것을 환영합니다!</span>
|
||||
<section layout:fragment="contentFragment" class="content main">
|
||||
<script th:src="@{/js/htmx.min.js}"></script>
|
||||
<!-- main slick -->
|
||||
<div class="slider-container">
|
||||
<div class="slider m-only">
|
||||
<div class="slider_box">
|
||||
|
||||
<img th:src="@{/img/img_mainslick_m.png}" alt="슬라이드 1">
|
||||
<div class="slick_txt">
|
||||
<p>디지털 금융 리더 <span>케이뱅크</span>가<br/>새로운 비즈니스 기회를<br/>제공합니다.</p>
|
||||
</div>
|
||||
<h1 class="main-headline">
|
||||
<span class="headline-wa">Wa한</span> 금융 혁신,<br>
|
||||
여러분의 아이디어로 시작됩니다
|
||||
</h1>
|
||||
<h2 class="sub-headline">
|
||||
광주은행 오픈 API로 새로운 금융 서비스를 만들어보세요.<br>
|
||||
안전하고 빠르게, 그리고 쉽게 연동할 수 있습니다.
|
||||
</h2>
|
||||
<div class="hero-buttons">
|
||||
<a href="/apis" class="btn btn-primary">
|
||||
<i class="fas fa-code"></i> API 둘러보기
|
||||
</a>
|
||||
<a href="#get-started" class="btn btn-secondary">
|
||||
<i class="fas fa-play"></i> 5분 만에 시작하기
|
||||
</a>
|
||||
</div>
|
||||
<div class="slider_box">
|
||||
<img th:src="@{/img/img_mainslick2_m.png}" alt="슬라이드 2">
|
||||
<div class="slick_txt">
|
||||
<p>차별화된 <span>API와<br> 편리한 테스트 환경</span>을<br> 지원합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider_box">
|
||||
<img th:src="@{/img/img_mainslick3_m.png}" alt="슬라이드 3">
|
||||
<div class="slick_txt">
|
||||
<p><span>디지털 금융 혁신</span>을<br> 케이뱅크가<br> 함께합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API 검색 섹션 -->
|
||||
<section class="api-search-section">
|
||||
<div class="container">
|
||||
<div class="search-container">
|
||||
<form th:action="@{/apis}" method="get" class="search-form">
|
||||
<div class="search-wrapper">
|
||||
<input type="text" class="search-input" placeholder="원하는 API를 검색해보세요" id="apiSearchInput" name="keyword">
|
||||
<button class="search-button" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
<span>검색하기</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="hashtag-suggestions" th:if="${hashtags != null and not #lists.isEmpty(hashtags)}">
|
||||
<span class="hashtag-label">추천 검색어:</span>
|
||||
<a href="#" class="hashtag" th:each="tag : ${hashtags}"
|
||||
th:data-search="${tag}"
|
||||
th:text="${'#' + tag}">
|
||||
</a>
|
||||
<div class="slider pc-only">
|
||||
<div class="slider_box">
|
||||
<img th:src="@{/img/img_mainslick_pc.png}" alt="슬라이드 1">
|
||||
<div class="slick_txt">
|
||||
<p><span>케이뱅크</span>가 새로운 비즈니스<br> 기회를 제공합니다</p>
|
||||
</div>
|
||||
<!-- Fallback hashtags if no hashtags from controller -->
|
||||
<div class="hashtag-suggestions" th:unless="${hashtags != null and not #lists.isEmpty(hashtags)}">
|
||||
<span class="hashtag-label">추천 검색어:</span>
|
||||
<a href="#" class="hashtag" data-search="계좌조회">#계좌조회</a>
|
||||
<a href="#" class="hashtag" data-search="송금API">#송금API</a>
|
||||
<a href="#" class="hashtag" data-search="실시간결제">#실시간결제</a>
|
||||
<a href="#" class="hashtag" data-search="오픈뱅킹">#오픈뱅킹</a>
|
||||
</div>
|
||||
<div class="slider_box">
|
||||
<img th:src="@{/img/img_mainslick2_pc.png}" alt="슬라이드 2">
|
||||
<div class="slick_txt">
|
||||
<p>차별화된 <span>API와</span><br/>
|
||||
<span>편리한 테스트 환경</span>을<br/>지원합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider_box">
|
||||
<img th:src="@{/img/img_mainslick3_pc.png}" alt="슬라이드 3">
|
||||
<div class="slick_txt">
|
||||
<p><span>디지털 금융 혁신</span>을<br>케이뱅크가<br>함께합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<button class="slick-prev" aria-label="Previous" type="button">이전</button>
|
||||
<button class="slick-next" aria-label="Next" type="button">다음</button>
|
||||
<ul class="slick-dots">
|
||||
<li>
|
||||
<button type="button">1</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button">2</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button">3</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- // main slick -->
|
||||
|
||||
<section class="api-showcase">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="title-highlight">인기 API</span>로 바로 시작하세요
|
||||
</h2>
|
||||
<p class="section-subtitle">가장 많이 사용되는 API를 확인하고 바로 테스트해보세요</p>
|
||||
<div class="main-container">
|
||||
<div class="search_type">
|
||||
<form th:action="@{/apis}" id="searchForm" method="get">
|
||||
<div class="search-container">
|
||||
<input type="text" name="keyword" class="search-box" id="search-box" placeholder="어떤 API를 찾고 계신가요?">
|
||||
<button class="search-button" type="submit"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="hashtag">
|
||||
<ul>
|
||||
<li th:each="tag: ${hashtags}"><a href="#" th:text="${tag}">#인증</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="main_keyword">
|
||||
<div class="api_tit">
|
||||
<h2>가장 많이 찾는 API</h2>
|
||||
<p class="total_view">
|
||||
<a href="/apis">전체보기</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="api-grid">
|
||||
<!-- Dynamic API Cards from Controller (max 4) -->
|
||||
<div th:each="api, iterStat : ${apis}"
|
||||
th:if="${iterStat.index < 4}"
|
||||
class="api-card"
|
||||
th:classappend="${iterStat.index == 0} ? 'api-card-popular' : (${iterStat.index == 3} ? 'api-card-new' : '')">
|
||||
|
||||
<!-- Show badge for first (인기) and last (NEW) items -->
|
||||
<div th:if="${iterStat.index == 0}" class="api-badge">인기</div>
|
||||
<div th:if="${iterStat.index == 3}" class="api-badge new">NEW</div>
|
||||
|
||||
<!-- API Icon - using different icons based on index or API type -->
|
||||
<div class="api-icon">
|
||||
<!-- Icon selection based on API name or service type -->
|
||||
<i th:if="${api.apiName != null}"
|
||||
th:class="${#strings.containsIgnoreCase(api.apiName, '계좌') or #strings.containsIgnoreCase(api.apiName, '조회')} ? 'fas fa-wallet' :
|
||||
(${#strings.containsIgnoreCase(api.apiName, '송금') or #strings.containsIgnoreCase(api.apiName, '이체')} ? 'fas fa-exchange-alt' :
|
||||
(${#strings.containsIgnoreCase(api.apiName, '카드') or #strings.containsIgnoreCase(api.apiName, '결제')} ? 'fas fa-credit-card' :
|
||||
(${#strings.containsIgnoreCase(api.apiName, 'AI') or #strings.containsIgnoreCase(api.apiName, '분석')} ? 'fas fa-chart-line' :
|
||||
(${iterStat.index == 0} ? 'fas fa-wallet' :
|
||||
(${iterStat.index == 1} ? 'fas fa-exchange-alt' :
|
||||
(${iterStat.index == 2} ? 'fas fa-credit-card' : 'fas fa-chart-line'))))))"></i>
|
||||
</div>
|
||||
|
||||
<!-- API Name -->
|
||||
<h3 class="api-name" th:text="${api.apiName}">API 이름</h3>
|
||||
|
||||
<!-- API Description -->
|
||||
<p class="api-description" th:text="${api.apiSimpleDescription != null ? api.apiSimpleDescription : api.description}">API 설명</p>
|
||||
|
||||
<!-- API Features/Method Info -->
|
||||
<div class="api-features">
|
||||
<span class="feature-tag" th:if="${api.apiMethod != null}" th:text="${api.apiMethod}">GET</span>
|
||||
<span class="feature-tag" th:if="${api.service != null}" th:text="${api.service}">서비스</span>
|
||||
</div>
|
||||
|
||||
<!-- Test Link -->
|
||||
<a th:href="@{/apis/detail(id=${api.apiId})}" class="api-link">
|
||||
테스트하기 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="api_keyword">
|
||||
<ul class="keyword_list tab_list">
|
||||
<li>
|
||||
<a href="#"
|
||||
hx-get="/api_fragments"
|
||||
hx-target="#api_spec_list"
|
||||
hx-trigger="click"
|
||||
onclick="setActive(this)">전체<span class="number" th:text="${totalApiCount}"></span></a>
|
||||
</li>
|
||||
<li th:each="item : ${services}">
|
||||
<a href="#"
|
||||
th:hx-get="@{/api_fragments(id=${item.id})}"
|
||||
hx-target="#api_spec_list"
|
||||
hx-trigger="click"
|
||||
onclick="setActive(this)">
|
||||
[[${item.groupName}]]<span class="number" th:text="${item.apiGroupApiList.size()}"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="more_type m-only">
|
||||
<button class="showmore" type="button">
|
||||
<img th:src="@{/img/icon/icon_more_btn.png}" alt="더보기 버튼">
|
||||
</button>
|
||||
</div>
|
||||
<div class="more_type pc-only">
|
||||
<button class="showmore" type="button">
|
||||
<img th:src="@{/img/icon/icon_more_btn2.png}" alt="더보기 버튼">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback: Show static cards if no APIs from controller -->
|
||||
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-popular">
|
||||
<div class="api-badge">인기</div>
|
||||
<div class="api-icon">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<div class="api_info_container box_list" id="api_spec_list">
|
||||
<div class="api_info_box" th:each="api : ${apis}" th:data-href="@{/apis/detail(id=${api.apiId})}">
|
||||
<div class="api_list" th:data-href="@{/apis/detail(id=${api.apiId})}">
|
||||
<p class="txt1" th:text="${api.apiName}"></p>
|
||||
<p class="txt2" th:text="${api.apiSimpleDescription}"></p>
|
||||
</div>
|
||||
<h3 class="api-name">계좌 조회 API</h3>
|
||||
<p class="api-description">실시간 잔액 조회와 거래내역을 안전하게 확인</p>
|
||||
<div class="api-features">
|
||||
<span class="feature-tag">실시간</span>
|
||||
<span class="feature-tag">OAuth 2.0</span>
|
||||
<div class="api_img" style="height: 48px;">
|
||||
<a th:href="@{/apis/detail(id=${api.apiId})}" class="btn btn-primary">
|
||||
<img th:if="${#strings.isEmpty(api.mainIcon) == false}" th:src="${api.mainIcon}" alt="">
|
||||
</a>
|
||||
</div>
|
||||
<a href="#" class="api-link">
|
||||
테스트하기 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
|
||||
<div class="api-icon">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</div>
|
||||
<h3 class="api-name">송금 API</h3>
|
||||
<p class="api-description">간편하고 안전한 송금 서비스 구현</p>
|
||||
<div class="api-features">
|
||||
<span class="feature-tag">간편송금</span>
|
||||
<span class="feature-tag">실명확인</span>
|
||||
</div>
|
||||
<a href="#" class="api-link">
|
||||
테스트하기 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card">
|
||||
<div class="api-icon">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
</div>
|
||||
<h3 class="api-name">카드 결제 API</h3>
|
||||
<p class="api-description">온/오프라인 결제 솔루션 통합</p>
|
||||
<div class="api-features">
|
||||
<span class="feature-tag">PG연동</span>
|
||||
<span class="feature-tag">간편결제</span>
|
||||
</div>
|
||||
<a href="#" class="api-link">
|
||||
테스트하기 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div th:if="${apis == null or #lists.isEmpty(apis)}" class="api-card api-card-new">
|
||||
<div class="api-badge new">NEW</div>
|
||||
<div class="api-icon">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<h3 class="api-name">AI 신용평가 API</h3>
|
||||
<p class="api-description">머신러닝 기반 실시간 신용 분석</p>
|
||||
<div class="api-features">
|
||||
<span class="feature-tag">AI/ML</span>
|
||||
<span class="feature-tag">실시간</span>
|
||||
</div>
|
||||
<a href="#" class="api-link">
|
||||
테스트하기 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 정보 섹션 -->
|
||||
<section class="info-section">
|
||||
<div class="container">
|
||||
<div class="info-wrapper">
|
||||
<div class="image-container">
|
||||
<div class="image-placeholder">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>금융 혁신</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main_con">
|
||||
<p class="con_tit m-only"><img th:src="@{/img/img_con_tit.png}" alt="Kbank API PORTAL"></p>
|
||||
<p class="con_tit pc-only"><img th:src="@{/img/img_con_tit2.png}" alt="Kbank API PORTAL"></p>
|
||||
<p class="con_img m-only"><img th:src="@{/img/img_mask_group.png}" alt="Kbank API PORTAL"></p>
|
||||
<p class="con_img pc-only"><img th:src="@{/img/img_mask_group2.png}" alt="Kbank API PORTAL"></p>
|
||||
<p class="con_txt m-only">
|
||||
<span>Kbank API Portal</span>은 기업이 금융서비스를<br>
|
||||
편리하게 개발할 수 있도록 차별화된<br>
|
||||
API와 테스트환경을 제공합니다.<br>
|
||||
</p>
|
||||
<p class="con_txt pc-only">
|
||||
<span>Kbank API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록 차별화된 API와 테스트환경을 제공합니다.
|
||||
</p>
|
||||
<div class="main_btn">
|
||||
<a th:href="@{/intro}" class="common_btn_type_5"><span>처음 만나는 케이뱅크 API</span></a>
|
||||
<a th:href="@{/partnership}" class="common_btn_type_5 m_btn"><span>사업 제휴 문의</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-content">
|
||||
<p class="highlight-text">
|
||||
광주은행 <span class="text-accent">API Portal</span>은 기업이 금융서비스를 편리하게 개발할 수 있도록<br>
|
||||
차별화된 API와 테스트환경을 제공합니다.
|
||||
<div class="tiding_box">
|
||||
<div class="api_type">
|
||||
<div class="api_tiding">
|
||||
<div class="api_tit">
|
||||
<h2>새로운 케이뱅크 API 소식을<br> 만나보세요</h2>
|
||||
<p class="total_view">
|
||||
<a href="/portalnotice">전체보기</a>
|
||||
</p>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="#" class="action-btn">
|
||||
<span class="btn-number">1</span>
|
||||
<span class="btn-text">처음 만나는 광주뱅크 API</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
<div class="api_tiding_list">
|
||||
<ul>
|
||||
<li th:each="notice : ${portalNotices}">
|
||||
<a th:href="@{/portalnotice/detail(id=${notice.id})}">
|
||||
<p th:text="${notice.noticeSubject}"></p>
|
||||
</a>
|
||||
<span th:text="${#temporals.format(notice.createdDate, 'yyyy.MM.dd')}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api_info">
|
||||
<ul>
|
||||
<li class="img1">
|
||||
<a th:href="@{/faq_list}">
|
||||
<p>FAQ</p>
|
||||
<span>자주 묻는 질문과 답변을<br> 확인해보세요</span>
|
||||
<img th:src="@{/img/icon/icon_petals01.png}" alt="faq 이미지">
|
||||
</a>
|
||||
<a href="#" class="action-btn">
|
||||
<span class="btn-number">2</span>
|
||||
<span class="btn-text">사업 제휴 문의</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</li>
|
||||
<li class="img2">
|
||||
<a th:href="@{/inquiry}">
|
||||
<p>Q&A</p>
|
||||
<span>문의사항에<br> 친절하게 답변 드리겠습니다</span>
|
||||
<img th:src="@{/img/icon/icon_petals02.png}" alt="Q&A 이미지">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="img3">
|
||||
<a th:href="@{/partnership}">
|
||||
<p>사업제휴</p>
|
||||
<span>온라인 비즈니스 혁신을 위한<br> 사업 제안을 환영합니다</span>
|
||||
<img th:src="@{/img/icon/icon_petals03.png}" alt="사업제휴 이미지">
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 지원 센터 섹션 -->
|
||||
<section class="support-center">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
도움이 <span class="title-highlight">필요하신가요?</span>
|
||||
</h2>
|
||||
<p class="section-subtitle">다양한 방법으로 필요한 정보를 찾아보세요</p>
|
||||
</div>
|
||||
<div class="support-grid">
|
||||
<a th:href="@{/portalnotice}" class="support-card">
|
||||
<div class="card-icon">
|
||||
<i class="fas fa-bullhorn"></i>
|
||||
</div>
|
||||
<h3>공지사항</h3>
|
||||
<p>최신 업데이트와 중요한 소식을 확인하세요</p>
|
||||
<div class="card-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
<a th:href="@{/faq_list}" class="support-card">
|
||||
<div class="card-icon">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</div>
|
||||
<h3>자주 묻는 질문</h3>
|
||||
<p>자주 묻는 질문과 답변을 빠르게 찾아보세요</p>
|
||||
<div class="card-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
<a th:href="@{/inquiry}" class="support-card">
|
||||
<div class="card-icon">
|
||||
<i class="fas fa-comments"></i>
|
||||
</div>
|
||||
<h3>문의하기</h3>
|
||||
<p>추가 도움이 필요하시면 직접 문의해주세요</p>
|
||||
<div class="card-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
<div class="api_count">
|
||||
<div class="api_total_box">
|
||||
<p class="txt m-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API</em>와<br> 함께하고 있습니다</p>
|
||||
<p class="txt pc-only">우리곁의 수많은 서비스들이<br> <em>케이뱅크API와 함께</em>하고 있습니다</p>
|
||||
<div class="api_total">
|
||||
<ul>
|
||||
<li>
|
||||
<img th:src="@{/img/icon/icon_api01.png}" alt="서비스 이용 수">
|
||||
<div class="api_num">
|
||||
<p th:text="${serviceCount}"></p>
|
||||
<span>서비스 이용</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<img th:src="@{/img/icon/icon_api02.png}" alt="API 이용건수">
|
||||
<div class="api_num">
|
||||
<p th:text="${selectedApiCount}"></p>
|
||||
<span>API 이용</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<img th:src="@{/img/icon/icon_api03.png}" alt="API 활용기업">
|
||||
<div class="api_num">
|
||||
<p th:text="${approvedOrgCount}"></p>
|
||||
<span>API 활용 기업</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- API 활용 현황 섹션 -->
|
||||
<section class="api-stats-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
우리곁의 수많은 서비스들이<br>
|
||||
<span class="title-highlight">광주은행 API</span>와 함께하고 있습니다
|
||||
</h2>
|
||||
</div>
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-store"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" th:attr="data-target=${serviceCount != null ? serviceCount : 0}" th:text="${serviceCount != null ? serviceCount : 0}">0</div>
|
||||
<div class="stat-label">서비스 이용 수</div>
|
||||
<div class="stat-description">다양한 분야의 서비스가 활용중</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" th:attr="data-target=${totalApiCount != null ? totalApiCount : 0}" th:text="${totalApiCount != null ? totalApiCount : 0}">0</div>
|
||||
<div class="stat-label">API 이용 건수</div>
|
||||
<div class="stat-description">일일 평균 API 호출 수</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-building"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" th:attr="data-target=${approvedOrgCount != null ? approvedOrgCount : 0}" th:text="${approvedOrgCount != null ? approvedOrgCount : 0}">0</div>
|
||||
<div class="stat-label">API 활용 기업</div>
|
||||
<div class="stat-description">혁신적인 파트너사와 함께</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
<div id="main_bg"></div>
|
||||
</section>
|
||||
</body>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
// 숫자 카운트업 애니메이션
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const observerOptions = {
|
||||
threshold: 0.5,
|
||||
rootMargin: '0px 0px -100px 0px'
|
||||
};
|
||||
|
||||
const animateNumbers = (entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const numbers = entry.target.querySelectorAll('.stat-number');
|
||||
|
||||
numbers.forEach(number => {
|
||||
const target = parseInt(number.getAttribute('data-target'));
|
||||
const duration = 2000; // 2초
|
||||
const increment = target / (duration / 16);
|
||||
let current = 0;
|
||||
|
||||
const updateNumber = () => {
|
||||
current += increment;
|
||||
if (current < target) {
|
||||
number.textContent = Math.floor(current).toLocaleString('ko-KR');
|
||||
requestAnimationFrame(updateNumber);
|
||||
} else {
|
||||
number.textContent = target.toLocaleString('ko-KR');
|
||||
}
|
||||
};
|
||||
|
||||
updateNumber();
|
||||
});
|
||||
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(animateNumbers, observerOptions);
|
||||
const statsSection = document.querySelector('.api-stats-section');
|
||||
|
||||
if (statsSection) {
|
||||
observer.observe(statsSection);
|
||||
<script th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
// 기존 success 메시지 처리
|
||||
var successMsg = [[${success}]];
|
||||
console.log("Success message:", successMsg);
|
||||
if (successMsg) {
|
||||
console.log("Showing alert");
|
||||
customPopups.showAlert(successMsg);
|
||||
}
|
||||
|
||||
// API 검색 기능
|
||||
const searchInput = document.getElementById('apiSearchInput');
|
||||
const searchForm = document.querySelector('.search-form');
|
||||
const hashtags = document.querySelectorAll('.hashtag');
|
||||
// 이메일 인증 체크
|
||||
const emailVerificationRequired = [[${session.emailVerificationRequired}]];
|
||||
if (emailVerificationRequired) {
|
||||
const emailVerifyMsg = /*[[${session.success}]]*/ '';
|
||||
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
|
||||
|
||||
// 해시태그 클릭 이벤트
|
||||
hashtags.forEach(tag => {
|
||||
tag.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const searchTerm = this.getAttribute('data-search');
|
||||
searchInput.value = searchTerm;
|
||||
// 폼 제출
|
||||
if (searchForm) {
|
||||
searchForm.submit();
|
||||
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}]];
|
||||
if (dormantAccount) {
|
||||
const dormantMsg = /*[[${session.success}]]*/ '';
|
||||
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
|
||||
|
||||
console.log('dormantMsg:', dormantMsg);
|
||||
console.log('redirectUrl:', redirectUrl);
|
||||
|
||||
customPopups.showAlert(dormantMsg);
|
||||
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
|
||||
customPopups.hideAlert('#customAlert');
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
return; // 다른 체크 중단
|
||||
}
|
||||
|
||||
// 비밀번호 만료 체크 추가
|
||||
const passwordExpired = [[${session.passwordExpired}]];
|
||||
if (passwordExpired) {
|
||||
const pwdExpiredMsg = /*[[${session.success}]]*/ '';
|
||||
const formattedMsg = pwdExpiredMsg.replace(/\n/g, '<br>');
|
||||
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
|
||||
|
||||
console.log('pwdExpiredMsg:', pwdExpiredMsg);
|
||||
console.log('redirectUrl:', redirectUrl);
|
||||
|
||||
$('#customAlertMessage').html(formattedMsg);
|
||||
$('#customAlert').css('display','flex');
|
||||
|
||||
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
|
||||
customPopups.hideAlert('#customAlert');
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function setActive(element) {
|
||||
// Remove 'active' class from all links
|
||||
document.querySelectorAll('.keyword_list a').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
// Add 'active' class to clicked link
|
||||
element.classList.add('active');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const ITEMS_PER_PAGE = 4;
|
||||
let isExpanded = false;
|
||||
|
||||
// 더보기 버튼 초기화
|
||||
function initMoreButton() {
|
||||
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore');
|
||||
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore');
|
||||
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
|
||||
|
||||
// 5개 미만이면 더보기 버튼 숨기기
|
||||
const mobileMoreContainer = document.querySelector('.more_type.m-only');
|
||||
const pcMoreContainer = document.querySelector('.more_type.pc-only');
|
||||
|
||||
console.log('더보기 버튼 초기화:', {
|
||||
항목개수: apiInfoBoxes.length,
|
||||
보여질상태: apiInfoBoxes.length >= 5 ? 'block' : 'none'
|
||||
});
|
||||
|
||||
// 더보기 버튼 표시 여부 설정
|
||||
if (apiInfoBoxes.length < 5) {
|
||||
mobileMoreContainer.style.setProperty('display', 'none', 'important');
|
||||
pcMoreContainer.style.setProperty('display', 'none', 'important');
|
||||
} else {
|
||||
mobileMoreContainer.style.setProperty('display', 'block', 'important');
|
||||
pcMoreContainer.style.setProperty('display', 'block', 'important');
|
||||
}
|
||||
|
||||
// 초기 상태 설정
|
||||
updateApiListVisibility();
|
||||
|
||||
// 더보기 버튼 이벤트 리스너 설정
|
||||
[mobileMoreBtn, pcMoreBtn].forEach(btn => {
|
||||
if (btn) {
|
||||
btn.removeEventListener('click', handleMoreClick);
|
||||
btn.addEventListener('click', handleMoreClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMoreClick(e) {
|
||||
e.preventDefault();
|
||||
console.log('더보기 버튼 클릭');
|
||||
isExpanded = !isExpanded; // 상태 토글
|
||||
|
||||
// 버튼 이미지/텍스트 변경
|
||||
const mobileMoreBtn = document.querySelector('.more_type.m-only .showmore img');
|
||||
const pcMoreBtn = document.querySelector('.more_type.pc-only .showmore img');
|
||||
if (mobileMoreBtn) {
|
||||
mobileMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
|
||||
}
|
||||
if (pcMoreBtn) {
|
||||
pcMoreBtn.alt = isExpanded ? "접기 버튼" : "더보기 버튼";
|
||||
}
|
||||
|
||||
updateApiListVisibility();
|
||||
}
|
||||
|
||||
// API 리스트 표시 상태 업데이트
|
||||
function updateApiListVisibility() {
|
||||
const apiInfoBoxes = document.querySelectorAll('.api_info_box');
|
||||
const endIndex = isExpanded ? apiInfoBoxes.length : ITEMS_PER_PAGE;
|
||||
|
||||
console.log('업데이트 - API 목록:', {
|
||||
총개수: apiInfoBoxes.length,
|
||||
표시상태: isExpanded ? '전체표시' : '일부표시',
|
||||
표시할개수: endIndex
|
||||
});
|
||||
|
||||
apiInfoBoxes.forEach((box, index) => {
|
||||
box.style.display = index < endIndex ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// HTMX 로드 완료 이벤트 리스너
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'api_spec_list') {
|
||||
console.log('HTMX 컨텐츠 로드됨 - 페이지 초기화');
|
||||
isExpanded = false;
|
||||
initMoreButton();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSettle', function(evt) {
|
||||
const apiListItems = document.querySelectorAll('.api_list');
|
||||
apiListItems.forEach(function(item) {
|
||||
item.removeEventListener('click',null);
|
||||
item.addEventListener('click', function() {
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
const apiItems = document.querySelectorAll('.api_info_box');
|
||||
apiItems.forEach(function(item) {
|
||||
item.removeEventListener('click',null);
|
||||
item.addEventListener('click', function() {
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 초기 실행
|
||||
console.log('초기 실행');
|
||||
initMoreButton();
|
||||
|
||||
// All 카테고리를 기본으로 active 설정
|
||||
document.querySelector('.keyword_list li:first-child a').classList.add('active');
|
||||
|
||||
const apiListItems = document.querySelectorAll('.api_list');
|
||||
apiListItems.forEach(function(item) {
|
||||
item.addEventListener('click', function() {
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const apiItems = document.querySelectorAll('.api_info_box');
|
||||
apiItems.forEach(function(item) {
|
||||
item.removeEventListener('click',null);
|
||||
item.addEventListener('click', function() {
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
G
|
||||
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
|
||||
const searchBox = document.getElementById('search-box');
|
||||
|
||||
hashtagItems.forEach(function(item) {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Remove the # symbol if present and set the text in search box
|
||||
const searchText = this.textContent.replace('#', '').trim();
|
||||
searchBox.value = searchText;
|
||||
|
||||
document.getElementById("searchForm").submit();
|
||||
});
|
||||
});
|
||||
|
||||
const infoItems = document.querySelectorAll('.api_info ul li');
|
||||
infoItems.forEach(function (item) {
|
||||
item.style.cursor = 'pointer';
|
||||
item.addEventListener('click', function() {
|
||||
const link = this.querySelector('a');
|
||||
if (link) {
|
||||
location.href = link.getAttribute('href');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<!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>
|
||||
</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>
|
||||
@@ -13,6 +13,7 @@
|
||||
<package-name>org.apache.log4j.*</package-name>
|
||||
<package-name>org.apache.commons.logging.*</package-name>
|
||||
</prefer-application-packages>
|
||||
<!-- <prefer-web-inf-classes>true</prefer-web-inf-classes>-->
|
||||
</container-descriptor>
|
||||
|
||||
<session-descriptor>
|
||||
|
||||
Reference in New Issue
Block a user