diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..1a4c63c --- /dev/null +++ b/.cursorrules @@ -0,0 +1,139 @@ +# Cursor Rules for EAPIM Portal + +> 이 파일은 CLAUDE.md를 기반으로 합니다. 내용 수정 시 CLAUDE.md와 동기화해주세요. + +## 프로젝트 개요 + +광주은행 EAPIM Portal - 엔터프라이즈 API 포털 관리 시스템 +- Spring Boot 2.7.18, Java 8, Gradle 7.x +- Oracle 12c 이중 데이터베이스 (EMS, Gateway) +- Spring Security, JPA/Hibernate, MapStruct, Lombok + +## 빌드 & 실행 + +```bash +# gradlew 사용 금지 - offline gradle 사용 +gradle bootRun # 개발 환경 실행 +gradle bootWar # 실행 가능 WAR +gradle war # 서버용 WAR (JEUS/WebLogic) +gradle test # 테스트 +``` + +## 아키텍처 핵심 + +### 외부 모듈 의존성 +- elink-online-core-jpa: Gateway 데이터 모델 +- elink-portal-common: 공통 유틸리티 +- kjb-safedb: SafeDB 암호화 라이브러리 + +### 패키지 구조 (기능 모듈 방식) +``` +apps/ +├── user/ # 사용자 관리 +├── app/ # API 키 관리 +├── approval/ # 승인 워크플로우 +├── apis/ # API 카탈로그 +├── proxy/ # API 테스트 프록시 +└── ... +``` + +각 모듈: controller/ service/ repository/ dto/ mapper/ + +### 이중 DB +- EMS (Portal): jdbc/dsOBP_EMS, com.eactive.apim.portal.apps.* +- Gateway: jdbc/dsOBP_AGW, com.eactive.eai.data.entity.onl.* +- Atomikos JTA로 분산 트랜잭션 + +### 프로파일 +- dev: 개발 (192.168.240.177:1599, SQL 로깅) +- stage: 스테이징 (JNDI, proxy to inter-dapiwas01) +- prod: 운영 (JNDI, proxy to inter-apiwas00, 캐싱) + +## 개발 패턴 + +### 새 기능 모듈 +1. apps/ 하위에 패키지 생성 +2. controller/, service/, repository/, dto/, mapper/ 구조 +3. Entity는 AbstractAuditingEntity 확장 +4. DTO는 Lombok @Data +5. Mapper는 MapStruct +6. Service는 @Transactional +7. Thymeleaf 템플릿 추가 +8. application.yml의 pages 섹션에 라우트 설정 (또는 @Controller 직접 매핑) + +### 보안 +- config/PortalConfigSecurity.java +- PortalAuthenticationManager (커스텀 인증) +- CSRF, XSS 방어, 세션 고정 공격 방어 +- 역할 계층: ROLE_USER < ROLE_CORP_USER < ROLE_CORP_MANAGER + +### 트랜잭션 +- 기본: @Transactional (REQUIRED) +- 읽기 전용: @Transactional(readOnly = true) +- 다중 DB는 자동 처리 (Atomikos JTA) + +### 로깅 +- SLF4J with Lombok @Slf4j +- 로그 경로: /Log/App/eapim/ +- 개발 환경만 SQL 로깅 + +### 쿼리 +- 우선: Spring Data JPA 메서드 +- 복잡한 쿼리: QueryDSL + +## 주요 기능 + +### 사용자 관리 (apps/user/) +- 내부 사용자 (Staff), 기업 사용자 (Enterprise) +- 가입 → 이메일 인증 → 관리자 승인 → 활성화 +- 비밀번호 정책: 90일 만료, 복잡도 규칙 + +### API 키 관리 (apps/app/) +- 승인 워크플로우 +- 개발/운영 환경 지원 +- eLink Gateway ClientID/ClientSecret 연동 + +### 감사 추적 +- AbstractAuditingEntity (createdBy, createdDate, lastModifiedBy, lastModifiedDate) +- @Audited로 변경 이력 +- Spring Data Envers로 조회 + +## 중요 사항 + +### 한글 지원 +- Thymeleaf 템플릿 한글 +- DB 데이터 UTF-8 +- messages_ko.properties + +### Forward Proxy +- Dev: http://localhost:10000 +- Stage: http://inter-dapiwas01:10000 +- Prod: http://inter-apiwas00:10000 + +### 비밀번호 정책 (PortalUserValidator) +- 최소 8자, 대소문자+숫자+특수문자 +- 연속 문자 불가, 최근 3개 재사용 불가 +- 90일 만료 + +### 세션 +- 15분 타임아웃 +- 단일 세션 강제 +- Redis/Ehcache 클러스터링 (stage/prod) + +### 파일 업로드 +- 최대 8MB +- /Log/App/eapim/files/ +- XSS 스캐닝 + +## 배포 +- JEUS, WebLogic, GlassFish 6.3, Tomcat 지원 +- JNDI 설정 필수 (stage/prod) +- Docker 지원 + +## 관련 프로젝트 +- ../eapim-admin/ +- ../eapim-online/ +- ../kjb-eapim-sql/ + +--- +상세 내용은 CLAUDE.md 참조 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..35270b0 Binary files /dev/null and b/.github/copilot-instructions.md differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fd952b8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,456 @@ +# CLAUDE.md + +이 파일은 Claude Code(claude.ai/code)가 이 저장소의 코드를 작업할 때 참고하는 지침을 제공합니다. + +## 프로젝트 개요 + +**EAPIM Portal**은 광주은행을 위한 엔터프라이즈 API 포털 관리 시스템입니다. API 서비스 관리, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다. + +**기술 스택:** +- Spring Boot 2.7.18 with Spring MVC and Thymeleaf +- Java 8 (targetCompatibility: 1.8) +- Gradle 8.7 빌드 시스템 +- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스) +- Spring Security (커스텀 인증) +- JPA/Hibernate 5.6.15 with Envers (감사 추적) +- MapStruct (DTO 매핑), Lombok (보일러플레이트 제거) + +## 빌드 명령어 + +### 애플리케이션 실행 +- gradlew 사용 금지 - offline gradle 단독 실행 +- msi-gf63 컴퓨터에서는 /c/eactive/workspaces/shell-scripts/kjb-gradle.sh 로 gradle 실행 (jdk, gradle 등 환경 변수 세팅 후 gradle 실행하는 스크립트) + +```bash +# dev 프로파일로 실행 (기본값) +gradle bootRun + +# 특정 프로파일로 실행 +gradle bootRun --args='--spring.profiles.active=stage' +``` + +### 빌드 + +```bash +# Spring Boot 실행 가능 WAR 빌드 +gradle bootWar +# 출력: build/libs/eapim-portal-boot.war + +# 애플리케이션 서버용 표준 WAR 빌드 (JEUS/WebLogic) +gradle war +# 출력: build/libs/eapim-portal.war + +# 클린 후 빌드 +gradle clean build +``` + +### 테스트 + +```bash +# 모든 테스트 실행 +gradle test + +# 커버리지와 함께 테스트 실행 +gradle test --info + +# 특정 테스트 클래스 실행 +gradle test --tests "com.eactive.apim.portal.apps.user.AccountControllerTest" + +# 패턴에 매칭되는 테스트 실행 +gradle test --tests "*Controller*" +``` + +### 개발 + +```bash +# 현재 프로파일 확인 +gradle printProfile + +# 소스 세트 설정 확인 +gradle printSourceSets + +# Docker 이미지 빌드 +./build_docker.sh +``` + +## 아키텍처 + +### 멀티 모듈 구조 + +이 프로젝트는 Gradle composite build를 통해 3개의 외부 모듈에 의존합니다: + +1. **elink-online-core-jpa** (`../eapim-online/elink-online-core-jpa`) + - Gateway 데이터 모델 및 JPA 엔티티 + - API 명세 엔티티 및 리포지토리 제공 + - 패키지: `com.eactive.eai.data.entity.onl.*` + +2. **elink-portal-common** (`../elink-portal-common`) + - 공유 포털 유틸리티 및 공통 컴포넌트 + - 기본 리포지토리 구현, QueryDSL 지원 + - 공통 예외 핸들러 및 보안 유틸리티 + +3. **kjb-safedb** (`../kjb-safedb`) + - 광주은행 SafeDB 암호화 라이브러리 + - 데이터베이스 레벨 민감 데이터 암호화 + - 암호화 컬럼(사용자 비밀번호, API 키 등)에 필수 + +### 패키지 구조 + +코드는 기술 계층이 아닌 **기능 모듈**(수직 분할) 방식으로 구성됩니다: + +``` +com.eactive.apim.portal/ +├── apps/ # 기능 모듈 +│ ├── agreements/ # API 약관 +│ ├── apis/ # API 카탈로그 & 문서 +│ ├── apiservice/ # API 서비스 그룹핑 +│ ├── app/ # API 키 관리 +│ ├── approval/ # 승인 워크플로우 +│ ├── auth/ # 인증 +│ ├── community/ # FAQ, 공지사항, Q&A, 제휴문의 +│ ├── dashboard/ # 통계 & 분석 +│ ├── file/ # 파일 업로드/다운로드 +│ ├── login/ # 로그인/로그아웃 +│ ├── proxy/ # API 테스트용 Forward Proxy +│ ├── sample/ # 샘플 코드 생성 +│ └── user/ # 사용자 관리 +├── common/ # 공통 관심사 +├── config/ # Spring 설정 +└── gateway/ # Gateway DB 직접 접근 + └── data/ # Gateway 리포지토리 +``` + +각 기능 모듈은 일반적으로 다음을 포함합니다: +- `controller/` - Spring MVC 컨트롤러 (@Controller) +- `service/` - 비즈니스 로직 (@Service, @Transactional) +- `repository/` - Spring Data JPA 리포지토리 +- `dto/` - 데이터 전송 객체 (Lombok @Data) +- `mapper/` - MapStruct 인터페이스 (entity ↔ DTO 변환) + +### 이중 데이터베이스 아키텍처 + +애플리케이션은 두 개의 Oracle 데이터베이스를 사용합니다: + +1. **EMS Database** (Portal/Admin) + - JNDI: `jdbc/dsOBP_EMS` + - Schema: `EMSADM` + - Entities: `com.eactive.apim.portal.apps.*` + - 목적: 포털 사용자, 조직, API 키, 승인 + +2. **Gateway Database** (API Specs) + - JNDI: `jdbc/dsOBP_AGW` + - Schema: `AGWADM` + - Entities: `com.eactive.eai.data.entity.onl.*` + - 목적: API 명세, 서비스, 메시지 + +설정: `config/PortalDatasourceConfiguration.java` +- 각 데이터베이스별 별도 EntityManager +- Atomikos JTA를 통한 분산 트랜잭션 +- 트랜잭션 로그: `/Log/eapim/portal/` + +### 설정 프로파일 + +환경별 설정 파일: `src/main/resources/application-{profile}.yml` + +- **dev**: 개발 환경 (Oracle at 192.168.240.177:1599, DevTools 활성화, SQL 로깅) +- **stage**: 스테이징 환경 (JNDI 데이터소스, proxy to inter-dapiwas01) +- **prod**: 운영 환경 (JNDI 데이터소스, proxy to inter-apiwas00, 캐싱 활성화) +- **gf63**: 개인 개발 환경 +- **kjb_rinjae**: 개인 개발 환경 + +주요 설정: +- 세션 타임아웃: 15분 +- 파일 업로드 최대: 8MB +- 비밀번호 만료: 90일 +- 인증 토큰 TTL: 5분 (300초) +- 사용자 승인 필수: true + +## 주요 기능 및 비즈니스 로직 + +### 사용자 관리 (`apps/user/`) + +두 가지 사용자 유형: +1. **내부 사용자 (Staff)**: 포털 관리자 및 운영자 +2. **기업 사용자 (Enterprise)**: 외부 API 소비자 + +사용자 라이프사이클: +- 가입 → 이메일 인증 → 관리자 승인 → 활성화 +- 비밀번호 정책 적용 (90일 만료, 복잡도 규칙) +- 로그인 실패 시 계정 잠금 +- 휴면 계정 관리 + +### API 키 관리 (`apps/app/`) + +API 키는 승인 워크플로우와 함께 관리됩니다: +- 기업 관리자가 `AppRequestController`를 통해 키 요청 +- 관리자가 `ApprovalController`를 통해 승인/반려 +- 키는 개발/운영 환경 지원 +- 키는 eLink Gateway ClientID/ClientSecret과 연결 + +### 승인 워크플로우 (`apps/approval/`) + +다음 항목에 대한 범용 승인 시스템: +- 사용자 등록 승인 +- API 키 생성/수정 +- 제휴 신청 + +엔티티: `Approval`, `Approver`, `ApprovalStatus` (WAITING, APPROVED, REJECTED) + +### API 테스트 (`apps/proxy/`, `apps/sample/`) + +- Gateway 서버로의 Forward Proxy +- 요청/응답 로깅이 있는 API 테스터 +- 여러 언어의 샘플 코드 생성 +- API 문서를 위한 Swagger UI 통합 + +### 감사 추적 (Hibernate Envers) + +모든 엔티티는 자동 감사를 위해 `AbstractAuditingEntity`를 확장합니다: +- `createdBy`, `createdDate` +- `lastModifiedBy`, `lastModifiedDate` +- `@Audited` 어노테이션으로 변경 이력 관리 +- Spring Data Envers를 통한 이력 조회 + +## 공통 개발 패턴 + +### 새 기능 모듈 추가하기 + +1. `apps/` 하위에 패키지 생성 (예: `apps/newfeature/`) +2. `controller/`, `service/`, `repository/`, `dto/`, `mapper/`로 구조화 +3. `repository/entity/`에 JPA 엔티티 생성 (`AbstractAuditingEntity` 확장) +4. `dto/`에 Lombok `@Data`를 사용한 DTO 생성 +5. `mapper/`에 MapStruct 매퍼 인터페이스 생성 +6. `@Service`와 `@Transactional`을 사용한 서비스 계층 구현 +7. `@Controller`와 `@RequestMapping`을 사용한 컨트롤러 생성 +8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가 +9. `application.yml`의 `pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑) + +### MapStruct DTO 매핑 + +타입 안전한 DTO 변환을 위해 MapStruct 사용: + +```java +@Mapper(componentModel = "spring") +public interface UserMapper { + PortalUserDto toDto(PortalUser entity); + PortalUser toEntity(PortalUserDto dto); + List toDtoList(List entities); +} +``` + +생성된 구현체는 `build/generated/sources/annotationProcessor/java/main/`에 위치 + +### 보안 및 인증 + +보안 설정: `config/PortalConfigSecurity.java` +- 커스텀 인증 관리자: `PortalAuthenticationManager` +- `changeSessionId()`를 통한 세션 고정 공격 방어 +- 쿠키 기반 토큰을 사용한 CSRF 방어 +- Lucy 필터를 통한 XSS 방어 + +역할 계층: +``` +ROLE_USER → [ROLE_INQUIRY, ROLE_ACCOUNT] +ROLE_CORP_USER → [ROLE_INQUIRY, ROLE_APP, ROLE_ACCOUNT] +ROLE_CORP_MANAGER → [ROLE_API_KEY_REQUEST, ROLE_INQUIRY, ROLE_APP, + ROLE_ACCOUNT, ROLE_CORP_API, ROLE_DASHBOARD, + ROLE_USER_MANAGER] +``` + +### 데이터베이스 쿼리 + +Spring Data JPA 리포지토리 메서드 사용 권장: + +```java +public interface PortalUserRepository extends JpaRepository { + Optional findByEmail(String email); + + @Query("SELECT u FROM PortalUser u WHERE u.status = :status") + List findByStatus(@Param("status") UserStatus status); +} +``` + +복잡한 쿼리는 QueryDSL 사용: +```java +QPortalUser user = QPortalUser.portalUser; +return queryFactory.selectFrom(user) + .where(user.email.eq(email) + .and(user.status.eq(UserStatus.ACTIVE))) + .fetchOne(); +``` + +### 트랜잭션 관리 + +적절한 propagation과 함께 `@Transactional` 사용: +- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성) +- 읽기 전용 작업: 최적화를 위해 `@Transactional(readOnly = true)` +- 다중 데이터베이스: Atomikos JTA가 자동으로 분산 트랜잭션 처리 + +### 에러 처리 + +`common/exception/`의 글로벌 예외 처리: +- `@ControllerAdvice`가 적용된 `GlobalExceptionHandler` +- 커스텀 예외는 `RuntimeException` 확장 +- Thymeleaf 에러 뷰 또는 AJAX 요청에는 JSON 반환 + +### 로깅 + +`application.yml`의 로깅 설정: +```yaml +logging: + file: + path: /Log/App/eapim/ + level: + com.eactive.apim.portal: DEBUG + org.hibernate.SQL: DEBUG (개발 환경만) +``` + +Lombok의 `@Slf4j`와 함께 SLF4J 사용: +```java +@Slf4j +@Service +public class UserService { + public void someMethod() { + log.debug("디버그 메시지"); + log.info("정보 메시지"); + log.error("에러 메시지", exception); + } +} +``` + +## 테스트 가이드 + +### 테스트 구조 + +`src/test/java/com/eactive/apim/portal/`의 테스트: +- `BaseWebTest` - Spring Boot 테스트 컨텍스트를 가진 기본 클래스 +- 컨트롤러 테스트는 `BaseWebTest` 확장 +- 통합 테스트는 `@SpringBootTest` 사용 +- 독립적인 컨트롤러 테스트는 `@WebMvcTest` 사용 + +### 일반적인 테스트 패턴 + +```java +@SpringBootTest +@AutoConfigureMockMvc +class UserControllerTest extends BaseWebTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Test + void testUserRegistration() throws Exception { + mockMvc.perform(post("/user/register") + .param("email", "test@example.com") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("user/register-success")); + } +} +``` + +### SafeDB 테스트 + +테스트에서는 기본적으로 SafeDB 암호화가 비활성화됩니다. 필요한 경우: +```bash +gradle test -Dsafedb=/path/to/safedb +``` + +## 중요 사항 + +### 한글 지원 + +이 포털은 한글을 광범위하게 사용합니다: +- Thymeleaf 템플릿은 한글 레이블 사용 +- 데이터베이스는 한글 데이터 포함 (UTF-8) +- 한글 메시지 프로퍼티 (`messages_ko.properties`) +- 한글 사용자 가이드 (`개발자포탈.md`) + +### Forward Proxy 설정 + +API 테스트는 Gateway로의 Forward Proxy 사용: +- Dev: `http://localhost:10000` +- Stage: `http://inter-dapiwas01:10000` +- Prod: `http://inter-apiwas00:10000` + +설정: `apps/proxy/service/ProxyService.java` + +### 비밀번호 정책 + +`PortalUserValidator`에서 적용: +- 최소 8자 +- 대문자, 소문자, 숫자, 특수문자 포함 필수 +- 연속된 문자 사용 불가 +- 최근 3개 비밀번호 재사용 불가 +- 90일 후 만료 + +### 세션 관리 + +- 사용자당 단일 세션 강제 +- 세션 타임아웃: 15분 +- 로그인 시 세션 고정 공격 방어 +- Redis/Ehcache를 통한 세션 클러스터링 (스테이징/운영) + +### 파일 업로드 제한 + +- 최대 파일 크기: 8MB +- `FileService`에서 허용 확장자 설정 +- 파일 저장 위치: `/Log/App/eapim/files/` +- 업로드 시 XSS 스캐닝 + +## 배포 + +### 애플리케이션 서버 지원 + +WAR 파일 호환 서버: +- **JEUS** (`jeus-web-dd.xml` 필요) +- **WebLogic** (`weblogic.xml` 필요) +- **Tomcat** (Spring Boot 내장) + +### JNDI 설정 필수 + +스테이징/운영 환경에서 JNDI 리소스 설정: +- `jdbc/dsOBP_EMS` → Portal 데이터베이스 +- `jdbc/dsOBP_AGW` → Gateway 데이터베이스 + +### 환경 변수 + +```bash +# 선택사항: 기본 설정 오버라이드 +JAVA_OPTS="-Xmx2g -Xms1g -Dspring.profiles.active=prod" +``` + +### Docker 배포 + +```bash +# WAR 빌드 +gradle bootWar + +# Docker 이미지 빌드 +./build_docker.sh + +# 컨테이너 실행 +docker run -p 8080:8080 \ + -e JAVA_OPTS="-Dspring.profiles.active=prod" \ + -v /Log:/Log \ + eapim-portal:latest +``` + +## 관련 모듈 + +다음 항목에 영향을 주는 변경 시: +- **Gateway API 명세**: `elink-online-core-jpa` 모듈 확인 +- **공통 유틸리티**: `elink-portal-common` 모듈 확인 +- **암호화**: `kjb-safedb` 모듈 확인 +- **Admin 포털**: `../eapim-admin/` 관련 프로젝트 +- **Online 포털**: `../eapim-online/` 관련 프로젝트 + +## 추가 자료 + +- 사용자 가이드 (한글): `개발자포탈.md` +- 빌드 스크립트: `build-gf63.sh`, `deploy_portal.sh`, `deploy_portal2.sh` +- Docker: `Dockerfile`, `build_docker.sh` +- SQL 스크립트: `../kjb-eapim-sql/` diff --git a/README.md b/README.md new file mode 100644 index 0000000..64b2e56 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# EAPIM Portal + +광주은행 엔터프라이즈 API 포털 관리 시스템 + +## 프로젝트 개요 + +**EAPIM Portal**은 광주은행의 API 서비스를 관리하고, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다. + +## 기술 스택 + +- Spring Boot 2.7.18 +- Java 8 +- Gradle 8.7 +- Oracle 19c +- Spring Security +- JPA/Hibernate 5.6.15 with Envers +- MapStruct, Lombok + +## 빌드 및 실행 + +```bash +# 개발 환경 실행 +gradle bootRun + +# WAR 빌드 +gradle bootWar + +# 테스트 +gradle test +``` + +**주의**: gradlew 사용 금지 - offline gradle 단독 실행 + +## 프로젝트 구조 + +``` +src/main/java/com/eactive/apim/portal/ +├── apps/ # 기능 모듈 (user, app, approval, apis, etc.) +├── common/ # 공통 관심사 +├── config/ # Spring 설정 +└── gateway/ # Gateway DB 접근 +``` + +## 외부 모듈 + +이 프로젝트는 다음 모듈에 의존합니다: +- `elink-online-core-jpa`: Gateway 데이터 모델 +- `elink-portal-common`: 공통 유틸리티 +- `kjb-safedb`: SafeDB 암호화 라이브러리 + +## AI 코딩 어시스턴트 지침 파일 + +이 프로젝트는 여러 AI 코딩 어시스턴트를 지원합니다. 각 도구는 다음 파일을 참조합니다: + +### 지침 파일 위치 + +| AI 도구 | 지침 파일 경로 | 용도 | +|---------|---------------|------| +| **Claude Code** | `CLAUDE.md` | 마스터 지침 파일 (한글) | +| **GitHub Copilot** | `.github/copilot-instructions.md` | Copilot용 지침 | +| **Cursor** | `.cursorrules` | Cursor용 간결한 규칙 | + +### 지침 파일 관리 방법 + +1. **마스터 파일**: `CLAUDE.md` + - 가장 상세한 프로젝트 지침을 포함 + - 한글로 작성되어 한국 개발팀이 이해하기 쉬움 + - 수정 시 이 파일을 먼저 업데이트 + +2. **동기화**: + ```bash + # CLAUDE.md 수정 후 다른 파일들 동기화 + cp CLAUDE.md .github/copilot-instructions.md + # .cursorrules는 간결한 버전이므로 필요시 수동 업데이트 + ``` + +3. **새로운 AI 도구 지원 추가**: + - Windsurf: `.windsurfrules` 파일 생성 + - 기타 도구: 해당 도구의 규칙 파일명 확인 후 생성 + +### 지침 파일에 포함된 내용 + +- 프로젝트 개요 및 기술 스택 +- 빌드 명령어 및 개발 환경 설정 +- 아키텍처 구조 (멀티 모듈, 이중 DB) +- 주요 기능 및 비즈니스 로직 +- 개발 패턴 및 코딩 컨벤션 +- 보안, 트랜잭션, 로깅 가이드 +- 테스트 패턴 +- 배포 방법 + +## 환경 설정 + +### 프로파일 + +- **dev**: 개발 환경 +- **stage**: 스테이징 환경 +- **prod**: 운영 환경 +- **gf63**: 개인 개발 환경 +- **kjb_rinjae**: 개인 개발 환경 + +### 데이터베이스 + +- **EMS Database**: Portal 사용자, 조직, API 키 +- **Gateway Database**: API 명세, 서비스 + +## 주요 기능 + +- 사용자 관리 (내부/기업) +- API 키 관리 및 승인 워크플로우 +- API 카탈로그 및 문서화 +- API 테스트 프록시 +- 감사 추적 (Hibernate Envers) + +## 배포 + +### 지원 서버 +- JEUS +- WebLogic +- Tomcat (내장) + +### Docker +```bash +./build_docker.sh +docker run -p 8080:8080 eapim-portal:latest +``` + +## 관련 프로젝트 + +- `../eapim-admin/` - Admin 포털 +- `../eapim-online/` - Online 포털 +- `../kjb-eapim-sql/` - SQL 스크립트 + +## 문서 + +- **프로젝트 상세 지침**: `CLAUDE.md` (한글) +- **사용자 가이드**: `개발자포탈.md` (한글) +- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh` + +## 라이선스 + +© 광주은행 (Kwangju Bank) diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java index d873d08..99285ed 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java @@ -22,10 +22,10 @@ public class AuthNumberServiceImpl implements AuthNumberService { private final AuthNumberGenerator generator; private final MessageSender messageSender; - @Value("${auth-ttl:300}") + @Value("${portal.auth-ttl:300}") private int authNumberExpirationTime; - @Value("${auth.resend.limit.seconds:30}") + @Value("${portal.auth.resend_limit_seconds:30}") private int resendLimitSeconds; @Autowired diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java index a57a97b..a3e49dc 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java @@ -46,6 +46,7 @@ public class AccountController { private final UserFacade userFacade; private final OrgRegisterFacade orgRegisterFacade; private final AgreementsFacade agreementsFacade; + private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade; @PostMapping("/confirm_password") @@ -326,4 +327,115 @@ public class AccountController { return "redirect:/mypage"; } } + + @GetMapping("/mypage/verification-email") + public String showVerificationEmailPage(Model model) { + try { + // 현재 로그인한 사용자 정보 가져오기 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + + if (currentUser == null) { + return "redirect:/login"; + } + + // 사용자 이메일 주소를 모델에 추가 + model.addAttribute("email", currentUser.getUsername()); + model.addAttribute("userId", currentUser.getId()); + + return "apps/mypage/verificationEmail"; + } catch (Exception e) { + logger.error("이메일 인증 페이지 로드 실패", e); + return "redirect:/"; + } + } + + @PostMapping("/mypage/send-verification-code") + public ResponseEntity sendVerificationCode( + @org.springframework.web.bind.annotation.RequestBody java.util.Map request, + HttpSession session) { + try { + String email = request.get("email"); + + // 현재 로그인한 사용자의 이메일과 일치하는지 확인 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + if (currentUser == null || !currentUser.getUsername().equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("유효하지 않은 요청입니다."); + return ResponseEntity.badRequest().body(response); + } + + // 이전 세션 데이터 정리 + session.removeAttribute("emailVerified"); + + // 이메일 인증 코드 발송 + ValidationResponse response = authFacade.requestAuth(email, "EMAIL"); + + if (response.isValid()) { + session.setAttribute("verificationEmail", email); + } + + return ResponseEntity.ok(response); + + } catch (Exception e) { + logger.error("이메일 인증 코드 발송 실패", e); + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 코드 발송에 실패했습니다."); + return ResponseEntity.status(500).body(response); + } + } + + @PostMapping("/mypage/verify-email-code") + public ResponseEntity verifyEmailCode( + @org.springframework.web.bind.annotation.RequestBody java.util.Map request, + HttpSession session) { + try { + String email = request.get("email"); + String code = request.get("code"); + + // 현재 로그인한 사용자의 이메일과 일치하는지 확인 + PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser(); + if (currentUser == null || !currentUser.getUsername().equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("유효하지 않은 요청입니다."); + return ResponseEntity.badRequest().body(response); + } + + // 세션에 저장된 이메일과 일치하는지 확인 + String sessionEmail = (String) session.getAttribute("verificationEmail"); + if (sessionEmail == null || !sessionEmail.equals(email)) { + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 요청된 이메일과 일치하지 않습니다."); + return ResponseEntity.badRequest().body(response); + } + + // 인증 코드 확인 + ValidationResponse response = authFacade.verifyAuthNumber(email, code); + + if (response.isValid()) { + session.setAttribute("emailVerified", true); + + // 사용자 상태를 ACTIVE로 변경 + userFacade.activateUserByEmail(email); + + // 이메일 인증 관련 세션 정보 제거 + session.removeAttribute("emailVerificationRequired"); + session.removeAttribute("redirectUrl"); + + logger.info("이메일 인증 완료: {}", email); + } + + return ResponseEntity.ok(response); + + } catch (Exception e) { + logger.error("이메일 인증 코드 확인 실패", e); + ValidationResponse response = new ValidationResponse(); + response.setValid(false); + response.setMessage("인증 코드 확인에 실패했습니다."); + return ResponseEntity.status(500).body(response); + } + } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java index 9ad4952..47cedb2 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java @@ -15,4 +15,6 @@ public interface UserFacade { void updateCorporateManager(PortalUserDTO portalUserDTO); void withdrawUser(String userId); + + void activateUserByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java index 33c8b38..66ce1bf 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java @@ -198,4 +198,23 @@ public class UserFacadeImpl implements UserFacade { throw new IllegalArgumentException("새 비밀번호와 확인 비밀번호가 일치하지 않습니다."); } } + + @Override + @Transactional + public void activateUserByEmail(String email) { + PortalUser user = portalUserService.findByEmailAddr(email); + + if (user == null) { + throw new IllegalArgumentException("사용자를 찾을 수 없습니다."); + } + + if (user.getUserStatus() == PortalUserEnums.UserStatus.ACTIVE) { + throw new IllegalArgumentException("이미 활성화된 계정입니다."); + } + + user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE); + portalUserService.save(user); + + log.info("사용자 이메일 인증 완료 - 활성화: {}", email); + } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java index ba95543..d8b17c5 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java @@ -49,6 +49,15 @@ public class PortalUserService { return portalUserRepository.findByLoginId(loginId); } + public PortalUser findByEmailAddr(String emailAddr) { + return portalUserRepository.findPortalUserByEmailAddr(emailAddr) + .orElseThrow(() -> new IllegalArgumentException("해당 이메일 주소의 사용자를 찾을 수 없습니다.")); + } + + public PortalUser save(PortalUser user) { + return portalUserRepository.save(user); + } + public boolean existsByLoginId(String loginId) { return portalUserRepository.existsByLoginId(loginId); } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java index d0b3747..b9828a0 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java @@ -67,9 +67,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("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다."); diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java index df91d07..0806640 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java @@ -55,7 +55,11 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess HttpSession session = request.getSession(); // 세션에 상태 저장 - if (isDormantAccount(user)) { + if (isEmailVerificationRequired(user)) { + session.setAttribute("success", "이메일 인증이 완료되지 않았습니다. 이메일을 확인하여 인증을 완료해주세요."); + session.setAttribute("emailVerificationRequired", true); + session.setAttribute("redirectUrl", contextPath + "/mypage/verification-email"); + } else if (isDormantAccount(user)) { session.setAttribute("success", "90일 이상 미접속하여 계정이 잠금 처리되었습니다. 본인인증 후 이용해주세요."); session.setAttribute("dormantAccount", true); session.setAttribute("dormantLoginId", username); @@ -114,4 +118,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus()); } + private boolean isEmailVerificationRequired(PortalUser user) { + return PortalUserEnums.UserStatus.READY.equals(user.getUserStatus()); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 928b7d6..1bced55 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -75,6 +75,8 @@ portal: log-path: /Log/App/eapim/ auth-ttl: 300 + auth: + resend_limit_seconds: 30 user-approval: true password-expiration-days: 90 diff --git a/src/main/resources/logback-gf63.xml b/src/main/resources/logback-gf63.xml index a0220d0..27f5050 100644 --- a/src/main/resources/logback-gf63.xml +++ b/src/main/resources/logback-gf63.xml @@ -40,6 +40,7 @@ + diff --git a/src/main/resources/templates/views/apps/login/login.html b/src/main/resources/templates/views/apps/login/login.html index b2eb5da..0061ef3 100644 --- a/src/main/resources/templates/views/apps/login/login.html +++ b/src/main/resources/templates/views/apps/login/login.html @@ -145,10 +145,10 @@ } if (document.loginForm.message && document.loginForm.message.value) { if (document.loginForm.message.value.includes("이메일 인증이 완료되지 않았습니다")) { - console.log("이메일 재발송 팝업 트리거"); - let loginId = $('#loginId').val(); - console.log("Retrieved loginId:", loginId); - customPopups.showEmailResendPopup(document.loginForm.message.value, loginId); + console.log("이메일 인증 페이지 이동"); + location.href = '/mypage/verification-email' + // console.log("Retrieved loginId:", loginId); + // customPopups.showEmailResendPopup(document.loginForm.message.value, loginId); } else { customPopups.showAlert(document.loginForm.message.value); } diff --git a/src/main/resources/templates/views/apps/main/index.html b/src/main/resources/templates/views/apps/main/index.html index c6e38d8..30036cd 100644 --- a/src/main/resources/templates/views/apps/main/index.html +++ b/src/main/resources/templates/views/apps/main/index.html @@ -248,6 +248,23 @@ customPopups.showAlert(successMsg); } + // 이메일 인증 체크 + const emailVerificationRequired = [[${session.emailVerificationRequired}]]; + if (emailVerificationRequired) { + const emailVerifyMsg = /*[[${session.success}]]*/ ''; + const redirectUrl = /*[[${session.redirectUrl}]]*/ ''; + + console.log('emailVerifyMsg:', emailVerifyMsg); + console.log('redirectUrl:', redirectUrl); + + customPopups.showAlert(emailVerifyMsg); + $('#customAlertOkButton').off('click.custom').on('click.custom', function () { + customPopups.hideAlert('#customAlert'); + window.location.href = redirectUrl; + }); + return; // 다른 체크 중단 + } + // 장기미사용 계정 체크 const dormantAccount = [[${session.dormantAccount}]]; if (dormantAccount) { @@ -428,7 +445,7 @@ } }); }); - +G const hashtagItems = document.querySelectorAll('.hashtag ul li a'); const searchBox = document.getElementById('search-box'); diff --git a/src/main/resources/templates/views/apps/mypage/verificationEmail.html b/src/main/resources/templates/views/apps/mypage/verificationEmail.html new file mode 100644 index 0000000..85cdf6f --- /dev/null +++ b/src/main/resources/templates/views/apps/mypage/verificationEmail.html @@ -0,0 +1,246 @@ + + + +
+
+ +
+

이메일 인증

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

+ 이메일 주소 +

+
+
+ +
+
+

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

+
+ + + + + + + + +
+ 취소 +
+ +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + + + +