Merge branch 'jenkins_with_weblogic' into master
This commit is contained in:
+7
-1
@@ -101,4 +101,10 @@ TODO.txt
|
||||
|
||||
*.lck
|
||||
*.log
|
||||
.claude
|
||||
.claude
|
||||
*.report.md
|
||||
*.claude.md
|
||||
diff
|
||||
|
||||
*rinjae*
|
||||
*gf63*
|
||||
@@ -0,0 +1,173 @@
|
||||
# eapim-portal 로컬 개발 환경 실행 가이드
|
||||
|
||||
> Eclipse Terminal에서 `gradlew bootRun`으로 기동하는 방법
|
||||
|
||||
---
|
||||
|
||||
## 환경 구성 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| DB | 로컬 Docker Oracle 19c |
|
||||
| 포트 | 8080 |
|
||||
| 프로파일 | dev |
|
||||
| 실행 방법 | Eclipse Terminal → `gradlew.bat bootRun` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 사전 준비: 로그 디렉토리 생성
|
||||
|
||||
최초 1회만 실행 (없으면 기동 실패)
|
||||
|
||||
```cmd
|
||||
mkdir C:\Log\eapim\portal
|
||||
mkdir C:\Log\App\eapim
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Docker Oracle 기동
|
||||
|
||||
```cmd
|
||||
cd D:\eActive\workspaces\elink-djbank\docker\ekjb
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Oracle 완전 기동 확인 (처음 기동 시 3~5분 소요):
|
||||
|
||||
```cmd
|
||||
docker logs -f oracle
|
||||
```
|
||||
|
||||
`DATABASE IS READY TO USE!` 메시지 확인 후 다음 단계 진행.
|
||||
|
||||
---
|
||||
|
||||
## 3. Eclipse Terminal에서 bootRun 실행
|
||||
|
||||
### Eclipse Terminal 열기
|
||||
|
||||
`Window` → `Show View` → `Terminal`
|
||||
→ 우측 상단 **+** 클릭 → **Local Terminal** 선택
|
||||
|
||||
### 실행 명령어
|
||||
|
||||
```cmd
|
||||
cd D:\eActive\workspaces\elink-djbank\eapim-portal
|
||||
gradlew.bat bootRun
|
||||
```
|
||||
|
||||
### 기동 성공 확인
|
||||
|
||||
```
|
||||
##### PortalApplication Ready #####
|
||||
```
|
||||
|
||||
브라우저에서 `http://localhost:8080` 접속 확인.
|
||||
|
||||
---
|
||||
|
||||
## 4. 수정된 파일 목록
|
||||
|
||||
### 4-1. `src/main/resources/application-dev.yml`
|
||||
|
||||
DB 호스트를 원격 서버에서 로컬 Docker로 변경:
|
||||
|
||||
```yaml
|
||||
ems:
|
||||
datasource:
|
||||
jdbc-url: jdbc:oracle:thin:@//localhost:1599/DAPM # 변경
|
||||
username: EMSAPP
|
||||
password: elink1234
|
||||
|
||||
gateway:
|
||||
datasource:
|
||||
jdbc-url: jdbc:oracle:thin:@//localhost:1599/DAPM # 변경
|
||||
username: AGWAPP
|
||||
password: elink1234
|
||||
```
|
||||
|
||||
### 4-2. `build.gradle`
|
||||
|
||||
`bootRun` 태스크에 `sourceResources` 추가:
|
||||
|
||||
```groovy
|
||||
bootRun {
|
||||
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
|
||||
args = ["--spring.profiles.active=dev"]
|
||||
}
|
||||
```
|
||||
|
||||
> **이유**: `processResources`가 `application-dev.yml`을 빌드 출력에서 제외하도록 설정되어 있어,
|
||||
> `sourceResources sourceSets.main`을 추가해야 `bootRun` 실행 시 소스 디렉토리를 직접 참조함.
|
||||
|
||||
### 4-3. `src/main/resources/logback-spring.xml`
|
||||
|
||||
`dev` 프로파일에 CONSOLE appender 추가 (콘솔 로그 출력):
|
||||
|
||||
```xml
|
||||
<springProfile name="dev">
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="ROLLING"/>
|
||||
<appender-ref ref="CONSOLE"/> <!-- 추가 -->
|
||||
</root>
|
||||
</springProfile>
|
||||
```
|
||||
|
||||
### 4-4. `.classpath`
|
||||
|
||||
MapStruct 등 Gradle annotation processor 생성 소스를 Eclipse 소스 폴더로 추가:
|
||||
|
||||
```xml
|
||||
<classpathentry output="bin/main" kind="src"
|
||||
path="build/generated/sources/annotationProcessor/java/main">
|
||||
<attributes>
|
||||
<attribute name="gradle_scope" value="main"/>
|
||||
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
```
|
||||
|
||||
> **이유**: Eclipse는 Gradle annotation processing을 자동 실행하지 않으므로,
|
||||
> `gradlew.bat compileJava` 실행 후 생성된 소스 경로를 Eclipse 빌드 경로에 수동 추가 필요.
|
||||
|
||||
---
|
||||
|
||||
## 5. Annotation Processor 소스 재생성
|
||||
|
||||
소스 구조 변경(매퍼 추가 등) 시 재실행 필요:
|
||||
|
||||
```cmd
|
||||
cd D:\eActive\workspaces\elink-djbank\eapim-portal
|
||||
gradlew.bat compileJava
|
||||
```
|
||||
|
||||
이후 Eclipse에서 `Project` → `Clean` 실행.
|
||||
|
||||
---
|
||||
|
||||
## 6. Eclipse에서 직접 기동이 안 되는 이유
|
||||
|
||||
Eclipse는 참조 프로젝트(`elink-online-core-jpa`)의 test scope 의존성을 구분하지 않고
|
||||
런타임 클래스패스에 모두 포함시킵니다.
|
||||
|
||||
이로 인해 Spring Boot 버전 충돌 발생:
|
||||
|
||||
| 출처 | jar | 충돌 대상 |
|
||||
|------|-----|-----------|
|
||||
| `elink-online-core-jpa` (test) | `spring-boot-autoconfigure:2.6.15` | eapim-portal `2.7.18` |
|
||||
| `elink-online-core-jpa` (test) | `spring-core:5.3.27` | eapim-portal `5.3.31` |
|
||||
|
||||
→ `gradlew bootRun`은 Gradle의 dependency management(BOM)로 버전을 통일하므로 이 문제가 없음.
|
||||
|
||||
---
|
||||
|
||||
## 7. Docker Oracle 계정 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|----|
|
||||
| Host | localhost |
|
||||
| Port | 1599 |
|
||||
| SID | DAPM |
|
||||
| EMS 계정 | EMSAPP / elink1234 |
|
||||
| GW 계정 | AGWAPP / elink1234 |
|
||||
@@ -14,6 +14,46 @@
|
||||
- Spring Security (커스텀 인증)
|
||||
- JPA/Hibernate 5.6.15 with Envers (감사 추적)
|
||||
- MapStruct (DTO 매핑), Lombok (보일러플레이트 제거)
|
||||
- SASS/SCSS (CSS 전처리기) - 스타일 수정 시 반드시 SASS 파일 수정
|
||||
|
||||
**CSS/SASS 가이드:**
|
||||
- CSS 파일(`src/main/resources/static/css/main.css`)은 SASS에서 컴파일된 결과물
|
||||
- 스타일 수정 시 반드시 SASS 파일(`src/main/resources/static/sass/`)을 수정해야 함
|
||||
- CSS 파일 직접 수정 금지 - SASS 파일 수정 후 컴파일 필요
|
||||
- SASS 컴파일: `sass src/main/resources/static/sass/main.scss src/main/resources/static/css/main.css`
|
||||
|
||||
## Git 브랜치 전략
|
||||
|
||||
**저장소 정보:**
|
||||
- 기본 저장소: `ssh://git@192.168.240.178:18081/eapim/eapim-portal.git`
|
||||
- 대체 저장소: `https://git.eactive.synology.me:8090/kjb-eapim/eapim-portal.git`
|
||||
|
||||
**브랜치:**
|
||||
- **jenkins_with_weblogic**: Jenkins 빌드와 WebLogic 테스트를 위한 브랜치 (기본 개발 브랜치)
|
||||
- Jenkins CI/CD 파이프라인 설정 포함
|
||||
- WebLogic 배포 및 테스트 환경 설정
|
||||
- 일상적인 개발 작업은 이 브랜치에서 수행
|
||||
- 기능 개발, 버그 수정, 테스트 등 모든 개발 활동의 기본 브랜치
|
||||
|
||||
- **master**: 완벽히 동작하는 검증된 코드 저장소 (안정 브랜치)
|
||||
- 프로덕션 배포 가능한 안정적인 코드만 포함
|
||||
- jenkins_with_weblogic 브랜치에서 충분히 테스트된 코드만 병합
|
||||
- 직접 커밋 금지, Pull Request를 통한 병합만 허용
|
||||
|
||||
**브랜치 사용 가이드:**
|
||||
```bash
|
||||
# 개발 시작 시 jenkins_with_weblogic 브랜치에서 작업
|
||||
git checkout jenkins_with_weblogic
|
||||
|
||||
# 기능 개발 후 커밋
|
||||
git add .
|
||||
git commit -m "기능 설명"
|
||||
|
||||
# jenkins_with_weblogic 브랜치에 푸시
|
||||
git push origin jenkins_with_weblogic
|
||||
|
||||
# 충분한 테스트 완료 후 master로 병합 (Pull Request 사용)
|
||||
```
|
||||
|
||||
## 빌드 명령어
|
||||
|
||||
@@ -433,7 +473,7 @@ gradle bootWar
|
||||
./build_docker.sh
|
||||
|
||||
# 컨테이너 실행
|
||||
docker run -p 8080:8080 \
|
||||
docker run -p 30200:30200 \
|
||||
-e JAVA_OPTS="-Dspring.profiles.active=prod" \
|
||||
-v /Log:/Log \
|
||||
eapim-portal:latest
|
||||
|
||||
@@ -2,6 +2,55 @@
|
||||
|
||||
광주은행 엔터프라이즈 API 포털 관리 시스템
|
||||
|
||||
## 프로젝트 클론 (Git Clone)
|
||||
|
||||
아래 스크립트를 복사하여 한 번에 실행하면 모든 프로젝트를 클론합니다:
|
||||
|
||||
```bash
|
||||
# 변수 설정 (환경에 맞게 수정하세요)
|
||||
GIT_URL="ssh://git@192.168.240.178:18081/eapim"
|
||||
PROJECT_HOME="/c/eactive/workspaces/kjb-eapim-devPortal"
|
||||
DEFAULT_BRANCH="jenkins_with_weblogic"
|
||||
|
||||
# 프로젝트 홈 디렉토리 생성 및 이동
|
||||
mkdir -p "$PROJECT_HOME"
|
||||
cd "$PROJECT_HOME"
|
||||
|
||||
# 메인 프로젝트 클론
|
||||
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/kjb-eapim.git" kjb-eapim
|
||||
|
||||
# 의존 모듈 클론 (kjb-eapim 폴더와 동일 레벨)
|
||||
mkdir -p eapim-online
|
||||
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/elink-online-core-jpa.git" eapim-online/elink-online-core-jpa
|
||||
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/elink-portal-common.git" elink-portal-common
|
||||
git clone -b "$DEFAULT_BRANCH" "$GIT_URL/kjb-safedb.git" kjb-safedb
|
||||
|
||||
# 클론 후 폴더 구조 확인
|
||||
echo "=== 프로젝트 구조 ==="
|
||||
ls -la "$PROJECT_HOME"
|
||||
```
|
||||
|
||||
### 클론 후 폴더 구조
|
||||
|
||||
```
|
||||
$PROJECT_HOME/
|
||||
├── kjb-eapim/ # 메인 프로젝트
|
||||
│ └── eapim-portal/ # 이 프로젝트 (Portal)
|
||||
├── eapim-online/ # Gateway 데이터 모델
|
||||
│ └── elink-online-core-jpa/ # JPA 엔티티 모듈
|
||||
├── elink-portal-common/ # 공통 유틸리티
|
||||
└── kjb-safedb/ # SafeDB 암호화 라이브러리
|
||||
```
|
||||
|
||||
### 의존 관계 (settings.gradle 참조)
|
||||
|
||||
```
|
||||
eapim-portal
|
||||
├── elink-online-core-jpa → ../eapim-online/elink-online-core-jpa
|
||||
├── elink-portal-common → ../elink-portal-common
|
||||
└── kjb-safedb → ../kjb-safedb
|
||||
```
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
**EAPIM Portal**은 광주은행의 API 서비스를 관리하고, 사용자 등록, API 키 발급, 문서화, 테스트 기능을 제공하는 웹 기반 플랫폼입니다.
|
||||
@@ -16,23 +65,255 @@
|
||||
- JPA/Hibernate 5.6.15 with Envers
|
||||
- MapStruct, Lombok
|
||||
|
||||
## 초기 세팅 가이드
|
||||
|
||||
### 사전 요구사항
|
||||
|
||||
- JDK 8 이상
|
||||
- Gradle 8.7
|
||||
- Git
|
||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
||||
|
||||
### Git Bash에서 초기 세팅
|
||||
|
||||
```bash
|
||||
# ====================================
|
||||
# 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
||||
# ====================================
|
||||
export WORKSPACE_DIR="/c/eactive/workspaces/kjb-eapim"
|
||||
export PROJECT_BRANCH="jenkins_with_weblogic"
|
||||
|
||||
# Git Repository URL 설정
|
||||
# 기본: ssh://git@192.168.240.178:18081/eapim
|
||||
# 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
||||
export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
|
||||
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
|
||||
|
||||
# Gradle 명령어 설정
|
||||
# 일반 환경: gradle
|
||||
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
|
||||
export GRADLE_CMD="gradle"
|
||||
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
|
||||
# ====================================
|
||||
# 2. 작업 디렉토리 생성
|
||||
# ====================================
|
||||
mkdir -p $WORKSPACE_DIR
|
||||
cd $WORKSPACE_DIR
|
||||
|
||||
# ====================================
|
||||
# 3. eapim-portal 프로젝트 클론
|
||||
# ====================================
|
||||
git clone ${GIT_REPO_BASE}/eapim-portal.git
|
||||
cd eapim-portal
|
||||
|
||||
# 프로젝트 브랜치로 체크아웃
|
||||
git checkout $PROJECT_BRANCH
|
||||
|
||||
# ====================================
|
||||
# 4. eapim-online 프로젝트 클론 (elink-online-core-jpa 포함)
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR
|
||||
git clone ${GIT_REPO_BASE}/eapim-online.git
|
||||
cd eapim-online
|
||||
git checkout $PROJECT_BRANCH
|
||||
|
||||
# ====================================
|
||||
# 5. 의존 모듈 클론
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR
|
||||
|
||||
# elink-portal-common 클론
|
||||
git clone ${GIT_REPO_BASE}/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# kjb-safedb 클론
|
||||
cd $WORKSPACE_DIR
|
||||
git clone ${GIT_REPO_BASE}/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# ====================================
|
||||
# 6. 최종 폴더 구조 확인
|
||||
# ====================================
|
||||
# kjb-eapim/
|
||||
# ├── eapim-portal/
|
||||
# ├── eapim-online/
|
||||
# │ └── elink-online-core-jpa/
|
||||
# ├── elink-portal-common/
|
||||
# └── kjb-safedb/
|
||||
|
||||
# ====================================
|
||||
# 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR/eapim-portal
|
||||
|
||||
# Gradle 디펜던시 다운로드
|
||||
$GRADLE_CMD dependencies
|
||||
```
|
||||
|
||||
### Windows CMD에서 초기 세팅
|
||||
|
||||
```cmd
|
||||
REM ====================================
|
||||
REM 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
||||
REM ====================================
|
||||
set WORKSPACE_DIR=C:\eactive\workspaces\kjb-eapim
|
||||
set PROJECT_BRANCH=jenkins_with_weblogic
|
||||
|
||||
REM Git Repository URL 설정
|
||||
REM 기본: ssh://git@192.168.240.178:18081/eapim
|
||||
REM 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
||||
set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
|
||||
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
|
||||
|
||||
REM Gradle 명령어 설정
|
||||
REM 일반 환경: gradle
|
||||
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
set GRADLE_CMD=gradle
|
||||
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
|
||||
REM ====================================
|
||||
REM 2. 작업 디렉토리 생성
|
||||
REM ====================================
|
||||
if not exist "%WORKSPACE_DIR%" mkdir "%WORKSPACE_DIR%"
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
|
||||
REM ====================================
|
||||
REM 3. eapim-portal 프로젝트 클론
|
||||
REM ====================================
|
||||
git clone %GIT_REPO_BASE%/eapim-portal.git
|
||||
cd eapim-portal
|
||||
|
||||
REM 프로젝트 브랜치로 체크아웃
|
||||
git checkout %PROJECT_BRANCH%
|
||||
|
||||
REM ====================================
|
||||
REM 4. eapim-online 프로젝트 클론 (elink-online-core-jpa 포함)
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
git clone %GIT_REPO_BASE%/eapim-online.git
|
||||
cd eapim-online
|
||||
git checkout %PROJECT_BRANCH%
|
||||
|
||||
REM ====================================
|
||||
REM 5. 의존 모듈 클론
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
|
||||
REM elink-portal-common 클론
|
||||
git clone %GIT_REPO_BASE%/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master
|
||||
|
||||
REM kjb-safedb 클론
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
git clone %GIT_REPO_BASE%/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master
|
||||
|
||||
REM ====================================
|
||||
REM 6. 최종 폴더 구조 확인
|
||||
REM ====================================
|
||||
REM kjb-eapim\
|
||||
REM ├── eapim-portal\
|
||||
REM ├── eapim-online\
|
||||
REM │ └── elink-online-core-jpa\
|
||||
REM ├── elink-portal-common\
|
||||
REM └── kjb-safedb\
|
||||
|
||||
REM ====================================
|
||||
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%\eapim-portal"
|
||||
|
||||
REM Gradle 디펜던시 다운로드
|
||||
%GRADLE_CMD% dependencies
|
||||
```
|
||||
|
||||
### 환경별 설정 파일
|
||||
|
||||
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
|
||||
|
||||
- `src/main/resources/application-dev.yml` - 개발 환경
|
||||
- `src/main/resources/application-stage.yml` - 스테이징 환경
|
||||
- `src/main/resources/application-prod.yml` - 운영 환경
|
||||
- `src/main/resources/application-gf63.yml` - 개인 개발 환경
|
||||
|
||||
### 데이터베이스 설정
|
||||
|
||||
개발 환경(dev)에서는 `application-dev.yml`에 데이터베이스 설정이 포함되어 있습니다.
|
||||
스테이징/운영 환경에서는 JNDI 데이터소스가 필요합니다:
|
||||
|
||||
- `jdbc/dsOBP_EMS` - Portal 데이터베이스
|
||||
- `jdbc/dsOBP_AGW` - Gateway 데이터베이스
|
||||
|
||||
### 주의사항
|
||||
|
||||
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
||||
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
||||
- **SafeDB**: 암호화 기능이 필요한 경우 kjb-safedb 모듈 설정 확인
|
||||
|
||||
## 빌드 및 실행
|
||||
|
||||
### Gradle 명령어 설정
|
||||
|
||||
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
|
||||
|
||||
**Git Bash:**
|
||||
```bash
|
||||
# 일반 환경
|
||||
export GRADLE_CMD="gradle"
|
||||
|
||||
# msi-gf63 환경
|
||||
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
```
|
||||
|
||||
**Windows CMD:**
|
||||
```cmd
|
||||
REM 일반 환경
|
||||
set GRADLE_CMD=gradle
|
||||
|
||||
REM msi-gf63 환경
|
||||
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
```
|
||||
|
||||
### 빌드 및 실행 명령어
|
||||
|
||||
```bash
|
||||
# 개발 환경 실행
|
||||
gradle bootRun
|
||||
$GRADLE_CMD bootRun
|
||||
# 또는: %GRADLE_CMD% bootRun (CMD)
|
||||
|
||||
# WAR 빌드
|
||||
gradle bootWar
|
||||
$GRADLE_CMD bootWar
|
||||
|
||||
# 테스트
|
||||
gradle test
|
||||
$GRADLE_CMD test
|
||||
|
||||
# 컴파일만 수행
|
||||
$GRADLE_CMD classes
|
||||
|
||||
# 전체 빌드
|
||||
$GRADLE_CMD clean build
|
||||
```
|
||||
|
||||
**주의**: gradlew 사용 금지 - offline gradle 단독 실행
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
### 전체 프로젝트 구조
|
||||
```
|
||||
kjb-eapim/
|
||||
├── eapim-portal/ # Portal 웹 애플리케이션 (현재 프로젝트)
|
||||
├── eapim-online/ # Online 프로젝트
|
||||
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
|
||||
├── elink-portal-common/ # 공통 유틸리티
|
||||
└── kjb-safedb/ # SafeDB 암호화 라이브러리
|
||||
```
|
||||
|
||||
### eapim-portal 내부 구조
|
||||
```
|
||||
src/main/java/com/eactive/apim/portal/
|
||||
├── apps/ # 기능 모듈 (user, app, approval, apis, etc.)
|
||||
@@ -43,10 +324,153 @@ src/main/java/com/eactive/apim/portal/
|
||||
|
||||
## 외부 모듈
|
||||
|
||||
이 프로젝트는 다음 모듈에 의존합니다:
|
||||
- `elink-online-core-jpa`: Gateway 데이터 모델
|
||||
- `elink-portal-common`: 공통 유틸리티
|
||||
- `kjb-safedb`: SafeDB 암호화 라이브러리
|
||||
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
|
||||
- `eapim-online/elink-online-core-jpa`: Gateway 데이터 모델 (../eapim-online/elink-online-core-jpa)
|
||||
- `elink-portal-common`: 공통 유틸리티 (../elink-portal-common)
|
||||
- `kjb-safedb`: SafeDB 암호화 라이브러리 (../kjb-safedb)
|
||||
|
||||
## SASS / CSS 스타일 개발
|
||||
|
||||
### 개요
|
||||
|
||||
이 프로젝트는 **Dart Sass**를 사용하여 CSS를 관리합니다.
|
||||
`src/main/resources/static/css/main.css`는 SASS에서 컴파일된 결과물이므로 **직접 수정하면 안 됩니다.**
|
||||
스타일 변경은 반드시 `src/main/resources/static/sass/` 하위 파일을 수정하세요.
|
||||
|
||||
### 설치
|
||||
|
||||
```bash
|
||||
# Node.js가 설치된 상태에서
|
||||
npm install
|
||||
```
|
||||
|
||||
### 빌드 명령어
|
||||
|
||||
```bash
|
||||
# 1회 컴파일 (개발용, 들여쓰기 유지)
|
||||
npm run sass:build
|
||||
|
||||
# 1회 컴파일 (배포용, minified)
|
||||
npm run sass:build:minified
|
||||
|
||||
# 파일 변경 감지 후 자동 컴파일 (개발 중 권장)
|
||||
npm run sass:watch
|
||||
|
||||
# 빌드 + minified 동시 생성
|
||||
npm run build
|
||||
|
||||
# 단축 스크립트 (npm 없이 sass 직접 실행)
|
||||
./sass-build.sh
|
||||
```
|
||||
|
||||
### SASS 파일 구조
|
||||
|
||||
```
|
||||
src/main/resources/static/sass/
|
||||
├── main.scss # 진입점 - 모든 파일을 @use로 조합
|
||||
│
|
||||
├── abstracts/ # CSS 출력 없는 헬퍼 (변수, 믹스인 등)
|
||||
│ ├── _variables.scss # 색상, 타이포그래피, 그림자 변수
|
||||
│ ├── _mixins.scss # 재사용 믹스인
|
||||
│ └── _color-functions.scss # Dart Sass 3.0 color 유틸리티
|
||||
│
|
||||
├── base/ # 전역 기본 스타일
|
||||
│ ├── _reset.scss
|
||||
│ ├── _typography.scss
|
||||
│ ├── _animations.scss
|
||||
│ ├── _kjb-font.scss # 광주은행 전용 폰트
|
||||
│ └── _utilities.scss # 유틸리티 클래스
|
||||
│
|
||||
├── layout/ # 페이지 레이아웃 구조
|
||||
│ ├── _header.scss
|
||||
│ ├── _footer.scss
|
||||
│ ├── _grid.scss
|
||||
│ └── _container.scss
|
||||
│
|
||||
├── components/ # 재사용 UI 컴포넌트
|
||||
│ ├── _buttons.scss
|
||||
│ ├── _forms.scss
|
||||
│ ├── _modals.scss # customPopup 모달 스타일
|
||||
│ ├── _tables.scss
|
||||
│ ├── _badges.scss
|
||||
│ ├── _cards.scss
|
||||
│ ├── _navigation.scss
|
||||
│ ├── _pagination.scss
|
||||
│ └── ...
|
||||
│
|
||||
├── pages/ # 페이지별 스타일
|
||||
│ ├── _mypage.scss
|
||||
│ ├── _login.scss
|
||||
│ ├── _apikey-list.scss
|
||||
│ └── ...
|
||||
│
|
||||
├── themes/
|
||||
│ └── _dark.scss # 다크 테마 (예약)
|
||||
│
|
||||
└── vendors/
|
||||
└── _overrides.scss # 외부 라이브러리 스타일 덮어쓰기
|
||||
```
|
||||
|
||||
### 새 스타일 추가하는 방법
|
||||
|
||||
#### 1. 기존 컴포넌트/페이지 파일에 추가
|
||||
해당 `_*.scss` 파일을 직접 수정합니다.
|
||||
|
||||
```scss
|
||||
// components/_buttons.scss 예시
|
||||
.btn-withdrawal {
|
||||
color: $text-gray;
|
||||
border: 1px solid $border-gray;
|
||||
|
||||
&:hover {
|
||||
color: $accent-orange;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 새 페이지 스타일 파일 추가
|
||||
|
||||
```bash
|
||||
# 1. 파일 생성 (언더스코어 접두사 필수)
|
||||
touch src/main/resources/static/sass/pages/_new-page.scss
|
||||
|
||||
# 2. main.scss 에 @use 추가
|
||||
```
|
||||
|
||||
```scss
|
||||
// main.scss 에 추가
|
||||
@use 'pages/new-page' as *;
|
||||
```
|
||||
|
||||
#### 3. 변수 활용 예시
|
||||
|
||||
```scss
|
||||
// abstracts/_variables.scss 에 정의된 변수 사용
|
||||
.my-component {
|
||||
color: $primary-blue; // #0049b4 (광주은행 블루)
|
||||
background: $gray-bg; // #F8FAFC
|
||||
border: 1px solid $border-gray; // #E2E8F0
|
||||
box-shadow: $shadow-md;
|
||||
font-family: $font-family-primary;
|
||||
}
|
||||
```
|
||||
|
||||
### 컴파일 결과 확인
|
||||
|
||||
```bash
|
||||
# 컴파일 후 출력 파일 확인
|
||||
ls -lh src/main/resources/static/css/main.css
|
||||
ls -lh src/main/resources/static/css/main.min.css # minified
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- `src/main/resources/static/css/main.css` **직접 수정 금지** — SASS 재컴파일 시 덮어씌워짐
|
||||
- `style2.css` 는 레거시 파일(구 디자인 시스템)이며 `main.css` 와 **별도로 관리됨**
|
||||
- `@import` 대신 **`@use`** 사용 (Dart Sass 3.0 호환)
|
||||
- 파일명 앞에 언더스코어(`_`) 필수 — `_filename.scss` 형식이어야 단독 컴파일되지 않음
|
||||
|
||||
---
|
||||
|
||||
## AI 코딩 어시스턴트 지침 파일
|
||||
|
||||
@@ -127,16 +551,16 @@ docker run -p 8080:8080 eapim-portal:latest
|
||||
|
||||
## 관련 프로젝트
|
||||
|
||||
- `../eapim-admin/` - Admin 포털
|
||||
- `../eapim-online/` - Online 포털
|
||||
- `../kjb-eapim-sql/` - SQL 스크립트
|
||||
프로젝트 구조 (kjb-eapim 디렉토리 기준):
|
||||
- `eapim-portal/` - Portal 웹 애플리케이션 (현재 프로젝트)
|
||||
- `eapim-online/` - Online 프로젝트 (elink-online-core-jpa 포함)
|
||||
- `elink-portal-common/` - 공통 유틸리티 라이브러리
|
||||
- `kjb-safedb/` - SafeDB 암호화 라이브러리
|
||||
- `eapim-admin/` - Admin 포털 (선택사항)
|
||||
- `kjb-eapim-sql/` - SQL 스크립트 (선택사항)
|
||||
|
||||
## 문서
|
||||
|
||||
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
|
||||
- **사용자 가이드**: `개발자포탈.md` (한글)
|
||||
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
|
||||
|
||||
## 라이선스
|
||||
|
||||
© 광주은행 (Kwangju Bank)
|
||||
|
||||
+40
-1
@@ -14,6 +14,14 @@ targetCompatibility = "1.8"
|
||||
|
||||
def nexusUrl = "https://nexus.eactive.synology.me:8090"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
url "${nexusUrl}/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28"
|
||||
@@ -27,7 +35,7 @@ dependencies {
|
||||
|
||||
implementation project(':elink-online-core-jpa')
|
||||
implementation project(':elink-portal-common')
|
||||
implementation project(':kjb-safedb')
|
||||
// implementation project(':kjb-safedb')
|
||||
|
||||
implementation('org.springframework.boot:spring-boot-starter')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
|
||||
@@ -79,6 +87,11 @@ dependencies {
|
||||
|
||||
implementation 'net.bytebuddy:byte-buddy:1.14.5'
|
||||
|
||||
// Commons FileUpload (WAS 독립적인 multipart 처리)
|
||||
implementation 'commons-fileupload:commons-fileupload:1.5'
|
||||
implementation 'commons-io:commons-io:2.15.1'
|
||||
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
|
||||
testImplementation 'org.mockito:mockito-inline:3.11.2'
|
||||
@@ -110,6 +123,7 @@ bootRun {
|
||||
// 디버깅 사용시에는 아래 내용 주석 해제 하세요
|
||||
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
|
||||
|
||||
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
|
||||
args = ["--spring.profiles.active=dev"]
|
||||
}
|
||||
|
||||
@@ -132,6 +146,15 @@ compileJava {
|
||||
options.compilerArgs += ["-parameters"]
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
processResources {
|
||||
exclude { details ->
|
||||
details.file.name.startsWith('application-') &&
|
||||
details.file.name.endsWith('.yml') &&
|
||||
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
enabled = true
|
||||
@@ -158,4 +181,20 @@ task printSourceSets {
|
||||
println " Output dir : ${srcSet.output.classesDirs.asPath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CryptoCli 실행 task (bcrypt 지원)
|
||||
task cryptoCli(type: JavaExec) {
|
||||
mainClass = 'com.eactive.ext.kjb.safedb.CryptoCli'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
standardInput = System.in
|
||||
|
||||
// SafeDB 라이브러리 경로 추가 (리눅스)
|
||||
classpath += files("/safedb/JavaAPI/config")
|
||||
classpath += fileTree(dir: "/safedb/JavaAPI/lib", include: ["*.jar"])
|
||||
|
||||
// CLI 인자 전달: ./gradlew cryptoCli --args="bcrypt hash password123"
|
||||
if (project.hasProperty('args')) {
|
||||
args project.args.split('\\s+')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>광주은행 API Portal</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
/* Reset styles */
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* Custom styles */
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
background-color: #c3dfea;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
background-color: #c3dfea;
|
||||
}
|
||||
.header-section {
|
||||
padding: 28px 25px;
|
||||
}
|
||||
.content-section {
|
||||
background-color: #fdfdfd;
|
||||
margin: 0 25px;
|
||||
border-top: 4px solid #0049b4;
|
||||
min-height: 400px;
|
||||
}
|
||||
.content-inner {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.footer-section {
|
||||
padding: 25px;
|
||||
}
|
||||
.footer-notice {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #515151;
|
||||
line-height: 20px;
|
||||
}
|
||||
.footer-links {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #515151;
|
||||
}
|
||||
.footer-links a {
|
||||
color: #515151;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.footer-info {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #515151;
|
||||
}
|
||||
.footer-copyright {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #8c959f;
|
||||
}
|
||||
.divider {
|
||||
color: #515151;
|
||||
padding: 0 7px;
|
||||
}
|
||||
.logo-text {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
vertical-align: middle;
|
||||
padding-left: 8px;
|
||||
}
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 700px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.content-section {
|
||||
margin: 0 15px !important;
|
||||
}
|
||||
.content-inner {
|
||||
padding: 30px 20px !important;
|
||||
}
|
||||
.header-section,
|
||||
.footer-section {
|
||||
padding: 20px 15px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #c3dfea;">
|
||||
<!-- Email Wrapper -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #c3dfea;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!-- Email Container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="700" class="email-container" style="max-width: 700px; background-color: #c3dfea;">
|
||||
|
||||
<!-- Header Section -->
|
||||
<tr>
|
||||
<td class="header-section" style="padding: 28px 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="vertical-align: middle;">
|
||||
<!-- Logo Image - Replace src with actual logo URL -->
|
||||
<img src="email_logo_top.png" alt="광주은행" width="118" height="22" style="display: inline-block; vertical-align: middle;">
|
||||
<span class="logo-text" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 16px; font-weight: 700; color: #212529; vertical-align: middle; padding-left: 8px;">API Portal</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content Section -->
|
||||
<tr>
|
||||
<td style="padding: 0 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fdfdfd; border-top: 4px solid #0049b4;">
|
||||
<tr>
|
||||
<td class="content-inner" style="padding: 40px 30px; min-height: 400px;">
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- EMAIL CONTENT START -->
|
||||
<!-- ====================== -->
|
||||
|
||||
<!-- 이 영역에 실제 이메일 내용을 작성하세요 -->
|
||||
<!-- Example content structure: -->
|
||||
<!--
|
||||
<h1 style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 24px; color: #212529; margin: 0 0 20px 0;">
|
||||
이메일 제목
|
||||
</h1>
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 14px; color: #515151; line-height: 1.6; margin: 0 0 15px 0;">
|
||||
이메일 본문 내용이 들어갑니다.
|
||||
</p>
|
||||
-->
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- EMAIL CONTENT END -->
|
||||
<!-- ====================== -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<tr>
|
||||
<td class="footer-section" style="padding: 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<!-- Footer Logo and Notice -->
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td width="96" style="vertical-align: top; padding-right: 25px;">
|
||||
<!-- Footer Logo - Replace src with actual logo URL -->
|
||||
<img src="email_logo_bottom.png" alt="광주은행" width="96" height="18" style="display: block;">
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<p class="footer-notice" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; line-height: 20px; margin: 0;">
|
||||
본 메일은 발신 전용으로 회신되지 않습니다.<br>
|
||||
관련 문의사항은 고객센터 (1588-3388) 또는 홈페이지를 이용하시기 바랍니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<tr>
|
||||
<td style="padding: 10px 0 0 121px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td class="footer-links" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151;">
|
||||
<a href="https://www.kjbank.com" style="color: #515151; text-decoration: none;">은행소개</a>
|
||||
<span class="divider" style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/privacy" style="color: #515151; text-decoration: none;">개인정보처리방침</a>
|
||||
<span class="divider" style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/faq" style="color: #515151; text-decoration: none;">자주 묻는 질문</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<tr>
|
||||
<td style="padding: 8px 0 0 121px;">
|
||||
<p class="footer-info" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; margin: 0;">
|
||||
광주광역시 동구 제봉로225, 1층(대인동)
|
||||
<span class="divider" style="color: #515151; padding: 0 7px;">|</span>
|
||||
고객센터 : 1588-3388
|
||||
<span class="divider" style="color: #515151; padding: 0 7px;">|</span>
|
||||
사업자 등록번호 : ???-??-?????
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Copyright -->
|
||||
<tr>
|
||||
<td style="padding: 12px 0 0 121px;">
|
||||
<p class="footer-copyright" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #8c959f; margin: 0;">
|
||||
Copyright KWANGJU Bank. ALL Rights Reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Email Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>일회용 비밀번호 - 광주은행 API Portal</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
/* Reset styles */
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 700px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.content-section {
|
||||
margin: 0 15px !important;
|
||||
}
|
||||
.content-inner {
|
||||
padding: 30px 20px !important;
|
||||
}
|
||||
.header-section,
|
||||
.footer-section {
|
||||
padding: 20px 15px !important;
|
||||
}
|
||||
.password-box {
|
||||
width: 90% !important;
|
||||
}
|
||||
.login-button {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #ececec;">
|
||||
<!-- Email Wrapper -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #ececec;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!-- Email Container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="700" class="email-container" style="max-width: 700px; background-color: #ececec;">
|
||||
|
||||
<!-- Header Section -->
|
||||
<tr>
|
||||
<td class="header-section" style="padding: 14px 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="vertical-align: middle;">
|
||||
<!-- Logo Image -->
|
||||
<img src="email_logo_top.png" alt="광주은행" width="118" height="22" style="display: inline-block; vertical-align: middle;">
|
||||
<span style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 16px; font-weight: 700; color: #212529; vertical-align: middle; padding-left: 8px;">API Portal</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; text-align: right;">
|
||||
<!-- Key Icon -->
|
||||
<img src="email_icon_key.png" alt="비밀번호" width="74" height="60" style="display: inline-block; vertical-align: middle;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content Section -->
|
||||
<tr>
|
||||
<td style="padding: 0 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fdfdfd; border-top: 4px solid #0049b4;">
|
||||
<tr>
|
||||
<td class="content-inner" style="padding: 40px 30px;">
|
||||
|
||||
<!-- Title -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<h1 style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: #2f3b51; margin: 0;">
|
||||
[일회용 비밀번호 ]
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Greeting Message -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 20px; color: #000000; line-height: 36px; margin: 0; text-align: center;">
|
||||
<span style="font-weight: 700;">{{userName}}님</span> 안녕하세요.<br>
|
||||
광주은행 API Portal 비밀번호가 초기화 되어 일회용<br>
|
||||
비밀번호를 발급해 드립니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- One-Time Password Box -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" class="password-box" style="width: 438px; max-width: 100%;">
|
||||
<tr>
|
||||
<td align="center" style="background-color: #ffffff; border: 6px solid #e5e7eb; border-radius: 20px; padding: 20px 30px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 24px; font-weight: 700; color: #0049b4; margin: 0; letter-spacing: 2px;">
|
||||
{{otpPassword}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Login Guide Text -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 25px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 20px; font-weight: 700; color: #212529; margin: 0;">
|
||||
아래 버튼을 클릭하여 로그인을 진행해 주세요.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Login Button -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" class="login-button" style="background-color: #0049b4; border-radius: 12px; width: 220px;">
|
||||
<a href="{{loginUrl}}" target="_blank" style="display: block; padding: 18px 40px; font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 18px; font-weight: 700; color: #ffffff; text-decoration: none; text-align: center;">
|
||||
로그인
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<tr>
|
||||
<td class="footer-section" style="padding: 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<!-- Footer Logo and Notice -->
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td width="96" style="vertical-align: top; padding-right: 25px;">
|
||||
<!-- Footer Logo -->
|
||||
<img src="email_logo_bottom.png" alt="광주은행" width="96" height="18" style="display: block;">
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; line-height: 20px; margin: 0;">
|
||||
본 메일은 발신 전용으로 회신되지 않습니다.<br>
|
||||
관련 문의사항은 고객센터 (1588-3388) 또는 홈페이지를 이용하시기 바랍니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<tr>
|
||||
<td style="padding: 10px 0 0 121px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151;">
|
||||
<a href="https://www.kjbank.com" style="color: #515151; text-decoration: none;">은행소개</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/privacy" style="color: #515151; text-decoration: none;">개인정보처리방침</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/faq" style="color: #515151; text-decoration: none;">자주 묻는 질문</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<tr>
|
||||
<td style="padding: 8px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; margin: 0;">
|
||||
광주광역시 동구 제봉로225, 1층(대인동)
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
고객센터 : 1588-3388
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
사업자 등록번호 : ???-??-?????
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Copyright -->
|
||||
<tr>
|
||||
<td style="padding: 12px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #8c959f; margin: 0;">
|
||||
Copyright KWANGJU Bank. ALL Rights Reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Email Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>비밀번호 변경 완료 안내 - 광주은행 API Portal</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
/* Reset styles */
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 700px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.content-section {
|
||||
margin: 0 15px !important;
|
||||
}
|
||||
.content-inner {
|
||||
padding: 30px 20px !important;
|
||||
}
|
||||
.header-section,
|
||||
.footer-section {
|
||||
padding: 20px 15px !important;
|
||||
}
|
||||
.login-button {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #ececec;">
|
||||
<!-- Email Wrapper -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #ececec;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!-- Email Container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="700" class="email-container" style="max-width: 700px; background-color: #ececec;">
|
||||
|
||||
<!-- Header Section -->
|
||||
<tr>
|
||||
<td class="header-section" style="padding: 14px 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="vertical-align: middle;">
|
||||
<!-- Logo Image -->
|
||||
<img src="email_logo_top.png" alt="광주은행" width="118" height="22" style="display: inline-block; vertical-align: middle;">
|
||||
<span style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 16px; font-weight: 700; color: #212529; vertical-align: middle; padding-left: 8px;">API Portal</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; text-align: right;">
|
||||
<!-- Check/Shield Icon -->
|
||||
<img src="email_icon_check.png" alt="완료" width="64" height="64" style="display: inline-block; vertical-align: middle;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content Section -->
|
||||
<tr>
|
||||
<td style="padding: 0 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fdfdfd; border-top: 4px solid #0049b4;">
|
||||
<tr>
|
||||
<td class="content-inner" style="padding: 50px 30px;">
|
||||
|
||||
<!-- Title -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 40px;">
|
||||
<h1 style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: #2f3b51; margin: 0;">
|
||||
[ 비밀번호 변경 완료 안내 ]
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Greeting Message -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 50px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 20px; color: #000000; line-height: 36px; margin: 0; text-align: center;">
|
||||
<span style="font-weight: 700;">{{userName}}님</span> 안녕하세요.<br>
|
||||
광주은행 API Portal 비밀번호가 변경되어 안내 드립니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Login Guide Text -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 25px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 20px; font-weight: 700; color: #212529; margin: 0;">
|
||||
아래 버튼을 클릭하여 로그인을 진행해 주세요.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Login Button -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" class="login-button" style="background-color: #0049b4; border-radius: 12px; width: 220px;">
|
||||
<a href="{{loginUrl}}" target="_blank" style="display: block; padding: 18px 40px; font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 18px; font-weight: 700; color: #ffffff; text-decoration: none; text-align: center;">
|
||||
로그인
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<tr>
|
||||
<td class="footer-section" style="padding: 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<!-- Footer Logo and Notice -->
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td width="96" style="vertical-align: top; padding-right: 25px;">
|
||||
<!-- Footer Logo -->
|
||||
<img src="email_logo_bottom.png" alt="광주은행" width="96" height="18" style="display: block;">
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; line-height: 20px; margin: 0;">
|
||||
본 메일은 발신 전용으로 회신되지 않습니다.<br>
|
||||
관련 문의사항은 고객센터 (1588-3388) 또는 홈페이지를 이용하시기 바랍니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<tr>
|
||||
<td style="padding: 10px 0 0 121px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151;">
|
||||
<a href="https://www.kjbank.com" style="color: #515151; text-decoration: none;">은행소개</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/privacy" style="color: #515151; text-decoration: none;">개인정보처리방침</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/faq" style="color: #515151; text-decoration: none;">자주 묻는 질문</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<tr>
|
||||
<td style="padding: 8px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; margin: 0;">
|
||||
광주광역시 동구 제봉로225, 1층(대인동)
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
고객센터 : 1588-3388
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
사업자 등록번호 : ???-??-?????
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Copyright -->
|
||||
<tr>
|
||||
<td style="padding: 12px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #8c959f; margin: 0;">
|
||||
Copyright KWANGJU Bank. ALL Rights Reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Email Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,235 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>이메일 인증 - 광주은행 API Portal</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
/* Reset styles */
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* Responsive styles */
|
||||
@media screen and (max-width: 700px) {
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.content-section {
|
||||
margin: 0 15px !important;
|
||||
}
|
||||
.content-inner {
|
||||
padding: 30px 20px !important;
|
||||
}
|
||||
.header-section,
|
||||
.footer-section {
|
||||
padding: 20px 15px !important;
|
||||
}
|
||||
.verification-box {
|
||||
width: 90% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #c3dfea;">
|
||||
<!-- Email Wrapper -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #c3dfea;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!-- Email Container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="700" class="email-container" style="max-width: 700px; background-color: #c3dfea;">
|
||||
|
||||
<!-- Header Section -->
|
||||
<tr>
|
||||
<td class="header-section" style="padding: 14px 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="vertical-align: middle;">
|
||||
<!-- Logo Image -->
|
||||
<img src="email_logo_top.png" alt="광주은행" width="118" height="22" style="display: inline-block; vertical-align: middle;">
|
||||
<span style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 16px; font-weight: 700; color: #212529; vertical-align: middle; padding-left: 8px;">API Portal</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; text-align: right;">
|
||||
<!-- Lock Icon -->
|
||||
<img src="email_icon_lock.png" alt="보안" width="74" height="60" style="display: inline-block; vertical-align: middle;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content Section -->
|
||||
<tr>
|
||||
<td style="padding: 0 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #fdfdfd; border-top: 4px solid #0049b4;">
|
||||
<tr>
|
||||
<td class="content-inner" style="padding: 40px 30px;">
|
||||
|
||||
<!-- Title -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 40px;">
|
||||
<h1 style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 28px; font-weight: 700; color: #2f3b51; margin: 0;">
|
||||
[ 이메일 인증 ]
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Greeting Message -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 20px; color: #000000; line-height: 36px; margin: 0; text-align: center;">
|
||||
<span style="font-weight: 700;">{{userName}}님</span> 안녕하세요.<br>
|
||||
광주은행 API Portal에 가입해 주셔서 감사합니다.<br>
|
||||
아래 인증번호를 이메일 인증 페이지에 입력 해주세요.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Verification Code Box -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" class="verification-box" style="width: 438px; max-width: 100%;">
|
||||
<tr>
|
||||
<td align="center" style="background-color: #ffffff; border: 6px solid #e5e7eb; border-radius: 20px; padding: 20px 30px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 24px; font-weight: 700; color: #0049b4; margin: 0; letter-spacing: 2px;">
|
||||
{{verificationCode}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Verification Link -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 20px;">
|
||||
<a href="{{verificationUrl}}" style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 18px; color: #212529; text-decoration: none; word-break: break-all;">
|
||||
{{verificationUrl}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<tr>
|
||||
<td class="footer-section" style="padding: 25px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<!-- Footer Logo and Notice -->
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td width="96" style="vertical-align: top; padding-right: 25px;">
|
||||
<!-- Footer Logo -->
|
||||
<img src="email_logo_bottom.png" alt="광주은행" width="96" height="18" style="display: block;">
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; line-height: 20px; margin: 0;">
|
||||
본 메일은 발신 전용으로 회신되지 않습니다.<br>
|
||||
관련 문의사항은 고객센터 (1588-3388) 또는 홈페이지를 이용하시기 바랍니다.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<tr>
|
||||
<td style="padding: 10px 0 0 121px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151;">
|
||||
<a href="https://www.kjbank.com" style="color: #515151; text-decoration: none;">은행소개</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/privacy" style="color: #515151; text-decoration: none;">개인정보처리방침</a>
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
<a href="https://www.kjbank.com/faq" style="color: #515151; text-decoration: none;">자주 묻는 질문</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<tr>
|
||||
<td style="padding: 8px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #515151; margin: 0;">
|
||||
광주광역시 동구 제봉로225, 1층(대인동)
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
고객센터 : 1588-3388
|
||||
<span style="color: #515151; padding: 0 7px;">|</span>
|
||||
사업자 등록번호 : ???-??-?????
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Copyright -->
|
||||
<tr>
|
||||
<td style="padding: 12px 0 0 121px;">
|
||||
<p style="font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-size: 12px; color: #8c959f; margin: 0;">
|
||||
Copyright KWANGJU Bank. ALL Rights Reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Email Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 사업자등록번호 중복 체크 구현 작업 계획서
|
||||
|
||||
## 1. 현황 분석
|
||||
|
||||
### 문제점
|
||||
- 법인 등록 시 사업자등록번호(compRegNo) 중복 검증이 없음
|
||||
- `PortalOrgRepository.findByCompRegNo()` 메서드는 존재하나 사용되지 않음
|
||||
- 동일한 사업자등록번호로 여러 법인이 등록될 수 있는 상태
|
||||
|
||||
### 검증 시점
|
||||
1. **실시간 검증**: 사용자가 사업자등록번호 입력 시 `/check-business-number` API 호출
|
||||
2. **등록 시 검증**: 실제 법인 생성 시점 (`registerOrgFromDTOWithFile`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 수정 대상 파일
|
||||
|
||||
| 파일 경로 | 수정 내용 |
|
||||
|-----------|-----------|
|
||||
| `src/main/java/.../apps/user/service/PortalOrgService.java` | 사업자등록번호 중복 체크 메서드 추가 |
|
||||
| `src/main/java/.../apps/user/facade/OrgRegisterFacadeImpl.java` | `checkBusinessNumber()` 중복 체크 로직 추가, 등록 전 중복 체크 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 상세
|
||||
|
||||
### 3.1 PortalOrgService.java
|
||||
|
||||
```java
|
||||
// 추가할 메서드
|
||||
public boolean existsByCompRegNo(String compRegNo) {
|
||||
return portalOrgRepository.findByCompRegNo(compRegNo).isPresent();
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 OrgRegisterFacadeImpl.java
|
||||
|
||||
#### checkBusinessNumber 메서드 수정
|
||||
- 형식 검증 + 중복 검증 동시 수행
|
||||
- 중복 시 "이미 등록된 사업자등록번호입니다." 메시지 반환
|
||||
|
||||
```java
|
||||
@Override
|
||||
public ResponseEntity<ValidationResponse> checkBusinessNumber(String compRegNo) {
|
||||
// 1. 형식 검증
|
||||
boolean isValidFormat = validationService.isValidBusinessNumber(compRegNo);
|
||||
if (!isValidFormat) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "유효하지 않은 사업자 등록번호입니다."));
|
||||
}
|
||||
|
||||
// 2. 중복 검증
|
||||
boolean isDuplicate = portalOrgService.existsByCompRegNo(compRegNo);
|
||||
if (isDuplicate) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "이미 등록된 사업자등록번호입니다."));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new ValidationResponse(true, "사용 가능한 사업자 등록번호입니다."));
|
||||
}
|
||||
```
|
||||
|
||||
#### 법인 생성 메서드들에 중복 체크 추가
|
||||
- `registerNewCorporateUser()`
|
||||
- `convertExistingUserToCorporate()`
|
||||
- `registerNewCorporateUserWithExistingData()`
|
||||
|
||||
각 메서드에서 `portalOrgService.registerOrgFromDTOWithFile()` 호출 전 중복 체크 수행
|
||||
|
||||
---
|
||||
|
||||
## 4. 검증 흐름
|
||||
|
||||
```
|
||||
[사용자 입력]
|
||||
↓
|
||||
[실시간 검증] /check-business-number API
|
||||
├── 형식 검증 (10자리 숫자)
|
||||
└── 중복 검증 (DB 조회)
|
||||
↓
|
||||
[등록 요청]
|
||||
↓
|
||||
[등록 시 검증] registerOrgFromDTOWithFile 호출 전
|
||||
└── 중복 체크 (이중 방어)
|
||||
↓
|
||||
[법인 생성]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 기존 중복 데이터 처리
|
||||
|
||||
- 기존에 중복 등록된 데이터는 그대로 유지
|
||||
- 신규 등록 시에만 중복 체크 적용
|
||||
- 필요시 관리자 화면에서 중복 데이터 조회/정리 기능 별도 개발 가능
|
||||
|
||||
---
|
||||
|
||||
## 6. 테스트 시나리오
|
||||
|
||||
| 시나리오 | 예상 결과 |
|
||||
|----------|-----------|
|
||||
| 형식이 잘못된 사업자등록번호 입력 | "유효하지 않은 사업자 등록번호입니다." |
|
||||
| 이미 등록된 사업자등록번호 입력 | "이미 등록된 사업자등록번호입니다." |
|
||||
| 사용 가능한 사업자등록번호 입력 | "사용 가능한 사업자 등록번호입니다." |
|
||||
| 중복된 번호로 법인 등록 시도 | 등록 실패, 에러 메시지 표시 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 작업 완료
|
||||
|
||||
- [x] PortalOrgService에 existsByCompRegNo 메서드 추가
|
||||
- [x] OrgRegisterFacadeImpl.checkBusinessNumber 수정
|
||||
- [x] 법인 등록 메서드들에 중복 체크 로직 추가
|
||||
- [x] 빌드 검증 완료
|
||||
@@ -0,0 +1,165 @@
|
||||
# 네비게이션 바 디자인 Survey 기능 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
직원 대상 네비게이션 바 디자인 선호도 조사를 위한 UI 기능입니다.
|
||||
사용자가 A, B, C 등의 버튼을 클릭하면 실시간으로 네비게이션 바 디자인이 변경됩니다.
|
||||
|
||||
## 기능 활성화/비활성화
|
||||
|
||||
### PortalProperty 설정
|
||||
|
||||
DB의 `ptl_property` 테이블에서 `ui.design-survey` 값을 설정합니다.
|
||||
|
||||
```sql
|
||||
-- 활성화
|
||||
UPDATE ptl_property
|
||||
SET property_value = 'true'
|
||||
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
|
||||
|
||||
-- 비활성화
|
||||
UPDATE ptl_property
|
||||
SET property_value = 'false'
|
||||
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
|
||||
```
|
||||
|
||||
> 최초 페이지 로드 시 속성이 자동 생성됩니다 (기본값: `false`)
|
||||
|
||||
---
|
||||
|
||||
## 디자인 옵션 추가/수정 방법
|
||||
|
||||
### 파일 위치
|
||||
`src/main/resources/templates/views/fragment/kjbank/header_container.html`
|
||||
|
||||
### DESIGN_OPTIONS 배열
|
||||
|
||||
디자인 옵션은 JavaScript의 `DESIGN_OPTIONS` 배열에서 관리됩니다:
|
||||
|
||||
```javascript
|
||||
const DESIGN_OPTIONS = [
|
||||
{
|
||||
id: 'A', // 고유 식별자 (필수)
|
||||
label: 'A (현재)', // 버튼에 표시될 텍스트 (필수)
|
||||
isDefault: true, // 기본 선택 옵션 (선택, 하나만 true)
|
||||
styles: {} // 적용할 CSS 스타일 (필수)
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: 'B',
|
||||
styles: {
|
||||
'.logo-text': { 'font-size': '18px' },
|
||||
'.nav-link': { 'margin': '0 20px' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'C',
|
||||
styles: {
|
||||
'.nav-link': { 'margin': '0 20px', 'font-weight': 'bold' }
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 새 디자인 옵션 추가 예시
|
||||
|
||||
```javascript
|
||||
// D 옵션 추가
|
||||
{
|
||||
id: 'D',
|
||||
label: 'D',
|
||||
styles: {
|
||||
'.logo-text': {
|
||||
'font-size': '16px',
|
||||
'color': '#333333'
|
||||
},
|
||||
'.nav-link': {
|
||||
'padding': '10px 24px',
|
||||
'font-weight': '600'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### styles 객체 구조
|
||||
|
||||
```javascript
|
||||
styles: {
|
||||
'CSS 셀렉터': {
|
||||
'CSS 속성': '값',
|
||||
'CSS 속성2': '값2'
|
||||
},
|
||||
'다른 셀렉터': {
|
||||
'CSS 속성': '값'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자주 사용되는 CSS 셀렉터
|
||||
|
||||
| 셀렉터 | 설명 |
|
||||
|--------|------|
|
||||
| `.logo-text` | 로고 텍스트 "API Portal" |
|
||||
| `.nav-link` | 네비게이션 메뉴 링크 |
|
||||
| `.nav-menu` | 네비게이션 메뉴 컨테이너 |
|
||||
| `.header-content` | 헤더 콘텐츠 영역 |
|
||||
| `.global-header` | 전체 헤더 |
|
||||
| `.logo-wrapper` | 로고 래퍼 (이미지 + 텍스트) |
|
||||
|
||||
## 자주 사용되는 CSS 속성
|
||||
|
||||
| 속성 | 예시 값 | 설명 |
|
||||
|------|---------|------|
|
||||
| `font-size` | `18px`, `1.2rem` | 글자 크기 |
|
||||
| `font-weight` | `400`, `500`, `600`, `bold` | 글자 두께 |
|
||||
| `margin` | `0 20px`, `10px 15px` | 바깥 여백 |
|
||||
| `padding` | `8px 16px` | 안쪽 여백 |
|
||||
| `color` | `#333333`, `#0049b4` | 글자 색상 |
|
||||
| `background` | `#ffffff`, `#f5f5f5` | 배경 색상 |
|
||||
| `gap` | `12px`, `20px` | flex 아이템 간격 |
|
||||
|
||||
---
|
||||
|
||||
## 사용자 선택 저장
|
||||
|
||||
사용자의 디자인 선택은 브라우저의 `localStorage`에 저장됩니다.
|
||||
|
||||
- Key: `design-survey-selection`
|
||||
- Value: 선택한 디자인 ID (예: `A`, `B`, `C`)
|
||||
|
||||
```javascript
|
||||
// 저장된 선택 확인 (개발자 도구 콘솔)
|
||||
localStorage.getItem('design-survey-selection')
|
||||
|
||||
// 선택 초기화
|
||||
localStorage.removeItem('design-survey-selection')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `GlobalControllerAdvice.java` | `designSurveyEnabled` ModelAttribute 제공 |
|
||||
| `header_container.html` | Survey 바 HTML 및 DESIGN_OPTIONS 설정 |
|
||||
| `_header.scss` | Survey 바 스타일 |
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **디자인 옵션 ID는 고유해야 합니다** - 중복 ID 사용 시 오동작
|
||||
2. **isDefault는 하나의 옵션에만 설정** - 여러 개 설정 시 첫 번째만 적용
|
||||
3. **CSS 속성명은 kebab-case 사용** - `fontSize` (X) → `font-size` (O)
|
||||
4. **스타일 적용 시 `!important` 자동 추가** - 기존 스타일 덮어씀
|
||||
5. **Survey 기능 종료 후** - `ui.design-survey`를 `false`로 설정하여 비활성화
|
||||
|
||||
---
|
||||
|
||||
## 문의
|
||||
|
||||
기능 관련 문의는 개발팀에 연락해주세요.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
flowchart TD
|
||||
Start([법인 관리자가 사용자 초대]) --> GenerateToken[8글자 토큰 생성<br/>예: K7MNPQ2R<br/>UserInvitation 저장<br/>상태: PENDING]
|
||||
|
||||
GenerateToken --> SendEmail[이메일 발송<br/>- 링크 포함<br/>- 8글자 토큰 표시]
|
||||
|
||||
SendEmail --> UserType{사용자 유형}
|
||||
|
||||
%% ========================================
|
||||
%% 신규 회원 플로우
|
||||
%% ========================================
|
||||
UserType -->|신규 회원<br/>미가입자| NewUserPath{초대 확인 방법}
|
||||
|
||||
NewUserPath -->|이메일 링크| EmailLink1[이메일 링크 클릭<br/>/signup/portalUser?invitation=CODE]
|
||||
NewUserPath -->|일반 회원가입| DirectSignup[일반 회원가입 진행<br/>ROLE_USER로 생성]
|
||||
|
||||
EmailLink1 --> SignupPage1[회원가입 페이지<br/>- 이메일 자동 입력됨<br/>- 법인명 표시]
|
||||
SignupPage1 --> RegisterForm[가입 정보 입력<br/>초대 수락]
|
||||
RegisterForm --> RegisterComplete[회원가입 완료<br/>ROLE_CORP_USER로 생성<br/>상태: COMPLETED]
|
||||
RegisterComplete --> NewUserEnd([가입 완료 안내])
|
||||
|
||||
DirectSignup --> Login4[로그인]
|
||||
Login4 --> AfterLogin4[로그인 성공]
|
||||
AfterLogin4 --> CheckInvitation
|
||||
|
||||
%% ========================================
|
||||
%% 기존 회원 플로우
|
||||
%% ========================================
|
||||
UserType -->|기존 회원<br/>ROLE_USER| ExistingUserPath{초대 확인 방법}
|
||||
|
||||
ExistingUserPath -->|이메일 링크<br/>비로그인 상태| EmailLink2[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
|
||||
ExistingUserPath -->|이메일 링크<br/>로그인 상태| EmailLinkLoggedIn[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
|
||||
ExistingUserPath -->|직접 로그인| DirectLogin[로그인 페이지에서 로그인]
|
||||
|
||||
%% 비로그인 상태에서 이메일 링크 클릭
|
||||
EmailLink2 --> DecisionPagePublic[초대 수락 페이지<br/>비로그인 상태]
|
||||
DecisionPagePublic --> RedirectLogin2[로그인 유도]
|
||||
RedirectLogin2 --> Login2[로그인]
|
||||
Login2 --> AfterLogin2[로그인 성공]
|
||||
|
||||
%% 로그인 상태에서 이메일 링크 클릭 → 바로 수락 페이지
|
||||
EmailLinkLoggedIn --> DecisionPageDirect[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
|
||||
DecisionPageDirect --> UserDecision
|
||||
|
||||
%% 직접 로그인
|
||||
DirectLogin --> Login3[로그인]
|
||||
Login3 --> AfterLogin3[로그인 성공]
|
||||
|
||||
%% ========================================
|
||||
%% 로그인 후 초대 확인 (PortalAuthenticationSuccessHandler)
|
||||
%% ========================================
|
||||
AfterLogin2 --> CheckInvitation[PortalAuthenticationSuccessHandler<br/>PENDING 초대 확인<br/>findFirstByInvitationEmailAndStatus]
|
||||
AfterLogin3 --> CheckInvitation
|
||||
|
||||
CheckInvitation --> HasInvitation{PENDING 초대 존재?}
|
||||
|
||||
HasInvitation -->|없음| NormalHome[메인 페이지 이동]
|
||||
HasInvitation -->|있음| SaveToSession[세션에 초대 정보 저장<br/>pendingInvitation=true<br/>pendingInvitationToken<br/>pendingInvitationOrgName]
|
||||
|
||||
SaveToSession --> RedirectHome[메인 페이지로 이동]
|
||||
|
||||
%% ========================================
|
||||
%% 메인 페이지 팝업 (index.html)
|
||||
%% ========================================
|
||||
RedirectHome --> ShowPopup[팝업 표시<br/>OOO에서 초대가 도착했습니다<br/>초대를 확인하시겠습니까?]
|
||||
|
||||
ShowPopup --> PopupChoice{사용자 선택}
|
||||
|
||||
PopupChoice -->|확인| GoDecisionPage[초대 수락 페이지 이동<br/>/signup/decision?invitation=TOKEN]
|
||||
PopupChoice -->|취소| StayHome[메인 페이지 유지<br/>다음 로그인 시 팝업 재표시]
|
||||
|
||||
StayHome --> NextLogin[다음 로그인]
|
||||
NextLogin --> CheckInvitation
|
||||
|
||||
GoDecisionPage --> DecisionPage[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
|
||||
|
||||
DecisionPage --> UserDecision{사용자 선택}
|
||||
|
||||
UserDecision -->|수락| AcceptProcess[초대 수락 처리<br/>1. PortalOrg 변경<br/>2. ROLE_CORP_USER 전환<br/>3. 약관 재동의<br/>상태: COMPLETED]
|
||||
UserDecision -->|거절| RejectProcess[초대 거절 처리<br/>상태: REJECTED]
|
||||
|
||||
AcceptProcess --> LogoutPrompt[로그아웃 후 재로그인 안내]
|
||||
LogoutPrompt --> ExistingUserEnd([초대 수락 완료])
|
||||
|
||||
RejectProcess --> RejectEnd([초대 거절 완료<br/>다음 로그인 시 팝업 안 뜸])
|
||||
|
||||
%% ========================================
|
||||
%% 스타일링
|
||||
%% ========================================
|
||||
style Start fill:#e1f5e1
|
||||
style NewUserEnd fill:#c8e6c9
|
||||
style ExistingUserEnd fill:#c8e6c9
|
||||
style RejectEnd fill:#fff9c4
|
||||
style NormalHome fill:#e3f2fd
|
||||
|
||||
style GenerateToken fill:#bbdefb
|
||||
style AcceptProcess fill:#c5cae9
|
||||
style RejectProcess fill:#ffecb3
|
||||
|
||||
style CheckInvitation fill:#ffe0b2
|
||||
style SaveToSession fill:#ffe0b2
|
||||
style ShowPopup fill:#f8bbd0
|
||||
style PopupChoice fill:#f8bbd0
|
||||
|
||||
style StayHome fill:#e1bee7
|
||||
style NextLogin fill:#e1bee7
|
||||
@@ -0,0 +1,44 @@
|
||||
flowchart TD
|
||||
Start([법인 관리자가 사용자 초대 시작]) --> CheckUser{가입된 사용자인가?}
|
||||
|
||||
CheckUser -->|아니오| Error1[에러: 가입된 사용자만 초대 가능]
|
||||
CheckUser -->|예| CheckRole{이미 법인 사용자인가?}
|
||||
|
||||
CheckRole -->|예| Error2[에러: 이미 기관에 등록된 사용자]
|
||||
CheckRole -->|아니오 ROLE_USER| CreateInvitation[8글자 토큰 생성<br/>UserInvitation 저장<br/>상태: PENDING]
|
||||
|
||||
CreateInvitation --> SendEmail[이메일 발송<br/>UserInvitationEvent]
|
||||
|
||||
SendEmail --> UserReceive{사용자가 토큰 받음}
|
||||
|
||||
UserReceive -->|폐쇄망| ManualInput[8글자 토큰 직접 입력<br/>예: K7MNPQ2R]
|
||||
UserReceive -->|이메일 링크| EmailLink[Base64 인코딩된<br/>링크 클릭]
|
||||
|
||||
ManualInput --> DecodeToken[토큰 디코딩<br/>decodeInvitationToken]
|
||||
EmailLink --> DecodeToken
|
||||
|
||||
DecodeToken --> ValidateToken{토큰 유효성 검증}
|
||||
|
||||
ValidateToken -->|무효| Error3[에러: 유효하지 않은 초대]
|
||||
ValidateToken -->|유효 & PENDING| ShowDecision[약관 동의 페이지<br/>/signup/decision]
|
||||
|
||||
ShowDecision --> UserDecision{사용자 선택}
|
||||
|
||||
UserDecision -->|수락| ProcessAccept[processInvitation: accept]
|
||||
UserDecision -->|거절| ProcessReject[processInvitation: reject]
|
||||
|
||||
ProcessAccept --> UpdateUser[1. PortalOrg 변경<br/>2. ROLE_CORP_USER로 전환<br/>3. 약관 재동의 PRIVACY_COLLECT_ORG]
|
||||
UpdateUser --> CompleteInvitation[Invitation 상태:<br/>COMPLETED + completeDate]
|
||||
CompleteInvitation --> Success([초대 수락 완료<br/>로그아웃 후 재로그인])
|
||||
|
||||
ProcessReject --> RejectInvitation[Invitation 상태:<br/>REJECTED + completeDate]
|
||||
RejectInvitation --> End([초대 거절 완료])
|
||||
|
||||
style Start fill:#e1f5e1
|
||||
style Success fill:#c8e6c9
|
||||
style End fill:#fff9c4
|
||||
style Error1 fill:#ffcdd2
|
||||
style Error2 fill:#ffcdd2
|
||||
style Error3 fill:#ffcdd2
|
||||
style CreateInvitation fill:#bbdefb
|
||||
style UpdateUser fill:#c5cae9
|
||||
@@ -0,0 +1,122 @@
|
||||
sequenceDiagram
|
||||
actor 법인관리자
|
||||
participant Portal as Portal UI
|
||||
participant UserManFacade
|
||||
participant UserInvitationRepo as UserInvitation Repository
|
||||
participant MessageService as Message Handler
|
||||
participant Email as Email System
|
||||
actor 초대받은사용자
|
||||
participant AuthHandler as PortalAuthenticationSuccessHandler
|
||||
participant IndexController
|
||||
participant MainPage as 메인 페이지 (index.html)
|
||||
participant UserRegisterController
|
||||
participant UserRegisterFacade
|
||||
participant PortalUserRepo as PortalUser Repository
|
||||
participant PortalOrgRepo as PortalOrg Repository
|
||||
|
||||
%% ========================================
|
||||
%% 1. 초대 발송
|
||||
%% ========================================
|
||||
법인관리자->>Portal: 사용자 이메일 입력<br/>초대 요청
|
||||
Portal->>UserManFacade: sendInvitation(sender, emailAddr)
|
||||
|
||||
UserManFacade->>PortalUserRepo: findByEmailAddr(emailAddr)
|
||||
alt 사용자가 없거나 이미 법인 사용자
|
||||
UserManFacade-->>Portal: 에러 반환
|
||||
Portal-->>법인관리자: 초대 불가 알림
|
||||
else 일반 사용자 ROLE_USER 또는 비회원
|
||||
UserManFacade->>UserManFacade: generateEightCharToken()<br/>예: K7MNPQ2R
|
||||
UserManFacade->>UserInvitationRepo: save(UserInvitation)<br/>status: PENDING<br/>expiresOn: 7일 후
|
||||
UserManFacade->>MessageService: publishEvent(UserInvitationEvent)
|
||||
MessageService->>Email: 초대 이메일 발송<br/>(법인명, 관리자명, 토큰)
|
||||
UserManFacade-->>Portal: 초대 완료
|
||||
Portal-->>법인관리자: 초대 성공 알림
|
||||
end
|
||||
|
||||
Email->>초대받은사용자: 초대 이메일 수신
|
||||
|
||||
%% ========================================
|
||||
%% 2-A. 로그인 상태에서 이메일 링크 클릭 (바로 수락 페이지)
|
||||
%% ========================================
|
||||
alt 로그인 상태에서 이메일 링크 클릭
|
||||
초대받은사용자->>Portal: 이메일 링크 클릭<br/>/signup/decision?invitation=TOKEN
|
||||
Portal->>UserRegisterController: showDecisionPage(invitationToken)
|
||||
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
|
||||
UserRegisterController-->>Portal: 약관 동의 페이지
|
||||
Portal-->>초대받은사용자: 초대 수락/거절 화면<br/>(바로 표시, 팝업 없음)
|
||||
Note over 초대받은사용자: 6. 초대 수락/거절 처리로 이동
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 2-B. 로그인 (기존 회원 또는 신규 가입 후)
|
||||
%% ========================================
|
||||
alt 기존 회원 (비로그인 상태)
|
||||
초대받은사용자->>Portal: 로그인
|
||||
else 비회원이 일반 회원가입 후
|
||||
초대받은사용자->>Portal: 회원가입 (ROLE_USER)
|
||||
초대받은사용자->>Portal: 로그인
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 3. 로그인 성공 처리 (AuthenticationSuccessHandler)
|
||||
%% ========================================
|
||||
Portal->>AuthHandler: onAuthenticationSuccess()
|
||||
AuthHandler->>AuthHandler: 이메일 인증, 휴면계정,<br/>비밀번호 만료 체크
|
||||
|
||||
AuthHandler->>UserInvitationRepo: findFirstByInvitationEmailAndStatus<br/>(loginId, PENDING)
|
||||
|
||||
alt PENDING 초대 없음
|
||||
AuthHandler->>Portal: redirect to /
|
||||
Portal-->>초대받은사용자: 메인 페이지 (팝업 없음)
|
||||
else PENDING 초대 있음 & 만료 안됨
|
||||
AuthHandler->>PortalOrgRepo: findById(invitation.orgId)
|
||||
PortalOrgRepo-->>AuthHandler: PortalOrg (기관명)
|
||||
AuthHandler->>AuthHandler: session.setAttribute<br/>("pendingInvitation", true)<br/>("pendingInvitationToken", token)<br/>("pendingInvitationOrgName", orgName)
|
||||
AuthHandler->>Portal: redirect to /
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 4. 메인 페이지 팝업 표시
|
||||
%% ========================================
|
||||
Portal->>IndexController: GET /
|
||||
IndexController->>MainPage: render index.html
|
||||
|
||||
MainPage->>MainPage: 세션에서 pendingInvitation 확인
|
||||
|
||||
alt pendingInvitation == true
|
||||
MainPage-->>초대받은사용자: 팝업 표시<br/>"[기관명]에서 초대가 도착했습니다.<br/>초대를 확인하시겠습니까?"
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 5. 사용자 팝업 선택
|
||||
%% ========================================
|
||||
alt 팝업에서 "확인" 클릭
|
||||
초대받은사용자->>Portal: /signup/decision?invitation=TOKEN
|
||||
Portal->>UserRegisterController: showDecisionPage(invitationToken)
|
||||
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
|
||||
UserRegisterController-->>Portal: 약관 동의 페이지
|
||||
Portal-->>초대받은사용자: 초대 수락/거절 화면
|
||||
else 팝업에서 "취소" 클릭
|
||||
Note over 초대받은사용자,MainPage: 메인 페이지 유지<br/>다음 로그인 시 팝업 재표시
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 6. 초대 수락/거절 처리
|
||||
%% ========================================
|
||||
초대받은사용자->>Portal: 수락 or 거절 선택
|
||||
Portal->>UserRegisterController: processInvitation(action)
|
||||
UserRegisterController->>UserRegisterFacade: processInvitation(action, invitation)
|
||||
|
||||
alt 수락
|
||||
UserRegisterFacade->>PortalUserRepo: findByEmailAddr()
|
||||
UserRegisterFacade->>PortalUserRepo: updateUserToCorpUser()<br/>(PortalOrg 변경<br/>ROLE_CORP_USER 전환)
|
||||
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: COMPLETED<br/>completeDate: now()
|
||||
UserRegisterFacade-->>Portal: 수락 성공
|
||||
Portal-->>초대받은사용자: 성공 메시지<br/>로그아웃 후 재로그인 안내
|
||||
Note over 초대받은사용자: 다음 로그인 시<br/>PENDING 초대 없음 → 팝업 안 뜸
|
||||
else 거절
|
||||
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: REJECTED<br/>completeDate: now()
|
||||
UserRegisterFacade-->>Portal: 거절 처리 완료
|
||||
Portal-->>초대받은사용자: 거절 완료 메시지
|
||||
Note over 초대받은사용자: 다음 로그인 시<br/>REJECTED 상태 → 팝업 안 뜸
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
stateDiagram-v2
|
||||
[*] --> 초대생성: 법인관리자가 초대 시작
|
||||
|
||||
초대생성 --> PENDING: 토큰 생성 및 저장
|
||||
|
||||
PENDING --> 이메일발송: 초대 이메일 발송
|
||||
|
||||
state 로그인후팝업 {
|
||||
이메일발송 --> 로그인: 사용자 로그인
|
||||
로그인 --> 초대확인: AuthSuccessHandler
|
||||
초대확인 --> 세션저장: 세션에 초대 정보 저장
|
||||
세션저장 --> 팝업표시: 메인 페이지 팝업
|
||||
팝업표시 --> 팝업확인: 확인 클릭
|
||||
팝업표시 --> 팝업취소: 취소 클릭
|
||||
팝업취소 --> 로그인: 다음 로그인 시 재표시
|
||||
}
|
||||
|
||||
팝업확인 --> 초대수락: 수락 페이지 이동
|
||||
|
||||
초대수락 --> COMPLETED: 수락
|
||||
초대수락 --> REJECTED: 거절
|
||||
|
||||
COMPLETED --> [*]: 초대 완료
|
||||
REJECTED --> [*]: 초대 거절
|
||||
PENDING --> CANCELED: 관리자 취소
|
||||
PENDING --> EXPIRED: 만료일 경과
|
||||
CANCELED --> [*]: 초대 취소
|
||||
EXPIRED --> [*]: 초대 만료
|
||||
|
||||
note right of PENDING
|
||||
PENDING 상태일 때만
|
||||
로그인 시 팝업 표시
|
||||
end note
|
||||
|
||||
note right of REJECTED
|
||||
REJECTED 상태가 되면
|
||||
더 이상 팝업 표시 안 함
|
||||
end note
|
||||
|
||||
note right of COMPLETED
|
||||
COMPLETED 상태가 되면
|
||||
더 이상 팝업 표시 안 함
|
||||
ROLE_CORP_USER로 전환됨
|
||||
end note
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 574 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+830
-2
@@ -1,6 +1,834 @@
|
||||
{
|
||||
"name": "eactive-portal",
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.57.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.69.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readdirp": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.57.0"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"description": "SASS build system for EAPIM Portal",
|
||||
"scripts": {
|
||||
"sass:build": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded",
|
||||
"sass:build:minified": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.min.css --style=compressed",
|
||||
"sass:watch": "sass --watch src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded",
|
||||
"build": "npm run sass:build && npm run sass:build:minified",
|
||||
"dev": "npm run sass:watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.57.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sass src/main/resources/static/sass/main.scss src/main/resources/static/css/main.css
|
||||
+2
-2
@@ -7,5 +7,5 @@ project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-
|
||||
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||
|
||||
|
||||
include 'kjb-safedb'
|
||||
project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
||||
//include 'kjb-safedb'
|
||||
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
||||
+1
-1
@@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
@Controller
|
||||
@RequestMapping("/agreements")
|
||||
public class AgreementsController {
|
||||
public static final String TERMS_AGREEMENTS = "fragment/kbank/terms_agreements";
|
||||
public static final String TERMS_AGREEMENTS = "fragment/kjbank/terms_agreements";
|
||||
|
||||
private final AgreementsFacade agreementsFacade;
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ public class AgreementsDTO {
|
||||
|
||||
private LocalDateTime updatedDate;
|
||||
|
||||
/* 첨부파일 ID */
|
||||
private String attachedFileId;
|
||||
|
||||
/* 첨부파일명 (확장자 포함) */
|
||||
private String attachedFileName;
|
||||
|
||||
// 기본 생성자 추가
|
||||
public AgreementsDTO() {
|
||||
this.contents = ""; // null 방지를 위한 기본값 설정
|
||||
|
||||
+86
-12
@@ -7,30 +7,48 @@ import com.eactive.apim.portal.apps.agreements.dto.AgreementsDTO;
|
||||
import com.eactive.apim.portal.apps.agreements.mapper.AgreementsMapper;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserPrivacyAgreementService;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.file.entity.FileDetail;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
|
||||
// 플레이스홀더 패턴: {{IMG:fileId:fileSn}} 또는 {{IMG:fileId:fileSn|속성}}
|
||||
private static final Pattern PLACEHOLDER_PATTERN =
|
||||
Pattern.compile("\\{\\{IMG:([a-f0-9\\-]+):(\\d+)(\\|[^}]+)?\\}\\}");
|
||||
|
||||
private final AgreementsService agreementsService;
|
||||
private final AgreementsMapper agreementsMapper;
|
||||
private final PortalUserPrivacyAgreementService userAgreementService;
|
||||
private final FileService fileService;
|
||||
|
||||
@Value("${editor.image.base-url:/file/view}")
|
||||
private String editorImageBaseUrl;
|
||||
|
||||
@Autowired
|
||||
public AgreementsFacadeImpl(AgreementsService agreementsService,
|
||||
AgreementsMapper agreementsMapper,
|
||||
PortalUserPrivacyAgreementService userAgreementService) {
|
||||
PortalUserPrivacyAgreementService userAgreementService,
|
||||
FileService fileService) {
|
||||
this.agreementsService = agreementsService;
|
||||
this.agreementsMapper = agreementsMapper;
|
||||
this.userAgreementService = userAgreementService;
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,15 +89,79 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
}
|
||||
}
|
||||
|
||||
// HTML 언이스케이프 처리
|
||||
// HTML 언이스케이프 및 이미지 플레이스홀더 변환 처리
|
||||
private AgreementsDTO convertToDTO(Agreements agreement) {
|
||||
AgreementsDTO dto = agreementsMapper.map(agreement);
|
||||
if (dto.getContents() != null) {
|
||||
dto.setContents(StringEscapeUtils.unescapeHtml4(dto.getContents()));
|
||||
String contents = StringEscapeUtils.unescapeHtml4(dto.getContents());
|
||||
// 이미지 플레이스홀더를 HTML img 태그로 변환
|
||||
contents = convertPlaceholdersToHtml(contents);
|
||||
dto.setContents(contents);
|
||||
}
|
||||
// 첨부파일 정보 설정
|
||||
if (StringUtils.isNotBlank(agreement.getAttachedFileId())) {
|
||||
dto.setAttachedFileId(agreement.getAttachedFileId());
|
||||
try {
|
||||
FileDetail fileDetail = fileService.getFileDetailByIds(agreement.getAttachedFileId(), 1);
|
||||
String fileName = fileDetail.getOriginalFileName();
|
||||
if (StringUtils.isNotBlank(fileDetail.getFileExtension())) {
|
||||
fileName += "." + fileDetail.getFileExtension();
|
||||
}
|
||||
dto.setAttachedFileName(fileName);
|
||||
} catch (Exception e) {
|
||||
log.warn("첨부파일 정보 조회 실패: fileId={}", agreement.getAttachedFileId(), e);
|
||||
}
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플레이스홀더를 실제 이미지 태그로 변환
|
||||
* {{IMG:fileId:fileSn}} 또는 {{IMG:fileId:fileSn|width=300|height=200|style=...}}
|
||||
* → <img src="/file/view?fileId={fileId}&fileSn={fileSn}" width="300" height="200" style="..." />
|
||||
*/
|
||||
private String convertPlaceholdersToHtml(String contents) {
|
||||
if (contents == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(contents);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String fileId = matcher.group(1);
|
||||
String fileSn = matcher.group(2);
|
||||
String attrs = matcher.group(3);
|
||||
|
||||
StringBuilder img = new StringBuilder();
|
||||
img.append("<img src=\"").append(editorImageBaseUrl)
|
||||
.append("?fileId=").append(fileId)
|
||||
.append("&fileSn=").append(fileSn).append("\"");
|
||||
|
||||
// 속성 파싱 및 추가
|
||||
if (attrs != null && !attrs.isEmpty()) {
|
||||
String[] parts = attrs.substring(1).split("\\|"); // 앞의 | 제거 후 분리
|
||||
for (String part : parts) {
|
||||
String[] kv = part.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
img.append(" ").append(kv[0]).append("=\"").append(kv[1]).append("\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 스타일 (style 속성이 없으면)
|
||||
if (attrs == null || !attrs.contains("style=")) {
|
||||
img.append(" style=\"max-width:100%;height:auto;\"");
|
||||
}
|
||||
|
||||
img.append(" />");
|
||||
matcher.appendReplacement(sb, Matcher.quoteReplacement(img.toString()));
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgreementsDTO getAgreement(String agreementsTypeCode) {
|
||||
LocalDateTime date = LocalDateTime.now();
|
||||
@@ -142,15 +224,7 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
return Collections.singletonList(createDefaultAgreement(String.valueOf(agreementTypeCode)));
|
||||
}
|
||||
return agreementsList.stream()
|
||||
.map(agreement -> {
|
||||
AgreementsDTO dto = agreementsMapper.map(agreement);
|
||||
|
||||
// HTML 언이스케이프 처리
|
||||
if (dto.getContents() != null) {
|
||||
dto.setContents(StringEscapeUtils.unescapeHtml4(dto.getContents()));
|
||||
}
|
||||
return dto;
|
||||
})
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,11 @@ package com.eactive.apim.portal.apps.apis.controller;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.service.AppServiceFacade;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -28,11 +21,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAjaxController {
|
||||
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final ApiService apiService;
|
||||
private final ApiServiceService apiServiceService;
|
||||
private final AppServiceFacade appServiceFacade;
|
||||
|
||||
public Map<String, ApiServiceDTO> getMainIconsFromServiceDtos(List<ApiServiceDTO> apiServices) {
|
||||
Map<String, ApiServiceDTO> mainIconsMap = new HashMap<>();
|
||||
@@ -54,53 +44,15 @@ public class ApiAjaxController {
|
||||
Map<String, ApiServiceDTO> mainIconsMap = getMainIconsFromServiceDtos(apiServices);
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = apiService.findApis();
|
||||
|
||||
for (ApiSpecInfoDto spec : apiSpecInfoDtos) {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
if (serviceDTO != null) {
|
||||
String mainIcon = serviceDTO.getMainIcon();
|
||||
String service = serviceDTO.getGroupName();
|
||||
spec.setMainIcon(mainIcon);
|
||||
spec.setService(service);
|
||||
}
|
||||
}
|
||||
return apiSpecInfoDtos;
|
||||
}
|
||||
|
||||
@GetMapping("/for_request_prod")
|
||||
@Secured("ROLE_API_KEY_REQUEST")
|
||||
public List<ApiSpecInfoDto> apiRequestProd(@ModelAttribute ApiGroupSearch search) {
|
||||
|
||||
List<ApiServiceDTO> apiServices = apiServiceService.searchApiGroups(search);
|
||||
Map<String, ApiServiceDTO> mainIconsMap = getMainIconsFromServiceDtos(apiServices);
|
||||
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
|
||||
List<ClientDTO> apikeys = appServiceFacade.getApiKeyList(user.getPortalOrg(), STG_SERVER);
|
||||
|
||||
List<ApiServiceApiDTO> apis = apikeys.stream()
|
||||
.flatMap(client -> client.getApiList().stream()) // flatten all apiLists into a single stream of ApiServiceApiDTO
|
||||
.distinct() // remove duplicates
|
||||
.collect(Collectors.toList()); // collect to List
|
||||
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = new ArrayList<>();
|
||||
|
||||
apis.forEach(api -> {
|
||||
ApiSpecInfoDto specInfoDto = new ApiSpecInfoDto();
|
||||
specInfoDto.setApiId(api.getApiId());
|
||||
specInfoDto.setApiName(api.getApiDesc());
|
||||
apiSpecInfoDtos.add(specInfoDto);
|
||||
});
|
||||
|
||||
for (ApiSpecInfoDto spec : apiSpecInfoDtos) {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
if (serviceDTO != null) {
|
||||
String mainIcon = serviceDTO.getMainIcon();
|
||||
String service = serviceDTO.getGroupName();
|
||||
spec.setMainIcon(mainIcon);
|
||||
spec.setService(service);
|
||||
}
|
||||
}
|
||||
return apiSpecInfoDtos;
|
||||
// Filter APIs that have matching service information
|
||||
return apiSpecInfoDtos.stream()
|
||||
.filter(spec -> mainIconsMap.containsKey(spec.getApiId()))
|
||||
.peek(spec -> {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
spec.setMainIcon(serviceDTO.getMainIcon());
|
||||
spec.setService(serviceDTO.getGroupName());
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.eactive.apim.portal.apps.apis.controller;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSearchData;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
@@ -81,7 +81,7 @@ public class ApiTesterFilter implements Filter {
|
||||
String scope = params.getOrDefault("scope", "default");
|
||||
|
||||
String token = "{\n" +
|
||||
" \"access_token\": \"kbank_gw_sample_token\",\n" +
|
||||
" \"access_token\": \"kjbank_gw_sample_token\",\n" +
|
||||
" \"token_type\": \"bearer\",\n" +
|
||||
" \"expires_in\": 86400,\n" +
|
||||
" \"scope\": \""+scope +"\",\n" +
|
||||
@@ -94,7 +94,7 @@ public class ApiTesterFilter implements Filter {
|
||||
} else {
|
||||
ApiSpecInfoDto apiSpecInfoDto = apiSpecInfoDtoService.selectDetailByURLAndMethod(parseUri(url), httpServletRequest.getMethod());
|
||||
|
||||
if (apiSpecInfoDto.getResponseType().equals("sample")) {
|
||||
if (apiSpecInfoDto.getResponseType() == null || apiSpecInfoDto.getResponseType().equals("sample")) {
|
||||
response.setContentType("application/json");
|
||||
response.getWriter().println(apiSpecInfoDto.getSampleResponse());
|
||||
} else {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.eactive.apim.portal.apps.app.controller;
|
||||
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSearchData;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientSimpleDTO;
|
||||
import com.eactive.apim.portal.apps.app.mapper.CredentialMapper;
|
||||
import com.eactive.apim.portal.apps.app.service.AppServiceFacade;
|
||||
import com.eactive.apim.portal.common.dto.ResponseDTO;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.EncryptionUtil;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.ValidationUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/myapikey")
|
||||
@RequiredArgsConstructor
|
||||
@Secured("ROLE_API_KEY_REQUEST")
|
||||
public class ApiKeyRestController {
|
||||
|
||||
private final static String PROD_SERVER = "PROD";
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final AppServiceFacade appServiceFacade;
|
||||
private final CredentialMapper credentialMapper;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
|
||||
@PostMapping("/api_key_request")
|
||||
public ResponseDTO apiRequest(@RequestBody AppRequestDTO appRequest) {
|
||||
appRequest.setOrg(SecurityUtil.getUserOrg());
|
||||
if (appRequest.getType() == null) {
|
||||
appRequest.setType(AppRequestType.NEW);
|
||||
}
|
||||
AppRequestDTO appRequestDTO = appServiceFacade.createAppRequest(appRequest);
|
||||
appServiceFacade.beginApproval(appRequestDTO.getApproval().getId());
|
||||
return new ResponseDTO(200, "SUCCESS", appRequestDTO.getType().getDescription() + " 신청이 완료되었습니다.");
|
||||
}
|
||||
|
||||
@PostMapping("/api_key_request_prod")
|
||||
public ResponseDTO requestProdApiKey(@RequestBody AppRequestDTO appRequest) {
|
||||
appRequest.setOrg(SecurityUtil.getUserOrg());
|
||||
|
||||
if (appRequest.getClientId() != null && !appRequest.getClientId().isEmpty()) {
|
||||
try {
|
||||
appRequest.setClientId(encryptionUtil.decrypt(appRequest.getClientId()));
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
return new ResponseDTO(400, "FAILURE", "API 키 정보가 올바르지 않습니다.");
|
||||
}
|
||||
appRequest.setType(AppRequestType.PROD_MODIFY);
|
||||
} else {
|
||||
appRequest.setType(AppRequestType.PROD_NEW);
|
||||
}
|
||||
|
||||
AppRequestDTO appRequestDTO = appServiceFacade.createAppRequest(appRequest);
|
||||
appServiceFacade.beginApproval(appRequestDTO.getApproval().getId());
|
||||
|
||||
return new ResponseDTO(200, "SUCCESS", appRequestDTO.getType().getDescription() + " 신청이 완료되었습니다.");
|
||||
}
|
||||
|
||||
@GetMapping("/dev")
|
||||
public List<ClientSimpleDTO> apiKeyList() {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
List<ClientDTO> apikeys = appServiceFacade.getApiKeyList(user.getPortalOrg(), STG_SERVER);
|
||||
|
||||
ApiSearchData apiSearchData = apiSearchFacade.loadAllApiData();
|
||||
for (ClientDTO apikey : apikeys) {
|
||||
apikey.getApiList().forEach(api -> {
|
||||
api.setService(apiSearchData.getServicesByApiId().get(api.getApiId()).getGroupName());
|
||||
});
|
||||
}
|
||||
|
||||
return apikeys.stream().map(credentialMapper::mapToClientSimpleDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/prod")
|
||||
public ClientSimpleDTO apiKeyProdList(@RequestBody AppRequestDTO appRequest) {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
|
||||
try {
|
||||
ClientDTO apiKey = appServiceFacade.getApiKey(user.getPortalOrg().getId(), encryptionUtil.decrypt(appRequest.getClientId()), PROD_SERVER);
|
||||
ApiSearchData apiSearchData = apiSearchFacade.loadAllApiData();
|
||||
apiKey.getApiList().forEach(api -> {
|
||||
api.setService(apiSearchData.getServicesByApiId().get(api.getApiId()).getGroupName());
|
||||
});
|
||||
apiKey.setClientid(encryptionUtil.encrypt(apiKey.getClientid()));
|
||||
return credentialMapper.mapToClientSimpleDTO(apiKey);
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
return new ClientSimpleDTO();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
package com.eactive.apim.portal.apps.app.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* API Key Registration Session Data
|
||||
* Holds data across multiple registration steps
|
||||
*/
|
||||
@Data
|
||||
public class ApiKeyRegistrationDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// Client ID for modification operations
|
||||
private String clientId;
|
||||
|
||||
// Request type for tracking operation type
|
||||
private String requestType; // "NEW" or "MODIFY"
|
||||
|
||||
// Existing app icon file ID (for modifications)
|
||||
private String appIconFileId;
|
||||
|
||||
// Step 1: Basic Information
|
||||
// Transient field for file upload (not serialized in session)
|
||||
private transient MultipartFile appIcon;
|
||||
|
||||
// Persisted file data
|
||||
private String appIconFileName;
|
||||
private byte[] appIconData;
|
||||
private String appIconContentType;
|
||||
|
||||
@NotBlank(message = "앱 이름을 입력해주세요.")
|
||||
@Length(max = 100, message = "앱 이름은 100자를 초과할 수 없습니다.")
|
||||
private String appName;
|
||||
|
||||
@NotBlank(message = "앱 설명을 입력해주세요.")
|
||||
@Length(max = 500, message = "앱 설명은 500자를 초과할 수 없습니다.")
|
||||
private String appDescription;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
private List<String> ipWhitelist = new ArrayList<>();
|
||||
|
||||
// Step 2: API Selection
|
||||
private List<String> selectedApis = new ArrayList<>();
|
||||
|
||||
// Helper method to add IP to whitelist
|
||||
public void addIpAddress(String ip) {
|
||||
if (ip != null && !ip.trim().isEmpty() && !ipWhitelist.contains(ip)) {
|
||||
ipWhitelist.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to set IP whitelist from comma-separated string
|
||||
public void setIpWhitelistFromString(String ipString) {
|
||||
ipWhitelist.clear();
|
||||
if (ipString != null && !ipString.trim().isEmpty()) {
|
||||
String[] ips = ipString.split(",");
|
||||
for (String ip : ips) {
|
||||
String trimmedIp = ip.trim();
|
||||
if (!trimmedIp.isEmpty()) {
|
||||
ipWhitelist.add(trimmedIp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get IP whitelist as comma-separated string
|
||||
public String getIpWhitelistAsString() {
|
||||
return String.join(",", ipWhitelist);
|
||||
}
|
||||
|
||||
// Helper method to get app icon as base64 data URI for image display
|
||||
public String getAppIconBase64() {
|
||||
if (appIconData == null || appIconData.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String base64Data = Base64.getEncoder().encodeToString(appIconData);
|
||||
String contentType = appIconContentType != null ? appIconContentType : "image/png";
|
||||
return "data:" + contentType + ";base64," + base64Data;
|
||||
}
|
||||
|
||||
// Validation helpers
|
||||
public boolean isStep1Complete() {
|
||||
// callbackUrl and appIcon are optional, so we only check required fields
|
||||
return appName != null && !appName.trim().isEmpty() &&
|
||||
appDescription != null && !appDescription.trim().isEmpty();
|
||||
}
|
||||
|
||||
public boolean isStep2Complete() {
|
||||
return selectedApis != null && !selectedApis.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return isStep1Complete() && isStep2Complete();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,14 @@ public class AppRequestDTO {
|
||||
|
||||
private String reason;
|
||||
|
||||
private String appDescription;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
private String ipWhitelist;
|
||||
|
||||
private String appIconFileId;
|
||||
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
private String prevApiList;
|
||||
|
||||
@@ -45,6 +45,10 @@ public class ClientDTO {
|
||||
|
||||
private String appstatus;
|
||||
|
||||
private String appDescription;
|
||||
|
||||
private String appIconFileId;
|
||||
|
||||
private List<ApiServiceApiDTO> apiList = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package com.eactive.apim.portal.apps.app.mapper;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apps.apiservice.mapper.ApiServiceApiMapper;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.approval.mapper.ApprovalMapper;
|
||||
import com.eactive.apim.portal.apps.user.mapper.PortalOrgMapper;
|
||||
import com.eactive.eai.data.mapper.BaseMapperConfig;
|
||||
@@ -13,4 +14,53 @@ import org.mapstruct.Mapper;
|
||||
@Mapper(config = BaseMapperConfig.class, uses = {PortalOrgMapper.class, ApiServiceApiMapper.class, ApprovalMapper.class})
|
||||
public interface AppRequestMapper extends GenericMapper<AppRequestDTO, AppRequest> {
|
||||
|
||||
/**
|
||||
* AppRequest를 ClientDTO로 간단하게 매핑합니다.
|
||||
* 뷰에서 AppRequest를 ClientDTO와 동일한 형식으로 표시하기 위해 사용됩니다.
|
||||
*
|
||||
* @param appRequest 앱 요청 엔티티
|
||||
* @return ClientDTO 표시용 DTO
|
||||
*/
|
||||
default ClientDTO mapToClientDTO(AppRequest appRequest) {
|
||||
if (appRequest == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClientDTO dto = new ClientDTO();
|
||||
|
||||
// 기본 식별 정보
|
||||
dto.setClientid(appRequest.getId()); // AppRequest ID를 임시 client ID로 사용
|
||||
dto.setClientname(appRequest.getClientName());
|
||||
|
||||
// 앱 상세 정보
|
||||
dto.setAppDescription(appRequest.getAppDescription());
|
||||
dto.setAppIconFileId(appRequest.getAppIconFileId());
|
||||
|
||||
// 네트워크 설정
|
||||
dto.setRedirecturi(appRequest.getCallbackUrl());
|
||||
dto.setAllowedips(appRequest.getIpWhitelist());
|
||||
|
||||
// 상태 정보 (ApprovalStatus → appstatus 매핑)
|
||||
if (appRequest.getStatus() != null) {
|
||||
switch (appRequest.getStatus()) {
|
||||
case APPROVED:
|
||||
dto.setAppstatus("1"); // 승인됨 = 활성화
|
||||
break;
|
||||
case DENIED:
|
||||
dto.setAppstatus("0"); // 거부됨 = 비활성화
|
||||
break;
|
||||
default:
|
||||
dto.setAppstatus("2"); // 대기중/진행중 = 보류
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 수정 날짜 (createdDate를 modifiedon으로 사용)
|
||||
dto.setModifiedon(appRequest.getCreatedDate());
|
||||
|
||||
// API 목록은 별도 처리 필요 (콤마로 구분된 문자열 → List<ApiServiceApiDTO>)
|
||||
// 필요시 컨트롤러에서 추가 처리
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.statemachine.ProcessingState;
|
||||
import com.eactive.apim.portal.approval.statemachine.RequestedState;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.apps.app.dto.ApiKeyRegistrationDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.mapper.AppRequestMapper;
|
||||
@@ -21,16 +23,24 @@ import com.eactive.apim.portal.apps.approval.service.ApprovalService;
|
||||
import com.eactive.apim.portal.apps.user.dto.PortalOrgDTO;
|
||||
import com.eactive.apim.portal.apps.user.mapper.PortalOrgMapper;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.ApiServiceHelper;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -47,53 +57,64 @@ public class AppServiceFacade {
|
||||
private final AppRequestMapper appRequestMapper;
|
||||
private final PortalOrgMapper portalOrgMapper;
|
||||
private final ApiServiceHelper apiServiceHelper;
|
||||
private final FileService fileService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public Page<ClientDTO> getApiKeyPageForServer(Pageable pageable, PortalOrg portalOrg, String server) {
|
||||
Page<Credential> clients = credentialRepository.findAllByOrgidAndServer(pageable, portalOrg.getId(), server);
|
||||
return clients.map(credentialMapper::toVo);
|
||||
}
|
||||
public List<ClientDTO> getApikeyList(PortalOrg portalOrg) {
|
||||
|
||||
public List<ClientDTO> getApiKeyList(PortalOrg portalOrg, String server) {
|
||||
List<Credential> clients = credentialRepository.findAllByOrgidAndServer(portalOrg.getId(), server);
|
||||
List<Credential> clients = credentialRepository.findAllByOrgid(portalOrg.getId());
|
||||
return clients.stream().map(credentialMapper::toVo).collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
public ClientDTO getApiKey(String orgid, String clientId, String server) {
|
||||
return credentialRepository.findByClientidAndOrgidAndServer(clientId, orgid, server).map(credentialMapper::toVo).orElse(null);
|
||||
public List<AppRequest> getPendingApiKeyList(PortalOrg portalOrg) {
|
||||
List<AppRequest> appRequests = appRequestRepository.findAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(portalOrg, Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE),
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
|
||||
return appRequests;
|
||||
}
|
||||
|
||||
public ClientDTO getApiKey(String orgid, String clientId) {
|
||||
return credentialRepository.findByClientidAndOrgid(clientId, orgid).map(credentialMapper::toVo).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AppRequest ID와 조직 정보로 AppRequest를 조회합니다.
|
||||
* API 그룹 및 API 목록 정보를 함께 처리하여 반환합니다.
|
||||
*
|
||||
* @param id AppRequest ID
|
||||
* @param portalOrg 포털 조직
|
||||
* @return AppRequest DTO (없으면 null)
|
||||
*/
|
||||
public AppRequestDTO getAppRequestById(String id, PortalOrg portalOrg) {
|
||||
return getApiRequestByIdAndPortalOrg(id, portalOrg);
|
||||
}
|
||||
|
||||
|
||||
public Page<AppRequestDTO> getApiRequestHistory(Pageable pageable, PortalOrgDTO portalOrg) {
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.NEW,AppRequestType.MODIFY, AppRequestType.DELETE)).map(appRequestMapper::toVo);
|
||||
}
|
||||
|
||||
public Page<AppRequestDTO> getApiRequestProdHistory(Pageable pageable, PortalOrgDTO portalOrg) {
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).map(appRequestMapper::toVo);
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE)).map(appRequestMapper::toVo);
|
||||
}
|
||||
|
||||
public AppRequestDTO createAppRequest(AppRequestDTO appRequest) {
|
||||
|
||||
setApiGroupsFromApiList(appRequest);
|
||||
AppRequest request = appRequestRepository.save(appRequestMapper.toEntity(appRequest));
|
||||
|
||||
if (request.getType() != null && (request.getType().equals(AppRequestType.MODIFY) || request.getType().equals(AppRequestType.DELETE) ||
|
||||
request.getType().equals(AppRequestType.PROD_MODIFY) || request.getType().equals(AppRequestType.PROD_DELETE))) {
|
||||
if (request.getType() != null && (request.getType().equals(AppRequestType.MODIFY) || request.getType().equals(AppRequestType.DELETE))) {
|
||||
|
||||
Optional<Credential> client = credentialRepository.findById(request.getClientId());
|
||||
|
||||
if (client.isPresent()) {
|
||||
request.setClientName(client.get().getClientname());
|
||||
|
||||
if(client.get().getApiList() != null) {
|
||||
if (client.get().getApiList() != null) {
|
||||
// 기존 API 목록을 저장
|
||||
String existingApiList = client.get().getApiList().stream()
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
.collect(Collectors.joining(","));
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
.collect(Collectors.joining(","));
|
||||
request.setPrevApiList(existingApiList);
|
||||
}
|
||||
|
||||
if(AppRequestType.DELETE.equals(request.getType()) || AppRequestType.PROD_DELETE.equals(request.getType())) {
|
||||
if (AppRequestType.DELETE.equals(request.getType())) {
|
||||
request.setApiList(request.getPrevApiList());
|
||||
}
|
||||
} else {
|
||||
@@ -108,22 +129,6 @@ public class AppServiceFacade {
|
||||
return appRequestMapper.toVo(request);
|
||||
}
|
||||
|
||||
private void setApiGroupsFromApiList(AppRequestDTO appRequest) {
|
||||
if (StringUtils.isEmpty(appRequest.getApiList())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> uniqueGroupIds = Arrays.stream(appRequest.getApiList().split(","))
|
||||
.map(apiId -> apiServiceService.findApiServiceByApiId(apiId))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(apiService -> apiService.getApiGroupApiList().stream())
|
||||
.map(ApiServiceApiDTO::getApiGroupId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
appRequest.setApiGroupList(String.join(",", uniqueGroupIds));
|
||||
}
|
||||
|
||||
|
||||
public void beginApproval(String approvalId) {
|
||||
approvalService.beginApproval(approvalId);
|
||||
}
|
||||
@@ -132,27 +137,11 @@ public class AppServiceFacade {
|
||||
appRequestRepository.findByIdAndOrg(id, portalOrg).ifPresent(approvalService::cancelAppApproval);
|
||||
}
|
||||
|
||||
public List<AppRequestDTO> getProdApiRequestHistory(String clientid) {
|
||||
return appRequestRepository.findAllByClientIdsContainsAndTypeIsIn(clientid,Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).stream().map(appRequestMapper::toVo).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public AppRequestDTO getApiRequestProdByIdAndPortalOrg(String id, PortalOrg portalOrg) {
|
||||
AppRequestDTO appRequest = appRequestRepository.findByIdAndOrgAndTypeIsIn(id, portalOrg,
|
||||
Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).map(this::convertToAppRequestDTO).orElse(null);
|
||||
|
||||
if(appRequest != null) {
|
||||
processApiGroups(appRequest);
|
||||
processApiList(appRequest);
|
||||
}
|
||||
return appRequest;
|
||||
}
|
||||
|
||||
public AppRequestDTO getApiRequestByIdAndPortalOrg(String id, PortalOrg org) {
|
||||
AppRequestDTO appRequest = appRequestRepository.findByIdAndOrg(id, org).map(this::convertToAppRequestDTO).orElse(null);
|
||||
|
||||
if(appRequest != null) {
|
||||
processApiGroups(appRequest);
|
||||
if (appRequest != null) {
|
||||
processApiList(appRequest);
|
||||
}
|
||||
return appRequest;
|
||||
@@ -167,16 +156,6 @@ public class AppServiceFacade {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void processApiGroups(AppRequestDTO appRequest) {
|
||||
|
||||
if (!StringUtils.isEmpty(appRequest.getApiGroupList())) {
|
||||
String[] apiGroupList = appRequest.getApiGroupList().split(",");
|
||||
for (String apiGroupId : apiGroupList) {
|
||||
appRequest.getApiServiceDTOList().add(apiServiceService.getApiGroupById(apiGroupId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processApiList(AppRequestDTO appRequest) {
|
||||
|
||||
if (!StringUtils.isEmpty(appRequest.getApiList())) {
|
||||
@@ -192,4 +171,140 @@ public class AppServiceFacade {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiKeyRegistrationDTO를 기반으로 AppRequest를 생성합니다.
|
||||
*
|
||||
* @param registration API Key 등록 정보
|
||||
* @param portalOrg 포털 조직
|
||||
* @return 생성된 AppRequest DTO
|
||||
* @throws IOException 파일 처리 중 오류 발생 시
|
||||
*/
|
||||
public AppRequestDTO createAppRequestFromRegistration(ApiKeyRegistrationDTO registration, PortalOrg portalOrg) throws IOException {
|
||||
// 1. 앱 아이콘 파일 저장
|
||||
String appIconFileId = null;
|
||||
|
||||
if (registration.getAppIconData() != null && registration.getAppIconData().length > 0) {
|
||||
FileInfo fileInfo = fileService.createSingleFileFromBytes(registration.getAppIconFileName(), registration.getAppIconContentType(), registration.getAppIconData());
|
||||
appIconFileId = fileInfo.getFileId();
|
||||
}
|
||||
|
||||
// 2. AppRequestDTO 생성 및 필드 매핑
|
||||
AppRequestDTO appRequestDTO = new AppRequestDTO();
|
||||
appRequestDTO.setOrg(portalOrgMapper.toVo(portalOrg));
|
||||
appRequestDTO.setType(AppRequestType.NEW);
|
||||
|
||||
// 기본 정보 매핑
|
||||
appRequestDTO.setClientName(registration.getAppName());
|
||||
appRequestDTO.setAppDescription(registration.getAppDescription());
|
||||
appRequestDTO.setCallbackUrl(registration.getCallbackUrl());
|
||||
appRequestDTO.setAppIconFileId(appIconFileId);
|
||||
|
||||
// IP 화이트리스트 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getIpWhitelist() != null && !registration.getIpWhitelist().isEmpty()) {
|
||||
appRequestDTO.setIpWhitelist(String.join(",", registration.getIpWhitelist()));
|
||||
}
|
||||
|
||||
// 선택된 API 목록 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getSelectedApis() != null && !registration.getSelectedApis().isEmpty()) {
|
||||
appRequestDTO.setApiList(String.join(",", registration.getSelectedApis()));
|
||||
}
|
||||
|
||||
// 3. 기존 createAppRequest 메소드 호출하여 승인 프로세스 시작
|
||||
return createAppRequest(appRequestDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiKeyRegistrationDTO를 기반으로 수정 AppRequest를 생성합니다.
|
||||
*
|
||||
* @param registration API Key 수정 정보
|
||||
* @param portalOrg 포털 조직
|
||||
* @return 생성된 AppRequest DTO
|
||||
* @throws IOException 파일 처리 중 오류 발생 시
|
||||
*/
|
||||
public AppRequestDTO createModifyRequestFromRegistration(ApiKeyRegistrationDTO registration, PortalOrg portalOrg) throws IOException {
|
||||
// 1. 기존 Credential 정보 조회
|
||||
ClientDTO existingClient = getApiKey(portalOrg.getId(), registration.getClientId());
|
||||
if (existingClient == null) {
|
||||
throw new NotFoundException("Client not found: " + registration.getClientId());
|
||||
}
|
||||
|
||||
// 2. 앱 아이콘 파일 저장 (변경된 경우)
|
||||
String appIconFileId = null;
|
||||
|
||||
// 새로운 아이콘 데이터가 있으면 저장
|
||||
if (registration.getAppIconData() != null && registration.getAppIconData().length > 0) {
|
||||
FileInfo fileInfo = fileService.createSingleFileFromBytes(registration.getAppIconFileName(), registration.getAppIconContentType(), registration.getAppIconData());
|
||||
appIconFileId = fileInfo.getFileId();
|
||||
}
|
||||
// 새로운 아이콘이 없으면 기존 아이콘 ID 사용 (registration에서 가져오거나 기존 클라이언트에서 가져옴)
|
||||
else if (registration.getAppIconFileId() != null) {
|
||||
appIconFileId = registration.getAppIconFileId();
|
||||
} else {
|
||||
appIconFileId = existingClient.getAppIconFileId();
|
||||
}
|
||||
|
||||
// 3. AppRequestDTO 생성 및 필드 매핑
|
||||
AppRequestDTO appRequestDTO = new AppRequestDTO();
|
||||
appRequestDTO.setOrg(portalOrgMapper.toVo(portalOrg));
|
||||
appRequestDTO.setType(AppRequestType.MODIFY);
|
||||
appRequestDTO.setClientId(registration.getClientId()); // 수정할 클라이언트 ID 설정
|
||||
|
||||
// 기본 정보 매핑
|
||||
appRequestDTO.setClientName(registration.getAppName());
|
||||
appRequestDTO.setAppDescription(registration.getAppDescription());
|
||||
appRequestDTO.setCallbackUrl(registration.getCallbackUrl());
|
||||
appRequestDTO.setAppIconFileId(appIconFileId);
|
||||
|
||||
// IP 화이트리스트 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getIpWhitelist() != null && !registration.getIpWhitelist().isEmpty()) {
|
||||
appRequestDTO.setIpWhitelist(String.join(",", registration.getIpWhitelist()));
|
||||
}
|
||||
|
||||
// 선택된 API 목록 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getSelectedApis() != null && !registration.getSelectedApis().isEmpty()) {
|
||||
appRequestDTO.setApiList(String.join(",", registration.getSelectedApis()));
|
||||
}
|
||||
|
||||
// 4. 기존 createAppRequest 메소드 호출하여 승인 프로세스 시작
|
||||
// createAppRequest 내부에서 MODIFY 타입인 경우 기존 API 목록을 prevApiList에 저장함
|
||||
return createAppRequest(appRequestDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 비밀번호를 검증합니다.
|
||||
* Client Secret 조회 등 민감한 작업 수행 시 본인 확인을 위해 사용됩니다.
|
||||
*
|
||||
* @param user 인증된 사용자 정보
|
||||
* @param password 검증할 비밀번호 (평문)
|
||||
* @return 비밀번호 일치 여부
|
||||
*/
|
||||
public boolean verifyUserPassword(PortalAuthenticatedUser user, String password) {
|
||||
if (user == null || password == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// PasswordEncoder를 사용하여 평문 비밀번호와 저장된 해시를 비교
|
||||
return passwordEncoder.matches(password, user.getPassword());
|
||||
} catch (Exception e) {
|
||||
// 비밀번호 검증 중 오류 발생 시 false 반환 (보안상 구체적인 오류 노출 방지)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key(Credential)를 즉시 삭제합니다.
|
||||
* 승인 프로세스 없이 바로 삭제 처리됩니다.
|
||||
*
|
||||
* @param orgId 조직 ID
|
||||
* @param clientId 삭제할 클라이언트 ID
|
||||
* @throws NotFoundException 클라이언트를 찾을 수 없는 경우
|
||||
*/
|
||||
public void deleteApp(String orgId, String clientId) {
|
||||
Credential credential = credentialRepository.findByClientidAndOrgid(clientId, orgId)
|
||||
.orElseThrow(() -> new NotFoundException("Client not found: " + clientId));
|
||||
|
||||
credentialRepository.delete(credential);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.eactive.apim.portal.apps.auth.service;
|
||||
|
||||
public interface AuthNumberService {
|
||||
|
||||
void sendRequestAuthNumber(String recipientKey, String msgType);
|
||||
String sendRequestAuthNumber(String recipientKey, String msgType);
|
||||
|
||||
boolean verifyAuthNumber(String recipientKey, String authNumber);
|
||||
}
|
||||
|
||||
@@ -40,10 +40,11 @@ public class AuthNumberServiceImpl implements AuthNumberService {
|
||||
/**
|
||||
* @param recipientKey
|
||||
* @param msgType
|
||||
* @return 생성된 인증번호
|
||||
*/
|
||||
@Override
|
||||
@Transactional(noRollbackFor = AuthNumberException.class)
|
||||
public void sendRequestAuthNumber(String recipientKey, String msgType) {
|
||||
public String sendRequestAuthNumber(String recipientKey, String msgType) {
|
||||
logger.info("Sending auth number to: {} via {}", recipientKey, msgType);
|
||||
|
||||
validateResendTime(recipientKey);
|
||||
@@ -55,6 +56,8 @@ public class AuthNumberServiceImpl implements AuthNumberService {
|
||||
|
||||
storage.saveAuthNumber(recipientKey, authNumber,
|
||||
LocalDateTime.now().plusSeconds(authNumberExpirationTime));
|
||||
|
||||
return authNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.eactive.apim.portal.apps.commission.constant;
|
||||
|
||||
/**
|
||||
* Application constants.
|
||||
*/
|
||||
public final class Constants {
|
||||
|
||||
public static final String SYSTEM_ACCOUNT = "system";
|
||||
|
||||
public static final String APIM_FOR_OBP_KEY = "x-obp-trust-system";
|
||||
|
||||
public static final String APIM_CODE_FOR_OBP_KEY = "x-obp-partnercode";
|
||||
|
||||
public static final String SMS_URL = "/api/messaging/sms";
|
||||
|
||||
public static final String COMMISSION_URL = "/api/billing/billing/findBillingForCondition";
|
||||
|
||||
public static final String COMMISSION_PRINT_URL = "/api/billing/billing/findBillingForCondition/print";
|
||||
|
||||
public static final String OBP_ORGANIZATION_URL = "/api/customer/findCustomerByBusinessManRegistrationNo/";
|
||||
|
||||
public static String LANGUAGE = "ko";
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
}
|
||||
Executable
+320
@@ -0,0 +1,320 @@
|
||||
package com.eactive.apim.portal.apps.commission.controller;
|
||||
|
||||
import com.eactive.apim.portal.apps.commission.constant.Constants;
|
||||
import com.eactive.apim.portal.apps.commission.dto.CommissionDTO;
|
||||
import com.eactive.apim.portal.apps.commission.dto.CommissionSearch;
|
||||
import com.eactive.apim.portal.apps.commission.exception.ErrorUtil;
|
||||
import com.eactive.apim.portal.apps.commission.service.ExternalService;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
|
||||
@Controller
|
||||
@RequestMapping(value = "/commission")
|
||||
@Secured("ROLE_APP")
|
||||
@RequiredArgsConstructor
|
||||
public class CommissionController {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(CommissionController.class);
|
||||
|
||||
private final ExternalService externalService;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
@GetMapping("/manage")
|
||||
public String commissionManagePage(Model model) {
|
||||
// 기본 정보 설정
|
||||
model.addAttribute("organization", SecurityUtil.getUserOrg());
|
||||
model.addAttribute("noOrgCode", StringUtils.isEmpty(SecurityUtil.getUserOrg().getOrgCode()));
|
||||
return "apps/commission/commissionManage";
|
||||
}
|
||||
|
||||
@GetMapping("/print/{year}/{month}")
|
||||
public String commissionPrintPage(
|
||||
@PathVariable("year") String year,
|
||||
@PathVariable("month") String month,
|
||||
Model model) {
|
||||
|
||||
CommissionSearch search = new CommissionSearch();
|
||||
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
|
||||
search.setYear(year);
|
||||
search.setMonth(month);
|
||||
|
||||
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_PRINT_URL);
|
||||
|
||||
if (result.hasError()) {
|
||||
model.addAttribute("error", result.getError());
|
||||
return "apps/commission/commissionPrint";
|
||||
}
|
||||
|
||||
// Model에 데이터 바인딩
|
||||
model.addAttribute("commission", result.getData());
|
||||
model.addAttribute("organization", SecurityUtil.getUserOrg().getOrgName());
|
||||
model.addAttribute("year", year);
|
||||
model.addAttribute("month", month);
|
||||
model.addAttribute("toDay", java.time.LocalDate.now().toString());
|
||||
|
||||
return "apps/commission/commissionPrint";
|
||||
}
|
||||
|
||||
// ==================== API Endpoints ====================
|
||||
|
||||
@PostMapping("/print/check")
|
||||
public ResponseEntity<?> checkPrintData(@RequestBody CommissionSearch search) {
|
||||
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
|
||||
|
||||
if (StringUtils.isEmpty(search.getPartnerCode())) {
|
||||
return ErrorUtil.create("fail.commission.needPartnerCode");
|
||||
}
|
||||
|
||||
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_PRINT_URL);
|
||||
|
||||
if (result.hasError()) {
|
||||
return ErrorUtil.create("fail.commission.print", result.getError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().body(result.getData());
|
||||
}
|
||||
|
||||
@PostMapping()
|
||||
public ResponseEntity<?> list(@RequestBody CommissionSearch search) {
|
||||
search.setPartnerCode(SecurityUtil.getUserOrg().getOrgCode());
|
||||
|
||||
if (StringUtils.isEmpty(search.getPartnerCode())) {
|
||||
return ErrorUtil.create("fail.commission.needPartnerCode");
|
||||
}
|
||||
|
||||
CommissionResult result = fetchCommissionData(search, Constants.COMMISSION_URL);
|
||||
|
||||
if (result.hasError()) {
|
||||
return ErrorUtil.create("fail.commission.list", result.getError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().body(result.getData());
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
/**
|
||||
* 외부 API를 호출하여 수수료 데이터를 조회합니다.
|
||||
*
|
||||
* @param search 검색 조건
|
||||
* @param apiPath API 경로 (Constants.COMMISSION_URL 또는 Constants.COMMISSION_PRINT_URL)
|
||||
* @return 조회 결과
|
||||
*/
|
||||
private CommissionResult fetchCommissionData(CommissionSearch search, String apiPath) {
|
||||
if (StringUtils.isEmpty(search.getPartnerCode())) {
|
||||
return CommissionResult.error("파트너 코드가 없습니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
String obmUrl = portalPropertyService.getPortalPropertiesAsMap("Portal").getOrDefault("obp.obm.url", "");
|
||||
String url = obmUrl + apiPath;
|
||||
|
||||
// 첫 번째 API 호출
|
||||
String result = externalService.getResponseHttpPost(url, toJson(search));
|
||||
if (StringUtils.isEmpty(result)) {
|
||||
return CommissionResult.error("외부 서버 연결에 실패하였습니다.");
|
||||
}
|
||||
|
||||
HashMap<String, Object> returnObj = objectMapper.readValue(result, HashMap.class);
|
||||
|
||||
// 에러 응답 체크
|
||||
String errorMessage = extractErrorMessage(returnObj);
|
||||
if (errorMessage != null) {
|
||||
return CommissionResult.error(errorMessage);
|
||||
}
|
||||
|
||||
// Together 관련 추가 처리 (000002-01)
|
||||
if (StringUtils.pathEquals(search.getPartnerCode(), "000002-01")) {
|
||||
log.debug("Found Together : " + search.getPartnerCode());
|
||||
|
||||
CommissionSearch togetherSearch = new CommissionSearch();
|
||||
togetherSearch.setPartnerCode("000002-02");
|
||||
togetherSearch.setYear(search.getYear());
|
||||
togetherSearch.setMonth(search.getMonth());
|
||||
|
||||
String togetherResult = externalService.getResponseHttpPost(url, toJson(togetherSearch));
|
||||
if (StringUtils.isEmpty(togetherResult)) {
|
||||
return CommissionResult.error("외부 서버 연결에 실패하였습니다.");
|
||||
}
|
||||
|
||||
HashMap<String, Object> togetherReturnObj = objectMapper.readValue(togetherResult, HashMap.class);
|
||||
log.debug("togetherLendingReturnObj : " + togetherReturnObj.toString());
|
||||
|
||||
String togetherErrorMessage = extractErrorMessage(togetherReturnObj);
|
||||
if (togetherErrorMessage != null) {
|
||||
return CommissionResult.error(togetherErrorMessage);
|
||||
}
|
||||
|
||||
HashMap<String, Object> mergeMap = mergeCommissionData(returnObj, togetherReturnObj);
|
||||
result = objectMapper.writeValueAsString(mergeMap);
|
||||
}
|
||||
|
||||
CommissionDTO commissionDTO = objectMapper.readValue(result, CommissionDTO.class);
|
||||
return CommissionResult.success(commissionDTO);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage(), e);
|
||||
return CommissionResult.error("수수료 조회에 실패하였습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답에서 에러 메시지를 추출합니다.
|
||||
*/
|
||||
private String extractErrorMessage(HashMap<String, Object> response) {
|
||||
if (!StringUtils.isEmpty(response.get("error"))) {
|
||||
HashMap<String, Object> error = (HashMap) response.get("error");
|
||||
return error.get("message").toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String toJson(CommissionSearch search) {
|
||||
try {
|
||||
Map<String, String> jsonMap = new HashMap<>();
|
||||
jsonMap.put("partnerCode", search.getPartnerCode());
|
||||
// 월을 두 자리로 패딩 (예: 1 -> 01, 12 -> 12)
|
||||
String paddedMonth = String.format("%02d", Integer.parseInt(search.getMonth()));
|
||||
jsonMap.put("targetOnMonth", search.getYear() + paddedMonth);
|
||||
|
||||
return objectMapper.writeValueAsString(jsonMap);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to serialize CommissionSearch to JSON", e);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse double value from HashMap
|
||||
* Handles both numeric types and string representations
|
||||
*/
|
||||
private double safeGetDouble(HashMap<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return 0.0;
|
||||
}
|
||||
try {
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else if (value instanceof String) {
|
||||
return Double.parseDouble((String) value);
|
||||
}
|
||||
return 0.0;
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Failed to parse double value for key: {} with value: {}", key, value);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<String, Object> mergeCommissionData(HashMap<String, Object> o1, HashMap<String, Object> o2) {
|
||||
|
||||
HashMap<String, Object> mergeMap = new HashMap<String, Object>();
|
||||
|
||||
String o1Status = MapUtils.getString(o1, "status");
|
||||
String o2Status = MapUtils.getString(o2, "status");
|
||||
String resultStatus = null;
|
||||
if (Integer.parseInt(o1Status) <= Integer.parseInt(o2Status)) {
|
||||
resultStatus = o1Status;
|
||||
} else {
|
||||
resultStatus = o2Status;
|
||||
}
|
||||
double apiTotalValueSum = safeGetDouble(o1, "apiTotalValueSum") + safeGetDouble(o2, "apiTotalValueSum");
|
||||
double apiSum = safeGetDouble(o1, "apiSum") + safeGetDouble(o2, "apiSum");
|
||||
double apiTotalValueSum2 = safeGetDouble(o1, "apiTotalValueSum2") + safeGetDouble(o2, "apiTotalValueSum2");
|
||||
double apiSum2 = safeGetDouble(o1, "apiSum2") + safeGetDouble(o2, "apiSum2");
|
||||
double productTotalValueCountSum = safeGetDouble(o1, "productTotalValueCountSum") + safeGetDouble(o2, "productTotalValueCountSum");
|
||||
double productTotalValueAmountSum = safeGetDouble(o1, "productTotalValueAmountSum") + safeGetDouble(o2, "productTotalValueAmountSum");
|
||||
double productSum = safeGetDouble(o1, "productSum") + safeGetDouble(o2, "productSum");
|
||||
double productTotalValueCountSum2 = safeGetDouble(o1, "productTotalValueCountSum2") + safeGetDouble(o2, "productTotalValueCountSum2");
|
||||
double productTotalValueAmountSum2 = safeGetDouble(o1, "productTotalValueAmountSum2") + safeGetDouble(o2, "productTotalValueAmountSum2");
|
||||
double productSum2 = safeGetDouble(o1, "productSum2") + safeGetDouble(o2, "productSum2");
|
||||
double totalValueCountSum = safeGetDouble(o1, "totalValueCountSum") + safeGetDouble(o2, "totalValueCountSum");
|
||||
double totalValueCountSum2 = safeGetDouble(o1, "totalValueCountSum2") + safeGetDouble(o2, "totalValueCountSum2");
|
||||
double totalValueAmountSum = safeGetDouble(o1, "totalValueAmountSum") + safeGetDouble(o2, "totalValueAmountSum");
|
||||
double totalValueAmountSum2 = safeGetDouble(o1, "totalValueAmountSum2") + safeGetDouble(o2, "totalValueAmountSum2");
|
||||
double sum = safeGetDouble(o1, "sum") + safeGetDouble(o2, "sum");
|
||||
double sum2 = safeGetDouble(o1, "sum2") + safeGetDouble(o2, "sum2");
|
||||
|
||||
List<Map> o1List = (ArrayList) o1.get("list");
|
||||
List<Map> o2pList = (ArrayList) o2.get("list");
|
||||
o1List.addAll(o2pList);
|
||||
|
||||
mergeMap.put("status", resultStatus);
|
||||
mergeMap.put("apiTotalValueSum", String.format("%.2f", apiTotalValueSum));
|
||||
mergeMap.put("apiSum", String.format("%.2f", apiSum));
|
||||
mergeMap.put("apiTotalValueSum2", String.format("%.2f", apiTotalValueSum2));
|
||||
mergeMap.put("apiSum2", String.format("%.2f", apiSum2));
|
||||
mergeMap.put("productTotalValueCountSum", String.format("%.2f", productTotalValueCountSum));
|
||||
mergeMap.put("productTotalValueAmountSum", String.format("%.2f", productTotalValueAmountSum));
|
||||
mergeMap.put("productSum", String.format("%.2f", productSum));
|
||||
mergeMap.put("productTotalValueCountSum2", String.format("%.2f", productTotalValueCountSum2));
|
||||
mergeMap.put("productTotalValueAmountSum2", String.format("%.2f", productTotalValueAmountSum2));
|
||||
mergeMap.put("productSum2", String.format("%.2f", productSum2));
|
||||
mergeMap.put("totalValueCountSum", String.format("%.2f", totalValueCountSum));
|
||||
mergeMap.put("totalValueCountSum2", String.format("%.2f", totalValueCountSum2));
|
||||
mergeMap.put("totalValueAmountSum", String.format("%.2f", totalValueAmountSum));
|
||||
mergeMap.put("totalValueAmountSum2", String.format("%.2f", totalValueAmountSum2));
|
||||
mergeMap.put("sum", String.format("%.2f", sum));
|
||||
mergeMap.put("sum2", String.format("%.2f", sum2));
|
||||
mergeMap.put("list", o1List);
|
||||
|
||||
return mergeMap;
|
||||
}
|
||||
|
||||
// ==================== Inner Classes ====================
|
||||
|
||||
/**
|
||||
* 수수료 조회 결과를 담는 내부 클래스
|
||||
*/
|
||||
private static class CommissionResult {
|
||||
private CommissionDTO data;
|
||||
private String error;
|
||||
|
||||
static CommissionResult success(CommissionDTO data) {
|
||||
CommissionResult result = new CommissionResult();
|
||||
result.data = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
static CommissionResult error(String message) {
|
||||
CommissionResult result = new CommissionResult();
|
||||
result.error = message;
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean hasError() {
|
||||
return error != null;
|
||||
}
|
||||
|
||||
CommissionDTO getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.eactive.apim.portal.apps.commission.dto;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CommissionDTO {
|
||||
|
||||
private String status;
|
||||
private String apiTotalValueSum;
|
||||
private String apiSum;
|
||||
private String apiTotalValueSum2;
|
||||
private String apiSum2;
|
||||
private String productTotalValueCountSum;
|
||||
private String productTotalValueAmountSum;
|
||||
private String productSum;
|
||||
private String productTotalValueCountSum2;
|
||||
private String productTotalValueAmountSum2;
|
||||
private String productSum2;
|
||||
private String totalValueCountSum;
|
||||
private String totalValueCountSum2;
|
||||
private String totalValueAmountSum;
|
||||
private String totalValueAmountSum2;
|
||||
private String sum;
|
||||
private String sum2;
|
||||
private List<CommissionList> list;
|
||||
private String documentFormatNo;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.eactive.apim.portal.apps.commission.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Created by jskim on 17. 01. 26.
|
||||
*
|
||||
* @author jskim
|
||||
*/
|
||||
@Data
|
||||
public class CommissionList {
|
||||
|
||||
private String paymentTargetName;
|
||||
private String paymentTargetType;
|
||||
private String value;
|
||||
private String minimumAmount;
|
||||
private String maximumAmount;
|
||||
private String totalCount;
|
||||
private String totalCount2;
|
||||
private String totalValue;
|
||||
private String totalValue2;
|
||||
private String minimumAmount2;
|
||||
private String maximumAmount2;
|
||||
private String paymentBaseName;
|
||||
private String paymentTimingName;
|
||||
private String valueDivisionName;
|
||||
private String sum;
|
||||
private String sum2;
|
||||
private String detailFeeTypeName;
|
||||
private String value2;
|
||||
private String changeReason2;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.eactive.apim.portal.apps.commission.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CommissionSearch {
|
||||
|
||||
@ApiModelProperty(value = "대상년")
|
||||
private String year;
|
||||
|
||||
@ApiModelProperty(value = "대상월")
|
||||
private String month;
|
||||
|
||||
private String partnerCode;
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.eactive.apim.portal.apps.commission.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* Created by ybsong on 16. 12. 7.
|
||||
*
|
||||
* @author ybsong
|
||||
*/
|
||||
public class ErrorUtil {
|
||||
public static ResponseEntity<ErrorVM> create(HttpStatus status, String message, String description) {
|
||||
return new ResponseEntity<>(new ErrorVM(message, description), status);
|
||||
}
|
||||
|
||||
public static ResponseEntity<ErrorVM> create(HttpStatus status, String message) {
|
||||
return create(status, message, null);
|
||||
}
|
||||
|
||||
public static ResponseEntity<ErrorVM> create(String message, String description) {
|
||||
return create(HttpStatus.BAD_REQUEST, message, description);
|
||||
}
|
||||
|
||||
public static ResponseEntity<ErrorVM> create(String message) {
|
||||
return create(message, null);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.eactive.apim.portal.apps.commission.exception;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* View Model for transferring error message with a list of field errors.
|
||||
*/
|
||||
@Data
|
||||
public class ErrorVM implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String message;
|
||||
private final String description;
|
||||
|
||||
private List<FieldErrorVM> fieldErrors;
|
||||
|
||||
public ErrorVM(String message) {
|
||||
this(message, null);
|
||||
}
|
||||
|
||||
public ErrorVM(String message, String description) {
|
||||
this.message = message;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public ErrorVM(String message, String description, List<FieldErrorVM> fieldErrors) {
|
||||
this.message = message;
|
||||
this.description = description;
|
||||
this.fieldErrors = fieldErrors;
|
||||
}
|
||||
|
||||
public void add(String objectName, String field, String message) {
|
||||
if (fieldErrors == null) {
|
||||
fieldErrors = new ArrayList<>();
|
||||
}
|
||||
fieldErrors.add(new FieldErrorVM(objectName, field, message));
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.eactive.apim.portal.apps.commission.exception;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class FieldErrorVM implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String objectName;
|
||||
|
||||
private final String field;
|
||||
|
||||
private final String message;
|
||||
|
||||
public FieldErrorVM(String dto, String field, String message) {
|
||||
this.objectName = dto;
|
||||
this.field = field;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getObjectName() {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.eactive.apim.portal.apps.commission.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.commission.constant.Constants;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* 외부 API 호출 서비스
|
||||
* RestTemplate 기반으로 외부 시스템과 HTTP 통신을 수행합니다.
|
||||
*
|
||||
* @author ybsong
|
||||
* @since 2017-03-02
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalService {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(ExternalService.class);
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
/**
|
||||
* HTTP POST 요청을 통해 외부 API를 호출하고 응답을 받습니다.
|
||||
*
|
||||
* @param url 호출할 API URL
|
||||
* @param payload 요청 본문 (JSON 문자열)
|
||||
* @return API 응답 본문 (문자열), 오류 발생 시 null
|
||||
*/
|
||||
public String getResponseHttpPost(String url, String payload) {
|
||||
try {
|
||||
|
||||
Map<String, String> portalProperties = portalPropertyService.getPortalPropertiesAsMap("Portal");
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set(Constants.APIM_FOR_OBP_KEY, portalProperties.getOrDefault("obp.api_key","APIMPT-0002-QVBJTVBULTAwMDI="));
|
||||
headers.set(Constants.APIM_CODE_FOR_OBP_KEY, portalProperties.getOrDefault("obp.partner_code","100001-01"));
|
||||
|
||||
// HTTP 요청 엔티티 생성 (헤더 + 본문)
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(payload, headers);
|
||||
|
||||
// POST 요청 실행
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
|
||||
|
||||
// 응답 본문 반환
|
||||
return response.getBody();
|
||||
|
||||
} catch (HttpStatusCodeException e) {
|
||||
// 4xx, 5xx 에러더라도 응답 본문이 있으면 반환 (에러 메시지 포함)
|
||||
String responseBody = e.getResponseBodyAsString();
|
||||
log.warn("HTTP error from external API: url={}, status={}, body={}", url, e.getStatusCode(), responseBody);
|
||||
if (responseBody != null && !responseBody.isEmpty()) {
|
||||
return responseBody;
|
||||
}
|
||||
return null;
|
||||
} catch (RestClientException e) {
|
||||
log.error("Failed to call external API: url={}, error={}", url, e.getMessage(), e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error during HTTP POST: url={}", url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -1,8 +1,9 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.controller;
|
||||
package com.eactive.apim.portal.apps.community.partnership.controller;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.service.PartnershipApplicationFacade;
|
||||
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnership.service.PartnershipApplicationFacade;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import java.io.IOException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -38,13 +39,14 @@ public class PartnershipApplicationController {
|
||||
}
|
||||
|
||||
model.addAttribute("partnership", new PartnershipApplicationDTO());
|
||||
model.addAttribute("isInternalUser", UserTypeUtil.isCurrentUserInternal());
|
||||
return "apps/community/mainPartnershipForm";
|
||||
}
|
||||
|
||||
|
||||
@PostMapping
|
||||
public String createPartnershipApplication(@Valid @ModelAttribute PartnershipApplicationDTO partnershipApplicationDTO,
|
||||
BindingResult bindingResult, HttpServletRequest httpServletRequest, RedirectAttributes redirectAttributes) throws IOException {
|
||||
BindingResult bindingResult, HttpServletRequest httpServletRequest, RedirectAttributes redirectAttributes) throws IOException {
|
||||
if (bindingResult.hasErrors()) {
|
||||
return "apps/community/mainPartnershipForm";
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.dto;
|
||||
package com.eactive.apim.portal.apps.community.partnership.dto;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.mapper;
|
||||
package com.eactive.apim.portal.apps.community.partnership.mapper;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.common.mapper.CommonMapper;
|
||||
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
|
||||
import org.mapstruct.Mapper;
|
||||
@@ -9,8 +9,8 @@ import org.mapstruct.ReportingPolicy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Mapper(componentModel = "spring",
|
||||
uses = {CommonMapper.class},
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
uses = {CommonMapper.class},
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Component
|
||||
|
||||
public interface PartnershipApplicationMapper {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.repository;
|
||||
package com.eactive.apim.portal.apps.community.partnership.repository;
|
||||
|
||||
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.service;
|
||||
package com.eactive.apim.portal.apps.community.partnership.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface PartnershipApplicationFacade {
|
||||
+15
-5
@@ -1,7 +1,10 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.service;
|
||||
package com.eactive.apim.portal.apps.community.partnership.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.mapper.PartnershipApplicationMapper;
|
||||
import com.eactive.apim.portal.apps.community.partnership.dto.PartnershipApplicationDTO;
|
||||
import com.eactive.apim.portal.apps.community.partnership.mapper.PartnershipApplicationMapper;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.file.service.FileTypeContext;
|
||||
@@ -25,8 +28,15 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF
|
||||
|
||||
FileInfo file = null;
|
||||
if (!partnershipApplicationDTO.getFiles().isEmpty()) {
|
||||
FileTypeContext.setFileType("business");
|
||||
file = fileService.createFile(partnershipApplicationDTO.getFiles());
|
||||
try {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
FileTypeContext.setFileType("business");
|
||||
file = fileService.createFile(partnershipApplicationDTO.getFiles());
|
||||
} finally {
|
||||
FileService.clearInternalUserContext();
|
||||
}
|
||||
}
|
||||
PartnershipApplication partnershipApplication = partnershipApplicationMapper.toEntity(partnershipApplicationDTO);
|
||||
if (file != null) {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package com.eactive.apim.portal.apps.community.partnershipapplication.service;
|
||||
package com.eactive.apim.portal.apps.community.partnership.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.partnershipapplication.repository.PartnershipApplicationRepository;
|
||||
import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository;
|
||||
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
+3
-8
@@ -5,9 +5,9 @@ import com.eactive.apim.portal.apps.community.qna.dto.InquirySearch;
|
||||
import com.eactive.apim.portal.apps.community.qna.service.InquiryFacade;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.eactive.apim.portal.file.exception.InvalidFileException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -68,6 +68,7 @@ public class InquiryController {
|
||||
@GetMapping("/new")
|
||||
public String newInquiryForm(Model model) {
|
||||
model.addAttribute("inquiry", new InquiryDTO());
|
||||
model.addAttribute("isInternalUser", UserTypeUtil.isCurrentUserInternal());
|
||||
return APPS_COMMUNITY_MAIN_INQUIRY_FORM;
|
||||
}
|
||||
|
||||
@@ -78,6 +79,7 @@ public class InquiryController {
|
||||
public String editInquiryForm(@RequestParam("id") String id, Model model) {
|
||||
InquiryDTO inquiry = inquiryFacade.getMyInquiry(SecurityUtil.getPortalAuthenticatedUser(), id);
|
||||
model.addAttribute("inquiry", inquiry);
|
||||
model.addAttribute("isInternalUser", UserTypeUtil.isCurrentUserInternal());
|
||||
return APPS_COMMUNITY_MAIN_INQUIRY_FORM;
|
||||
}
|
||||
|
||||
@@ -123,11 +125,4 @@ public class InquiryController {
|
||||
redirectAttributes.addFlashAttribute("success", "삭제 되었습니다.");
|
||||
return REDIRECT_INQUIRY;
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidFileException.class)
|
||||
public String handleInvalidFileException(InvalidFileException e, Model model) {
|
||||
model.addAttribute("inquiry", new InquiryDTO());
|
||||
model.addAttribute("error", e.getMessage());
|
||||
return "redirect:/inquiry/new";
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apps.community.qna.mapper.InquiryMapper;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.file.service.FileTypeContext;
|
||||
@@ -83,8 +84,15 @@ public class InquiryFacadeImpl implements InquiryFacade {
|
||||
|
||||
FileInfo file = null;
|
||||
if (!inquiryDTO.getFiles().isEmpty()) {
|
||||
FileTypeContext.setFileType("qna");
|
||||
file = fileService.createFile(inquiryDTO.getFiles());
|
||||
try {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
FileTypeContext.setFileType("qna");
|
||||
file = fileService.createFile(inquiryDTO.getFiles());
|
||||
} finally {
|
||||
FileService.clearInternalUserContext();
|
||||
}
|
||||
}
|
||||
|
||||
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
||||
@@ -104,12 +112,17 @@ public class InquiryFacadeImpl implements InquiryFacade {
|
||||
public void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException {
|
||||
Inquiry existingInquiry = inquiryService.getMyInquiry(user, id);
|
||||
|
||||
// 파일이 새로 업로드된 경우
|
||||
if (!inquiryDTO.getFiles().isEmpty() && !inquiryDTO.getFiles().get(0).isEmpty()) {
|
||||
FileTypeContext.setFileType("qna");
|
||||
FileInfo file = fileService.createFile(inquiryDTO.getFiles());
|
||||
if(file != null) {
|
||||
inquiryDTO.setAttachFile(file.getFileId());
|
||||
try {
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
FileTypeContext.setFileType("qna");
|
||||
FileInfo file = fileService.createFile(inquiryDTO.getFiles());
|
||||
if(file != null) {
|
||||
inquiryDTO.setAttachFile(file.getFileId());
|
||||
}
|
||||
} finally {
|
||||
FileService.clearInternalUserContext();
|
||||
}
|
||||
} else if (existingInquiry.getAttachFile() != null && inquiryDTO.getAttachFile() == null) {
|
||||
fileService.deleteFile(inquiryDTO.getAttachFile());
|
||||
|
||||
+1
-4
@@ -4,7 +4,6 @@ import com.eactive.apim.portal.apps.dashboard.service.DashboardService;
|
||||
import com.eactive.apim.portal.common.pagerouter.PageHandler;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -22,9 +21,7 @@ public class DashboardHandler implements PageHandler {
|
||||
if (SecurityUtil.getPortalAuthenticatedUser() != null && SecurityUtil.getPortalAuthenticatedUser().getPortalOrg() != null) {
|
||||
PortalOrg org = SecurityUtil.getPortalAuthenticatedUser().getPortalOrg();
|
||||
model.addAttribute("pendingKeyCount", dashboardService.getPendingApiKeyRequestCount(org));
|
||||
model.addAttribute("testKeyCount", dashboardService.getTestApiKeyCount(org));
|
||||
model.addAttribute("pendingProdCount", dashboardService.getProdApiKeyRequestCount(org));
|
||||
model.addAttribute("prodKeyCount", dashboardService.getProdApiKeyCount(org));
|
||||
model.addAttribute("testKeyCount", dashboardService.getApiKeyCount(org));
|
||||
|
||||
model.addAttribute("totalApi", dashboardService.getTotalApi(org));
|
||||
model.addAttribute("dailyTotal", dashboardService.getDailyTotalApiUsage(org));
|
||||
|
||||
@@ -5,24 +5,18 @@ import com.eactive.apim.portal.app.entity.Credential;
|
||||
import com.eactive.apim.portal.app.repository.CredentialRepository;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.ApprovalStatus;
|
||||
import com.eactive.apim.portal.approval.statemachine.ProcessingState;
|
||||
import com.eactive.apim.portal.approval.statemachine.RequestedState;
|
||||
import com.eactive.apim.portal.apps.approval.service.ApprovalService;
|
||||
import com.eactive.apim.portal.apps.dashboard.ApiDailyStatistics;
|
||||
import com.eactive.apim.portal.apps.dashboard.repository.PortalStatisticsRepository;
|
||||
import com.eactive.apim.portal.apps.dashboard.repository.PortalStatisticsRepository.TopApiUsage;
|
||||
import com.eactive.apim.portal.common.util.DateUtil;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -39,15 +33,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class DashboardService {
|
||||
|
||||
private final static String PROD_SERVER = "PROD";
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final AppRequestRepository appRequestRepository;
|
||||
private final CredentialRepository credentialRepository;
|
||||
private final PortalStatisticsRepository portalStatisticsRepository;
|
||||
|
||||
public int getTestApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.countAllByOrgidAndServer(org.getId(), STG_SERVER);
|
||||
public int getApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.countAllByOrgid(org.getId());
|
||||
}
|
||||
|
||||
public int getPendingApiKeyRequestCount(PortalOrg org) {
|
||||
@@ -55,18 +46,9 @@ public class DashboardService {
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
}
|
||||
|
||||
public int getProdApiKeyRequestCount(PortalOrg org) {
|
||||
return appRequestRepository.countAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(org, Arrays.asList(AppRequestType.PROD_NEW, AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE),
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
}
|
||||
|
||||
public int getProdApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.findAllByOrgidAndServer(org.getId(), PROD_SERVER).size();
|
||||
}
|
||||
|
||||
public int getTotalApi(PortalOrg org) {
|
||||
|
||||
List<Credential> credentials = credentialRepository.findAllByOrgidAndServer(org.getId(), PROD_SERVER);
|
||||
List<Credential> credentials = credentialRepository.findAllByOrgid(org.getId());
|
||||
return (int) credentials.stream()
|
||||
.flatMap(credential -> credential.getApiList().stream())
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
|
||||
+50
@@ -39,4 +39,54 @@ public class FileDownloadController {
|
||||
IOUtils.write(fileContents, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 인라인 표시용 (브라우저에서 직접 렌더링)
|
||||
* 약관 등에서 이미지를 표시할 때 사용
|
||||
*/
|
||||
@GetMapping(value = "/view")
|
||||
public void viewImage(@RequestParam(value = "fileId") String fileId,
|
||||
@RequestParam(value = "fileSn", defaultValue = "1") int fileSn,
|
||||
HttpServletResponse response) throws IOException {
|
||||
|
||||
FileDetail fileDetail = fileService.getFileDetailByIds(fileId, fileSn);
|
||||
|
||||
// Content-Type 설정 (이미지용)
|
||||
String contentType = getImageContentType(fileDetail.getFileExtension());
|
||||
response.setContentType(contentType);
|
||||
response.setContentLength(fileDetail.getFileSize());
|
||||
|
||||
// inline으로 설정 (브라우저에서 직접 표시)
|
||||
response.setHeader("Content-Disposition", "inline");
|
||||
|
||||
// 캐시 설정 (1일)
|
||||
response.setHeader("Cache-Control", "public, max-age=86400");
|
||||
|
||||
byte[] fileContents = fileService.retrieveFileContent(fileDetail);
|
||||
IOUtils.write(fileContents, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
private String getImageContentType(String extension) {
|
||||
if (extension == null) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
switch (extension.toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return "image/jpeg";
|
||||
case "png":
|
||||
return "image/png";
|
||||
case "gif":
|
||||
return "image/gif";
|
||||
case "bmp":
|
||||
return "image/bmp";
|
||||
case "webp":
|
||||
return "image/webp";
|
||||
case "svg":
|
||||
return "image/svg+xml";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apps.user.dto.PortalUserDTO;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -79,7 +80,7 @@ public class AccountRecoveryController {
|
||||
}
|
||||
|
||||
try {
|
||||
PortalUserDTO foundId = portalUserAuthService.findUsersByNameAndMobile(findIdDTO.getUserName(), findIdDTO.getMobileNumber());
|
||||
List<PortalUserDTO> foundUsers = portalUserAuthService.findAllUsersByNameAndMobile(findIdDTO.getUserName(), findIdDTO.getMobileNumber());
|
||||
|
||||
// 인증 관련 세션 정보 제거
|
||||
session.removeAttribute("mobileNumber");
|
||||
@@ -87,7 +88,7 @@ public class AccountRecoveryController {
|
||||
|
||||
//redirect 가 아닌 경우에는 model 을 사용해서 페이지 렌더링 처리
|
||||
model.addAttribute("findIdDTO", findIdDTO);
|
||||
model.addAttribute("foundId", foundId);
|
||||
model.addAttribute("foundUsers", foundUsers);
|
||||
|
||||
return ACCOUNT_RECOVERY_RESULT;
|
||||
} catch (UserNotFoundException e) {
|
||||
@@ -112,14 +113,23 @@ public class AccountRecoveryController {
|
||||
|
||||
// 세션에서 인증 여부 확인
|
||||
Boolean isVerified = (Boolean) session.getAttribute("isVerified");
|
||||
String sessionMobileNumber = (String) session.getAttribute("mobileNumber");
|
||||
|
||||
if (isVerified == null || !isVerified) {
|
||||
setModelForError(redirectAttributes, "resetPassword", null, "휴대폰 번호 인증을 먼저 진행해주세요.", null, passwordReset);
|
||||
return "redirect:/account_recovery?tab=resetPassword";
|
||||
}
|
||||
|
||||
// 인증된 전화번호와 입력된 전화번호 일치 여부 확인
|
||||
if (passwordReset.getMobileNumber() == null ||
|
||||
!passwordReset.getMobileNumber().replace("-", "").equals(sessionMobileNumber)) {
|
||||
setModelForError(redirectAttributes, "resetPassword", null, "인증된 휴대폰 번호와 입력한 번호가 일치하지 않습니다.", null, passwordReset);
|
||||
return "redirect:/account_recovery?tab=resetPassword";
|
||||
}
|
||||
|
||||
try {
|
||||
// 사용자 정보 확인
|
||||
portalUserAuthService.initializePassword(
|
||||
portalUserAuthService.resetPassword(
|
||||
passwordReset.getLoginId(),
|
||||
passwordReset.getUserName(),
|
||||
passwordReset.getMobileNumber()
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
package com.eactive.apim.portal.apps.main.controller;
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO;
|
||||
import com.eactive.apim.portal.apps.community.notice.service.PortalNoticeFacade;
|
||||
import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO;
|
||||
import com.eactive.apim.portal.apps.main.service.IndexStatisticsService;
|
||||
import com.eactive.apim.portal.apps.main.service.MainApiFacade;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class IndexController {
|
||||
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
private final MainApiFacade mainApiFacade;
|
||||
private final PortalNoticeFacade portalNoticeFacade;
|
||||
private final IndexStatisticsService indexStatisticsService;
|
||||
|
||||
/**
|
||||
* 메인 페이지 API는 Portal Property 에 main.service.list 에 등록된 그룹을 기준으로 API를 조회함.
|
||||
* 프로퍼티에 등록되어 있어도 어드민의 그룹 표시 설정이 N이면 표시 되지 않음.
|
||||
* 어드민의 API 스펙의 표시 설정이 N이면 표시되지 않음.
|
||||
* 그룹 또는 권한이 지정된 경우, 해당사용자의 권한 또는 기관이 일치해야 표시됨.
|
||||
* 어드민의 API 스펙의 표시 설정이 N이면 표시되지 않음. 그룹 또는 권한이 지정된 경우, 해당사용자의 권한 또는 기관이 일치해야 표시됨.
|
||||
* <p>
|
||||
* 검색어는 Portal Property 또는 메인 관리 화면에 main.keyword.list 에 등록된 내용이 표시 됨.
|
||||
*
|
||||
@@ -40,147 +30,28 @@ public class IndexController {
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index(Model model) {
|
||||
// Step 1: Fetch and sort API services
|
||||
List<ApiServiceDTO> apiServices = getSortedApiServices(null);
|
||||
|
||||
// Step 2: Create a map of API ID to ApiServiceDTO for icon mapping
|
||||
Map<String, ApiServiceDTO> mainIconsMap = createMainIconsMap(apiServices);
|
||||
|
||||
// Step 3: Merge all API lists from apiServices
|
||||
List<ApiServiceApiDTO> allApis = getAllApis(apiServices);
|
||||
|
||||
// Step 4: Enrich API spec info DTOs with main icon and service info
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = enrichApiSpecInfo(apiServices, allApis, mainIconsMap);
|
||||
|
||||
// Step 5: Calculate total API count
|
||||
int totalApiCount = calculateTotalApiCount(apiServices);
|
||||
|
||||
// Step 6: Fetch portal notices and hashtags
|
||||
List<PortalNoticeDTO> portalNotices = portalNoticeFacade.getLatestNotices();
|
||||
List<ApiServiceDTO> services = mainApiFacade.getOpenApiServices();
|
||||
List<String> hashTags = mainApiFacade.getHashTags();
|
||||
|
||||
ApiGroupSearch search = new ApiGroupSearch();
|
||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
|
||||
// 인덱스 페이지 하단 통계 조회 (캐싱 적용)
|
||||
IndexStatisticsDTO statistics = indexStatisticsService.getStatistics();
|
||||
|
||||
int selectedApiCount = (int) searchResult.get("selectedApiCount");
|
||||
|
||||
// 서비스 이용 수
|
||||
int serviceCount = ((List<?>) searchResult.get("services")).size();
|
||||
|
||||
// 승인된 기업 건수
|
||||
int approvedOrgCount = mainApiFacade.getApprovedOrgCount();
|
||||
|
||||
// Step 7: Add attributes to model
|
||||
addAttributesToModel(model, apiServices, apiSearchFacade.sortApisByService(apiSpecInfoDtos, apiServices), totalApiCount, portalNotices, hashTags, serviceCount, approvedOrgCount, selectedApiCount);
|
||||
addAttributesToModel(model, services, hashTags, statistics);
|
||||
|
||||
return "apps/main/index";
|
||||
}
|
||||
|
||||
private List<ApiServiceDTO> getSortedApiServices(String apiId) {
|
||||
List<ApiServiceDTO> apiServices = mainApiFacade.getOpenApiServices(apiId);
|
||||
apiServices.sort(Comparator.comparing(ApiServiceDTO::getDisplayOrder));
|
||||
return apiServices;
|
||||
}
|
||||
|
||||
private Map<String, ApiServiceDTO> createMainIconsMap(List<ApiServiceDTO> apiServices) {
|
||||
Map<String, ApiServiceDTO> mainIconsMap = new HashMap<>();
|
||||
apiServices.forEach(service ->
|
||||
service.getApiGroupApiList().forEach(api ->
|
||||
mainIconsMap.put(api.getApiId(), service)
|
||||
)
|
||||
);
|
||||
return mainIconsMap;
|
||||
}
|
||||
|
||||
private List<ApiServiceApiDTO> getAllApis(List<ApiServiceDTO> apiServices) {
|
||||
return apiServices.stream()
|
||||
.flatMap(service -> service.getApiGroupApiList().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<ApiSpecInfoDto> enrichApiSpecInfo(List<ApiServiceDTO> services, List<ApiServiceApiDTO> allApis, Map<String, ApiServiceDTO> mainIconsMap) {
|
||||
List<String> apiIds = allApis.stream().map(ApiServiceApiDTO::getApiId).collect(Collectors.toList());
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = mainApiFacade.getOpenApis(apiIds);
|
||||
apiSpecInfoDtos.forEach(spec -> {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
if (serviceDTO != null) {
|
||||
spec.setMainIcon(serviceDTO.getMainIcon());
|
||||
spec.setService(serviceDTO.getGroupName());
|
||||
}
|
||||
});
|
||||
|
||||
boolean isAuthenticated = SecurityUtil.isAuthenticated();
|
||||
String roleCode;
|
||||
String org;
|
||||
|
||||
if (isAuthenticated) {
|
||||
roleCode = SecurityUtil.getPortalAuthenticatedUser().getRoleCode().toString();
|
||||
org = SecurityUtil.getUserOrg().getId();
|
||||
} else {
|
||||
roleCode = null;
|
||||
org = null;
|
||||
}
|
||||
|
||||
List<ApiSpecInfoDto> filteredApiSpecInfoDtos = apiSpecInfoDtos.stream()
|
||||
.filter(spec -> {
|
||||
if (!isAuthenticated) {
|
||||
return spec.getDisplayOrg() == null && spec.getDisplayRoleCode() == null;
|
||||
}
|
||||
boolean orgMatch = false;
|
||||
if (org != null) {
|
||||
orgMatch = spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty() || spec.getDisplayOrg().contains(org);
|
||||
}
|
||||
boolean roleMatch = spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty() || spec.getDisplayRoleCode().contains(roleCode);
|
||||
return orgMatch || roleMatch;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<String> filteredApiIds = filteredApiSpecInfoDtos.stream()
|
||||
.map(ApiSpecInfoDto::getApiId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
services.forEach(service -> {
|
||||
if (service.getApiGroupApiList() != null) {
|
||||
List<ApiServiceApiDTO> filteredApiList = service.getApiGroupApiList().stream()
|
||||
.filter(api -> filteredApiIds.contains(api.getApiId()))
|
||||
.collect(Collectors.toList());
|
||||
service.setApiGroupApiList(filteredApiList);
|
||||
}
|
||||
});
|
||||
|
||||
return filteredApiSpecInfoDtos.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int calculateTotalApiCount(List<ApiServiceDTO> apiServices) {
|
||||
return apiServices.stream()
|
||||
.mapToInt(service -> service.getApiGroupApiList().size())
|
||||
.sum();
|
||||
}
|
||||
|
||||
private void addAttributesToModel(Model model, List<ApiServiceDTO> apiServices,
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos, int totalApiCount,
|
||||
List<PortalNoticeDTO> portalNotices, List<String> hashTags, int serviceCount, int approvedOrgCount, int selectedApiCount) {
|
||||
private void addAttributesToModel(Model model, List<ApiServiceDTO> apiServices, List<String> hashTags, IndexStatisticsDTO statistics) {
|
||||
|
||||
model.addAttribute("services", apiServices);
|
||||
model.addAttribute("apis", apiSpecInfoDtos);
|
||||
model.addAttribute("totalApiCount", totalApiCount);
|
||||
model.addAttribute("portalNotices", portalNotices);
|
||||
model.addAttribute("hashtags", hashTags);
|
||||
|
||||
model.addAttribute("serviceCount", serviceCount);
|
||||
model.addAttribute("approvedOrgCount", approvedOrgCount);
|
||||
model.addAttribute("selectedApiCount", selectedApiCount);
|
||||
// 인덱스 페이지 하단 통계
|
||||
model.addAttribute("statisticsVisible", statistics.isVisible());
|
||||
model.addAttribute("approvedOrgCount", statistics.getApprovedOrgCount());
|
||||
model.addAttribute("serviceCount", statistics.getActiveAppCount());
|
||||
model.addAttribute("totalApiCount", statistics.getTotalApiCount());
|
||||
}
|
||||
|
||||
@GetMapping("/api_fragments")
|
||||
public String getApiFragments(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||
List<ApiServiceDTO> apiServices = getSortedApiServices(id);
|
||||
Map<String, ApiServiceDTO> mainIconsMap = createMainIconsMap(apiServices);
|
||||
List<ApiServiceApiDTO> allApis = getAllApis(apiServices);
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = enrichApiSpecInfo(apiServices, allApis, mainIconsMap);
|
||||
model.addAttribute("apis", apiSpecInfoDtos);
|
||||
return "apps/main/apiListFragment :: apiListFragment";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.eactive.apim.portal.apps.main.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 인덱스 페이지 하단 통계 DTO
|
||||
* - API 활용 기업: 법인으로 등록된 수의 합계 (정상 상태)
|
||||
* - 서비스 이용 수: 전체 법인이 생성한 앱의 합계 (이용 가능 상태)
|
||||
* - API 이용 건수: 전체 법인이 생성한 앱의 API 수의 합계 (이용 가능 상태)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IndexStatisticsDTO {
|
||||
|
||||
/**
|
||||
* 통계 섹션 노출 여부
|
||||
* PortalProperty의 main.statistics.display 값으로 설정 (기본값: true)
|
||||
*/
|
||||
@Builder.Default
|
||||
private boolean visible = true;
|
||||
|
||||
/**
|
||||
* API 활용 기업 수
|
||||
* 법인으로 등록되고 정상 상태인 기업의 수
|
||||
*/
|
||||
private int approvedOrgCount;
|
||||
|
||||
/**
|
||||
* 서비스 이용 수
|
||||
* 정상 상태 법인이 생성한 앱 중 이용 가능 상태인 앱의 수
|
||||
*/
|
||||
private int activeAppCount;
|
||||
|
||||
/**
|
||||
* API 이용 건수
|
||||
* 정상 상태 법인이 생성한 이용 가능 앱에 연결된 API의 총 수
|
||||
*/
|
||||
private int totalApiCount;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.eactive.apim.portal.apps.main.service;
|
||||
|
||||
import com.eactive.apim.portal.app.entity.Credential;
|
||||
import com.eactive.apim.portal.app.repository.CredentialRepository;
|
||||
import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO;
|
||||
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 인덱스 페이지 통계 서비스
|
||||
* 메모리 캐싱을 통해 DB 조회를 최소화 (1시간 TTL)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class IndexStatisticsService {
|
||||
|
||||
private static final long CACHE_TTL_HOURS = 1;
|
||||
private static final String APP_STATUS_ACTIVE = "1";
|
||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||
private static final String STATISTICS_DISPLAY_KEY = "main.statistics.display";
|
||||
private static final String DISPLAY_YES = "Y";
|
||||
|
||||
private final PortalOrgRepository portalOrgRepository;
|
||||
private final CredentialRepository credentialRepository;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
// 캐시된 통계 데이터
|
||||
private volatile IndexStatisticsDTO cachedStatistics;
|
||||
// 마지막 집계 시간
|
||||
private volatile LocalDateTime lastAggregatedTime;
|
||||
|
||||
/**
|
||||
* 인덱스 페이지 통계 조회
|
||||
* 캐시가 없거나 1시간이 경과한 경우 재집계
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public IndexStatisticsDTO getStatistics() {
|
||||
if (shouldRefreshCache()) {
|
||||
synchronized (this) {
|
||||
// Double-check locking
|
||||
if (shouldRefreshCache()) {
|
||||
refreshStatistics();
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 갱신이 필요한지 확인
|
||||
*/
|
||||
private boolean shouldRefreshCache() {
|
||||
if (cachedStatistics == null || lastAggregatedTime == null) {
|
||||
return true;
|
||||
}
|
||||
return LocalDateTime.now().isAfter(lastAggregatedTime.plusHours(CACHE_TTL_HOURS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 재집계
|
||||
*/
|
||||
private void refreshStatistics() {
|
||||
log.info("인덱스 페이지 통계 집계 시작");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 0. 노출 여부 확인 (PortalProperty에서 조회, 기본값: Y)
|
||||
boolean visible = isStatisticsVisible();
|
||||
|
||||
// 1. API 활용 기업 수: 정상 상태이고 승인 완료된 기관 수
|
||||
int approvedOrgCount = (int) portalOrgRepository.countByOrgStatusAndApprovalStatus(
|
||||
OrgStatus.ACTIVE, ApprovalStatus.COMPLETED);
|
||||
|
||||
// 2. 정상 상태 기관의 ID 목록 조회
|
||||
List<String> activeOrgIds = portalOrgRepository.findIdsByOrgStatusAndApprovalStatus(
|
||||
OrgStatus.ACTIVE, ApprovalStatus.COMPLETED);
|
||||
|
||||
int activeAppCount = 0;
|
||||
int totalApiCount = 0;
|
||||
|
||||
if (!activeOrgIds.isEmpty()) {
|
||||
// 3. 서비스 이용 수: 정상 기관의 이용 가능 앱 수
|
||||
activeAppCount = (int) credentialRepository.countActiveAppsByOrgIds(activeOrgIds);
|
||||
|
||||
// 4. API 이용 건수: 정상 기관의 이용 가능 앱에 연결된 API 수
|
||||
List<Credential> activeApps = credentialRepository.findActiveAppsByOrgIds(activeOrgIds);
|
||||
totalApiCount = activeApps.stream()
|
||||
.mapToInt(credential -> credential.getApiList() != null ? credential.getApiList().size() : 0)
|
||||
.sum();
|
||||
}
|
||||
|
||||
cachedStatistics = IndexStatisticsDTO.builder()
|
||||
.visible(visible)
|
||||
.approvedOrgCount(approvedOrgCount)
|
||||
.activeAppCount(activeAppCount)
|
||||
.totalApiCount(totalApiCount)
|
||||
.build();
|
||||
|
||||
lastAggregatedTime = LocalDateTime.now();
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("인덱스 페이지 통계 집계 완료 - 소요시간: {}ms, 노출: {}, 기업: {}, 앱: {}, API: {}",
|
||||
elapsed, visible, approvedOrgCount, activeAppCount, totalApiCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("인덱스 페이지 통계 집계 실패", e);
|
||||
// 기존 캐시가 없는 경우 기본값 설정
|
||||
if (cachedStatistics == null) {
|
||||
cachedStatistics = IndexStatisticsDTO.builder()
|
||||
.visible(true)
|
||||
.approvedOrgCount(0)
|
||||
.activeAppCount(0)
|
||||
.totalApiCount(0)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PortalProperty에서 통계 노출 여부 조회
|
||||
* 프로퍼티가 없으면 기본값 "Y"를 DB에 저장 후 반환
|
||||
*/
|
||||
private boolean isStatisticsVisible() {
|
||||
String displayValue = portalPropertyService.getOrCreateProperty(
|
||||
PORTAL_PROPERTY_GROUP,
|
||||
STATISTICS_DISPLAY_KEY,
|
||||
DISPLAY_YES,
|
||||
"인덱스 페이지 하단 통계 섹션 노출 여부 (Y: 노출, N: 숨김)"
|
||||
);
|
||||
return DISPLAY_YES.equalsIgnoreCase(displayValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 강제 갱신 (관리자용)
|
||||
*/
|
||||
public void forceRefresh() {
|
||||
synchronized (this) {
|
||||
lastAggregatedTime = null;
|
||||
refreshStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 만료 시간 조회
|
||||
*/
|
||||
public LocalDateTime getCacheExpireTime() {
|
||||
if (lastAggregatedTime == null) {
|
||||
return null;
|
||||
}
|
||||
return lastAggregatedTime.plusHours(CACHE_TTL_HOURS);
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,13 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalOrgService;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Transactional
|
||||
@Service
|
||||
@@ -34,17 +30,13 @@ public class MainApiFacade {
|
||||
return portalOrgService.getActiveApprovedOrgCount();
|
||||
}
|
||||
|
||||
public List<ApiServiceDTO> getOpenApiServices(String serviceId) {
|
||||
public List<ApiServiceDTO> getOpenApiServices() {
|
||||
ApiGroupSearch search = new ApiGroupSearch();
|
||||
|
||||
Map<String, String> property = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY);
|
||||
String services = property.getOrDefault("main.service.list", "");
|
||||
|
||||
if (StringUtils.hasText(serviceId) && services.contains(serviceId)) {
|
||||
search.setGroupIds(Collections.singletonList(serviceId));
|
||||
} else {
|
||||
search.setGroupIds(Arrays.asList(services.split(",")));
|
||||
}
|
||||
search.setGroupIds(Arrays.asList(services.replace(" ", "").split(",")));
|
||||
|
||||
return apiServiceService.searchApiGroups(search);
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
package com.eactive.apim.portal.apps.proxy;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/_proxy") // /proxy 대신 /_proxy 사용
|
||||
@Slf4j
|
||||
public class ApimProxyController {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ProxyProperties proxyProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MediaType JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
public ApimProxyController(
|
||||
RestTemplateBuilder restTemplateBuilder,
|
||||
ProxyProperties proxyProperties,
|
||||
ObjectMapper objectMapper) {
|
||||
this.proxyProperties = proxyProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.restTemplate = restTemplateBuilder
|
||||
.setConnectTimeout(Duration.ofMillis(proxyProperties.getTimeout().getConnect()))
|
||||
.setReadTimeout(Duration.ofMillis(proxyProperties.getTimeout().getRead()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private ResponseEntity<String> createJsonResponse(HttpStatus status, String message) {
|
||||
ObjectNode jsonResponse = objectMapper.createObjectNode()
|
||||
.put("status", status.value())
|
||||
.put("message", message);
|
||||
return ResponseEntity
|
||||
.status(status)
|
||||
.contentType(JSON_UTF8)
|
||||
.body(jsonResponse.toString());
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/**")
|
||||
public ResponseEntity<String> proxyRequest(
|
||||
HttpServletRequest request,
|
||||
@RequestBody(required = false) String body,
|
||||
@RequestHeader HttpHeaders headers) {
|
||||
|
||||
String targetKey = headers.getFirst("target_host");
|
||||
if (targetKey == null || targetKey.trim().isEmpty()) {
|
||||
log.error("Missing or empty target_host header");
|
||||
return createJsonResponse(HttpStatus.BAD_REQUEST, "Error: target_host header is required");
|
||||
}
|
||||
|
||||
String targetUrl = proxyProperties.getTargets().get(targetKey.trim().toLowerCase());
|
||||
if (targetUrl == null) {
|
||||
log.error("Invalid target_host: {}. Available targets: {}",
|
||||
targetKey, proxyProperties.getTargets().keySet());
|
||||
return createJsonResponse(HttpStatus.BAD_REQUEST,
|
||||
"Error: Invalid target_host. Must be one of: " +
|
||||
String.join(", ", proxyProperties.getTargets().keySet()));
|
||||
}
|
||||
|
||||
try {
|
||||
String requestUrl = request.getRequestURI().replace("/_proxy", "");
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromHttpUrl(targetUrl + requestUrl)
|
||||
.query(request.getQueryString())
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
HttpMethod method = HttpMethod.valueOf(request.getMethod());
|
||||
|
||||
HttpHeaders proxyHeaders = new HttpHeaders();
|
||||
headers.forEach((key, value) -> {
|
||||
if (!key.equalsIgnoreCase("host") &&
|
||||
!key.equalsIgnoreCase("target_host")) {
|
||||
proxyHeaders.addAll(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
HttpEntity<String> httpEntity = new HttpEntity<>(body, proxyHeaders);
|
||||
|
||||
log.debug("Forwarding request to: {} ({})", targetKey, uri);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
uri,
|
||||
method,
|
||||
httpEntity,
|
||||
String.class
|
||||
);
|
||||
log.debug("Received response status: {}", response.getStatusCode());
|
||||
|
||||
// Create JSON response with the original response data
|
||||
ObjectNode jsonResponse = objectMapper.createObjectNode()
|
||||
.put("status", response.getStatusCode().value());
|
||||
|
||||
// Try to parse the response body as JSON, if it fails, include it as a string
|
||||
try {
|
||||
jsonResponse.set("data", objectMapper.readTree(response.getBody()));
|
||||
} catch (Exception e) {
|
||||
jsonResponse.put("data", response.getBody());
|
||||
}
|
||||
|
||||
return ResponseEntity
|
||||
.status(response.getStatusCode())
|
||||
.contentType(JSON_UTF8)
|
||||
.body(jsonResponse.toString());
|
||||
|
||||
} catch (HttpStatusCodeException e) {
|
||||
log.error("Target server returned error: {} for host: {}", e.getStatusCode(), targetKey);
|
||||
ObjectNode jsonResponse = objectMapper.createObjectNode()
|
||||
.put("status", e.getStatusCode().value());
|
||||
|
||||
// Try to parse the error response body as JSON, if it fails, include it as a string
|
||||
try {
|
||||
jsonResponse.set("data", objectMapper.readTree(e.getResponseBodyAsString()));
|
||||
} catch (Exception ex) {
|
||||
jsonResponse.put("data", e.getResponseBodyAsString());
|
||||
}
|
||||
|
||||
return ResponseEntity
|
||||
.status(e.getStatusCode())
|
||||
.contentType(JSON_UTF8)
|
||||
.body(jsonResponse.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during proxy request to: {}", targetKey, e);
|
||||
return createJsonResponse(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.eactive.apim.portal.apps.proxy;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "proxy")
|
||||
@Getter
|
||||
@Setter
|
||||
public class ProxyProperties {
|
||||
private Map<String, String> targets;
|
||||
private Timeout timeout = new Timeout();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Timeout {
|
||||
private int connect = 5000;
|
||||
private int read = 5000;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package com.eactive.apim.portal.apps.statistics.controller;
|
||||
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsResultDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSearchDto;
|
||||
import com.eactive.apim.portal.apps.statistics.service.ApiStatisticsService;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* API 통계 Controller
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/statistics/api")
|
||||
@RequiredArgsConstructor
|
||||
public class ApiStatisticsController {
|
||||
|
||||
private final ApiStatisticsService apiStatisticsService;
|
||||
|
||||
/**
|
||||
* 통계 페이지 진입
|
||||
*/
|
||||
@GetMapping
|
||||
public String statisticsPage(Model model) {
|
||||
PortalOrg org = getPortalOrg();
|
||||
if (org == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
String orgId = org.getId();
|
||||
|
||||
// 앱 목록 조회 (선택용)
|
||||
model.addAttribute("appList", apiStatisticsService.getAppListByOrg(orgId));
|
||||
|
||||
// 기본 조회 (7일 전 ~ 오늘, 전체 앱)
|
||||
ApiStatisticsSearchDto searchDto = new ApiStatisticsSearchDto();
|
||||
searchDto.setStartDate(LocalDate.now().minusDays(7));
|
||||
searchDto.setEndDate(LocalDate.now());
|
||||
searchDto.setClientId(null);
|
||||
|
||||
ApiStatisticsResultDto result = apiStatisticsService.getStatistics(orgId, searchDto);
|
||||
model.addAttribute("summary", result.getSummary());
|
||||
model.addAttribute("details", result.getDetails());
|
||||
model.addAttribute("searchDto", searchDto);
|
||||
|
||||
return "apps/statistics/apiStatistics";
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회 (AJAX)
|
||||
*/
|
||||
@PostMapping("/search")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> searchStatistics(@RequestBody ApiStatisticsSearchDto searchDto) {
|
||||
PortalOrg org = getPortalOrg();
|
||||
if (org == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
// 날짜 범위 검증 (최대 40일)
|
||||
if (!searchDto.isValidDateRange()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(java.util.Collections.singletonMap("error",
|
||||
"조회 기간은 최대 " + ApiStatisticsSearchDto.MAX_DATE_RANGE_DAYS + "일까지 가능합니다."));
|
||||
}
|
||||
|
||||
String orgId = org.getId();
|
||||
ApiStatisticsResultDto result = apiStatisticsService.getStatistics(orgId, searchDto);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV 다운로드
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
public void downloadCsv(ApiStatisticsSearchDto searchDto, HttpServletResponse response) throws IOException {
|
||||
PortalOrg org = getPortalOrg();
|
||||
if (org == null) {
|
||||
response.sendError(401, "Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// 날짜 범위 검증 (최대 40일)
|
||||
if (!searchDto.isValidDateRange()) {
|
||||
response.sendError(400, "조회 기간은 최대 " + ApiStatisticsSearchDto.MAX_DATE_RANGE_DAYS + "일까지 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
String orgId = org.getId();
|
||||
apiStatisticsService.downloadCsv(orgId, searchDto, response);
|
||||
}
|
||||
|
||||
private PortalOrg getPortalOrg() {
|
||||
if (SecurityUtil.getPortalAuthenticatedUser() != null) {
|
||||
return SecurityUtil.getPortalAuthenticatedUser().getPortalOrg();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.eactive.apim.portal.apps.statistics.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 개별 API 통계 상세 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatisticsDetailDto {
|
||||
|
||||
private String apiId;
|
||||
private String apiName;
|
||||
private Long attemptedCount; // 호출 건
|
||||
private Long completedCount; // 성공 건
|
||||
private Long failedCount; // 실패 건
|
||||
private Double successRate; // 성공율 (%)
|
||||
private Double failureRate; // 실패율 (%)
|
||||
|
||||
/**
|
||||
* Repository 조회용 생성자
|
||||
*/
|
||||
public ApiStatisticsDetailDto(String apiId, String apiName, Long attemptedCount, Long completedCount) {
|
||||
this.apiId = apiId;
|
||||
this.apiName = apiName;
|
||||
this.attemptedCount = attemptedCount != null ? attemptedCount : 0L;
|
||||
this.completedCount = completedCount != null ? completedCount : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패건, 성공률, 실패율 계산
|
||||
*/
|
||||
public void calculateRates() {
|
||||
if (attemptedCount == null) {
|
||||
attemptedCount = 0L;
|
||||
}
|
||||
if (completedCount == null) {
|
||||
completedCount = 0L;
|
||||
}
|
||||
|
||||
this.failedCount = attemptedCount - completedCount;
|
||||
|
||||
if (attemptedCount > 0) {
|
||||
this.successRate = Math.round((completedCount * 1000.0) / attemptedCount) / 10.0;
|
||||
this.failureRate = Math.round((failedCount * 1000.0) / attemptedCount) / 10.0;
|
||||
} else {
|
||||
this.successRate = 0.0;
|
||||
this.failureRate = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.eactive.apim.portal.apps.statistics.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API 통계 조회 결과 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatisticsResultDto {
|
||||
|
||||
private ApiStatisticsSummaryDto summary;
|
||||
private List<ApiStatisticsDetailDto> details;
|
||||
|
||||
/**
|
||||
* 빈 결과 생성
|
||||
*/
|
||||
public static ApiStatisticsResultDto empty() {
|
||||
return new ApiStatisticsResultDto(
|
||||
ApiStatisticsSummaryDto.empty(),
|
||||
new ArrayList<>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.eactive.apim.portal.apps.statistics.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* API 통계 검색 조건 DTO
|
||||
*/
|
||||
@Data
|
||||
public class ApiStatisticsSearchDto {
|
||||
|
||||
/**
|
||||
* 최대 조회 가능 일수
|
||||
*/
|
||||
public static final int MAX_DATE_RANGE_DAYS = 40;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate startDate;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate endDate;
|
||||
|
||||
/**
|
||||
* 선택된 앱의 clientId (null 또는 빈값이면 전체 앱)
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
public ApiStatisticsSearchDto() {
|
||||
this.startDate = LocalDate.now().minusDays(7);
|
||||
this.endDate = LocalDate.now();
|
||||
this.clientId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 앱이 선택되었는지 여부
|
||||
*/
|
||||
public boolean hasSelectedApp() {
|
||||
return clientId != null && !clientId.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위가 유효한지 검증 (최대 40일)
|
||||
*/
|
||||
public boolean isValidDateRange() {
|
||||
if (startDate == null || endDate == null) {
|
||||
return false;
|
||||
}
|
||||
if (startDate.isAfter(endDate)) {
|
||||
return false;
|
||||
}
|
||||
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
|
||||
return daysBetween <= MAX_DATE_RANGE_DAYS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회 기간 일수 반환
|
||||
*/
|
||||
public long getDateRangeDays() {
|
||||
if (startDate == null || endDate == null) {
|
||||
return 0;
|
||||
}
|
||||
return ChronoUnit.DAYS.between(startDate, endDate);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.eactive.apim.portal.apps.statistics.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API 전체 통계 요약 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatisticsSummaryDto {
|
||||
|
||||
private Long totalAttempted; // 전체 호출 건
|
||||
private Long totalCompleted; // 전체 성공 건
|
||||
private Long totalFailed; // 전체 실패 건
|
||||
private Double successRate; // 성공율 (%)
|
||||
private Double failureRate; // 실패율 (%)
|
||||
|
||||
/**
|
||||
* Repository 조회용 생성자 (attempted, completed만)
|
||||
*/
|
||||
public ApiStatisticsSummaryDto(Long totalAttempted, Long totalCompleted) {
|
||||
this.totalAttempted = totalAttempted != null ? totalAttempted : 0L;
|
||||
this.totalCompleted = totalCompleted != null ? totalCompleted : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패건, 성공률, 실패율 계산
|
||||
*/
|
||||
public void calculateRates() {
|
||||
if (totalAttempted == null) {
|
||||
totalAttempted = 0L;
|
||||
}
|
||||
if (totalCompleted == null) {
|
||||
totalCompleted = 0L;
|
||||
}
|
||||
|
||||
this.totalFailed = totalAttempted - totalCompleted;
|
||||
|
||||
if (totalAttempted > 0) {
|
||||
this.successRate = Math.round((totalCompleted * 1000.0) / totalAttempted) / 10.0;
|
||||
this.failureRate = Math.round((totalFailed * 1000.0) / totalAttempted) / 10.0;
|
||||
} else {
|
||||
this.successRate = 0.0;
|
||||
this.failureRate = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 통계 생성
|
||||
*/
|
||||
public static ApiStatisticsSummaryDto empty() {
|
||||
ApiStatisticsSummaryDto dto = new ApiStatisticsSummaryDto(0L, 0L);
|
||||
dto.calculateRates();
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.eactive.apim.portal.apps.statistics.repository;
|
||||
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
|
||||
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* API 통계 Repository
|
||||
* ObpGwMetric 테이블에서 통계 데이터를 조회
|
||||
*/
|
||||
@EMSDataSource
|
||||
public interface ApiStatisticsRepository extends BaseRepository<ObpGwMetric, String> {
|
||||
|
||||
/**
|
||||
* 전체 통계 조회 (특정 기간, 특정 조직의 모든 앱)
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
|
||||
"COALESCE(SUM(m.attemptedCount), 0), COALESCE(SUM(m.completedCount), 0)) " +
|
||||
"FROM ObpGwMetric m " +
|
||||
"WHERE m.orgId = :orgId " +
|
||||
"AND m.timeslice BETWEEN :startTime AND :endTime")
|
||||
ApiStatisticsSummaryDto findSummaryByOrgAndPeriod(
|
||||
@Param("orgId") String orgId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 전체 통계 조회 (특정 기간, 특정 앱)
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
|
||||
"COALESCE(SUM(m.attemptedCount), 0), COALESCE(SUM(m.completedCount), 0)) " +
|
||||
"FROM ObpGwMetric m " +
|
||||
"WHERE m.orgId = :orgId " +
|
||||
"AND m.clientId = :clientId " +
|
||||
"AND m.timeslice BETWEEN :startTime AND :endTime")
|
||||
ApiStatisticsSummaryDto findSummaryByOrgAndApp(
|
||||
@Param("orgId") String orgId,
|
||||
@Param("clientId") String clientId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 개별 API 통계 조회 (특정 기간, 특정 조직의 모든 앱)
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
|
||||
"m.apiId, m.apiName, " +
|
||||
"COALESCE(SUM(m.attemptedCount), 0), COALESCE(SUM(m.completedCount), 0)) " +
|
||||
"FROM ObpGwMetric m " +
|
||||
"WHERE m.orgId = :orgId " +
|
||||
"AND m.timeslice BETWEEN :startTime AND :endTime " +
|
||||
"GROUP BY m.apiId, m.apiName " +
|
||||
"ORDER BY SUM(m.attemptedCount) DESC")
|
||||
List<ApiStatisticsDetailDto> findDetailByOrgAndPeriod(
|
||||
@Param("orgId") String orgId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 개별 API 통계 조회 (특정 기간, 특정 앱)
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
|
||||
"m.apiId, m.apiName, " +
|
||||
"COALESCE(SUM(m.attemptedCount), 0), COALESCE(SUM(m.completedCount), 0)) " +
|
||||
"FROM ObpGwMetric m " +
|
||||
"WHERE m.orgId = :orgId " +
|
||||
"AND m.clientId = :clientId " +
|
||||
"AND m.timeslice BETWEEN :startTime AND :endTime " +
|
||||
"GROUP BY m.apiId, m.apiName " +
|
||||
"ORDER BY SUM(m.attemptedCount) DESC")
|
||||
List<ApiStatisticsDetailDto> findDetailByOrgAndApp(
|
||||
@Param("orgId") String orgId,
|
||||
@Param("clientId") String clientId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package com.eactive.apim.portal.apps.statistics.service;
|
||||
|
||||
import com.eactive.apim.portal.app.entity.Credential;
|
||||
import com.eactive.apim.portal.app.repository.CredentialRepository;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsResultDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSearchDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
|
||||
import com.eactive.apim.portal.apps.statistics.repository.ApiStatisticsRepository;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* API 통계 Service
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class ApiStatisticsService {
|
||||
|
||||
private final CredentialRepository credentialRepository;
|
||||
private final ApiStatisticsRepository apiStatisticsRepository;
|
||||
|
||||
/**
|
||||
* 로그인 사용자 법인의 앱 목록 조회 (전체)
|
||||
*/
|
||||
public List<Credential> getAppListByOrg(String orgId) {
|
||||
return credentialRepository.findAllByOrgid(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public ApiStatisticsResultDto getStatistics(String orgId, ApiStatisticsSearchDto searchDto) {
|
||||
LocalDateTime startTime = searchDto.getStartDate().atStartOfDay();
|
||||
LocalDateTime endTime = searchDto.getEndDate().atTime(LocalTime.MAX);
|
||||
|
||||
ApiStatisticsSummaryDto summary;
|
||||
List<ApiStatisticsDetailDto> details;
|
||||
|
||||
if (searchDto.hasSelectedApp()) {
|
||||
// 특정 앱 통계
|
||||
String clientId = searchDto.getClientId();
|
||||
|
||||
// 해당 앱이 조직 소속인지 검증
|
||||
if (!isAppBelongsToOrg(clientId, orgId)) {
|
||||
return ApiStatisticsResultDto.empty();
|
||||
}
|
||||
|
||||
summary = apiStatisticsRepository.findSummaryByOrgAndApp(
|
||||
orgId, clientId, startTime, endTime);
|
||||
details = apiStatisticsRepository.findDetailByOrgAndApp(
|
||||
orgId, clientId, startTime, endTime);
|
||||
} else {
|
||||
// 전체 앱 통계
|
||||
summary = apiStatisticsRepository.findSummaryByOrgAndPeriod(
|
||||
orgId, startTime, endTime);
|
||||
details = apiStatisticsRepository.findDetailByOrgAndPeriod(
|
||||
orgId, startTime, endTime);
|
||||
}
|
||||
|
||||
// null 체크 및 빈 결과 처리
|
||||
if (summary == null) {
|
||||
summary = ApiStatisticsSummaryDto.empty();
|
||||
} else {
|
||||
summary.calculateRates();
|
||||
}
|
||||
|
||||
if (details == null) {
|
||||
details = Collections.emptyList();
|
||||
} else {
|
||||
details.forEach(ApiStatisticsDetailDto::calculateRates);
|
||||
}
|
||||
|
||||
return new ApiStatisticsResultDto(summary, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱이 해당 조직 소속인지 검증
|
||||
*/
|
||||
private boolean isAppBelongsToOrg(String clientId, String orgId) {
|
||||
return credentialRepository.findByClientidAndOrgid(clientId, orgId).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV 다운로드
|
||||
*/
|
||||
public void downloadCsv(String orgId, ApiStatisticsSearchDto searchDto, HttpServletResponse response) throws IOException {
|
||||
ApiStatisticsResultDto result = getStatistics(orgId, searchDto);
|
||||
|
||||
String fileName = String.format("API_Statistics_%s_%s.csv",
|
||||
searchDto.getStartDate().toString(),
|
||||
searchDto.getEndDate().toString());
|
||||
|
||||
response.setContentType("text/csv; charset=UTF-8");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + "\"");
|
||||
|
||||
// BOM for Excel UTF-8 recognition
|
||||
response.getOutputStream().write(0xEF);
|
||||
response.getOutputStream().write(0xBB);
|
||||
response.getOutputStream().write(0xBF);
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8), true)) {
|
||||
// 조회 조건
|
||||
writer.println("조회 기간," + searchDto.getStartDate() + " ~ " + searchDto.getEndDate());
|
||||
|
||||
String appName = "전체 앱";
|
||||
if (searchDto.hasSelectedApp()) {
|
||||
appName = credentialRepository.findById(searchDto.getClientId())
|
||||
.map(Credential::getClientname)
|
||||
.orElse("선택된 앱");
|
||||
}
|
||||
writer.println("앱 구분," + appName);
|
||||
writer.println();
|
||||
|
||||
// 전체 통계
|
||||
ApiStatisticsSummaryDto summary = result.getSummary();
|
||||
writer.println("[전체 통계]");
|
||||
writer.println("총 호출 건," + summary.getTotalAttempted());
|
||||
writer.println("성공 건," + summary.getTotalCompleted() + "," + summary.getSuccessRate() + "%");
|
||||
writer.println("실패 건," + summary.getTotalFailed() + "," + summary.getFailureRate() + "%");
|
||||
writer.println();
|
||||
|
||||
// API별 통계 헤더
|
||||
writer.println("[API별 통계]");
|
||||
writer.println("API명,호출 건,성공 건,성공율,실패 건,실패율");
|
||||
|
||||
// API별 통계 데이터
|
||||
for (ApiStatisticsDetailDto detail : result.getDetails()) {
|
||||
writer.println(String.format("%s,%d,%d,%s%%,%d,%s%%",
|
||||
escapeCsv(detail.getApiName()),
|
||||
detail.getAttemptedCount(),
|
||||
detail.getCompletedCount(),
|
||||
detail.getSuccessRate(),
|
||||
detail.getFailedCount(),
|
||||
detail.getFailureRate()));
|
||||
}
|
||||
|
||||
// 전체 합계 행
|
||||
writer.println(String.format("전체,%d,%d,%s%%,%d,%s%%",
|
||||
summary.getTotalAttempted(),
|
||||
summary.getTotalCompleted(),
|
||||
summary.getSuccessRate(),
|
||||
summary.getTotalFailed(),
|
||||
summary.getFailureRate()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV 값 이스케이프 (쉼표, 따옴표 처리)
|
||||
*/
|
||||
private String escapeCsv(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,15 @@ import com.eactive.apim.portal.apps.user.facade.OrgRegisterFacade;
|
||||
import com.eactive.apim.portal.apps.user.facade.UserFacade;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.invitation.entity.UserInvitation;
|
||||
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
|
||||
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -33,6 +38,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
@@ -47,6 +53,7 @@ public class AccountController {
|
||||
private final OrgRegisterFacade orgRegisterFacade;
|
||||
private final AgreementsFacade agreementsFacade;
|
||||
private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade;
|
||||
private final UserInvitationRepository userInvitationRepository;
|
||||
|
||||
|
||||
@PostMapping("/confirm_password")
|
||||
@@ -122,7 +129,8 @@ public class AccountController {
|
||||
|
||||
try {
|
||||
// 현재 로그인된 사용자의 정보를 가져옴
|
||||
PortalUserDTO user = userFacade.findById(SecurityUtil.getPortalAuthenticatedUser().getId());
|
||||
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
|
||||
PortalUserDTO user = userFacade.findById(currentUser.getId());
|
||||
|
||||
// 개별 필드들을 모델에 추가
|
||||
mav.addObject("loginId", user.getLoginId());
|
||||
@@ -132,6 +140,20 @@ public class AccountController {
|
||||
// 기존 user 객체도 유지 (다른 곳에서 필요할 수 있으므로)
|
||||
mav.addObject("user", user);
|
||||
|
||||
// ROLE_USER인 경우 초대 여부 확인
|
||||
if (currentUser.getRoleCode() == RoleCode.ROLE_USER) {
|
||||
java.util.Optional<UserInvitation> pendingInvitation =
|
||||
userInvitationRepository.findFirstByInvitationEmailAndStatus(
|
||||
user.getEmailAddr(), InvitationStatus.PENDING);
|
||||
|
||||
if (pendingInvitation.isPresent()) {
|
||||
mav.addObject("hasPendingInvitation", true);
|
||||
mav.addObject("invitationToken", pendingInvitation.get().getToken());
|
||||
} else {
|
||||
mav.addObject("hasPendingInvitation", false);
|
||||
}
|
||||
}
|
||||
|
||||
// IP 목록을 분리하여 모델에 추가 (법인 관리자인 경우만)
|
||||
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_CORP_MANAGER
|
||||
&& user.getPortalOrg() != null
|
||||
@@ -143,7 +165,7 @@ public class AccountController {
|
||||
}
|
||||
|
||||
// 사용자의 RoleCode에 따라 다른 페이지로 이동
|
||||
switch (SecurityUtil.getPortalAuthenticatedUser().getRoleCode()) {
|
||||
switch (currentUser.getRoleCode()) {
|
||||
case ROLE_USER:
|
||||
mav.setViewName("apps/mypage/updatePersonalUser");
|
||||
break;
|
||||
@@ -252,18 +274,20 @@ public class AccountController {
|
||||
|
||||
|
||||
@PostMapping("/mypage/org-transfer")
|
||||
public String processOrgTransfer(
|
||||
@ResponseBody
|
||||
public Map<String, Object> processOrgTransfer(
|
||||
@ModelAttribute PortalOrgRegistrationDTO orgDTO,
|
||||
@ModelAttribute UserAgreementDTO agreementDTO,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
HttpServletResponse response) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
var currentUser = SecurityUtil.getPortalAuthenticatedUser();
|
||||
|
||||
if (currentUser.getRoleCode() != PortalUserEnums.RoleCode.ROLE_USER) {
|
||||
redirectAttributes.addFlashAttribute("error", "잘못된 접근입니다.");
|
||||
return "redirect:/mypage";
|
||||
result.put("status", "ERROR");
|
||||
result.put("message", "잘못된 접근입니다.");
|
||||
return result;
|
||||
}
|
||||
|
||||
ValidationResponse validationResponse = orgRegisterFacade.convertToOrgUser(
|
||||
@@ -291,16 +315,20 @@ public class AccountController {
|
||||
}
|
||||
}
|
||||
|
||||
redirectAttributes.addFlashAttribute("success", validationResponse.getMessage());
|
||||
return "apps/mypage/UserConversionCompleted";
|
||||
result.put("status", "SUCCESS");
|
||||
result.put("message", validationResponse.getMessage());
|
||||
result.put("redirectUrl", "/");
|
||||
return result;
|
||||
} else {
|
||||
redirectAttributes.addFlashAttribute("error", validationResponse.getMessage());
|
||||
return "redirect:/mypage/org-transfer";
|
||||
result.put("status", "ERROR");
|
||||
result.put("message", validationResponse.getMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("error", "법인회원 전환 중 오류가 발생했습니다: " + e.getMessage());
|
||||
return "redirect:/mypage/org-transfer";
|
||||
result.put("status", "ERROR");
|
||||
result.put("message", "법인회원 전환 중 오류가 발생했습니다: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-1
@@ -43,8 +43,20 @@ public class UserManRestController {
|
||||
//5. register new user
|
||||
@PostMapping("/invite")
|
||||
public ResponseDTO sendInvitation(@RequestBody PortalUserDTO newUser) {
|
||||
String email = newUser.getEmailAddr();
|
||||
|
||||
userManFacade.sendInvitation(SecurityUtil.getPortalAuthenticatedUser(), newUser.getEmailAddr());
|
||||
// 이메일 형식 검증
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return new ResponseDTO(400, "ERROR", "이메일 주소를 입력해주세요.");
|
||||
}
|
||||
|
||||
// 이메일 정규식 검증
|
||||
String emailPattern = "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$";
|
||||
if (!email.matches(emailPattern)) {
|
||||
return new ResponseDTO(400, "ERROR", "올바른 이메일 형식을 입력해주세요.");
|
||||
}
|
||||
|
||||
userManFacade.sendInvitation(SecurityUtil.getPortalAuthenticatedUser(), email);
|
||||
|
||||
return new ResponseDTO(200, "SUCCESS", "요청 메일이 발송되었습니다.");
|
||||
}
|
||||
@@ -56,4 +68,11 @@ public class UserManRestController {
|
||||
|
||||
return new ResponseDTO(200, "SUCCESS", "변경되었습니다. 확인 버튼을 누르시면 계정을 로그아웃 합니다.");
|
||||
}
|
||||
|
||||
// 6. Resend invitation
|
||||
@PostMapping("/resend-invitation")
|
||||
public ResponseDTO resendInvitation(@RequestBody PortalUserDTO user) {
|
||||
userManFacade.resendInvitation(SecurityUtil.getPortalAuthenticatedUser(), user.getId());
|
||||
return new ResponseDTO(200, "SUCCESS", "초대가 재발송되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
+159
-5
@@ -71,7 +71,8 @@ public class UserRegisterController {
|
||||
String[] parts = null;
|
||||
|
||||
if (invitationToken != null) {
|
||||
String decodedToken = new String(Base64.decode(invitationToken));
|
||||
// 8글자 토큰 사용
|
||||
String decodedToken = decodeInvitationToken(invitationToken);
|
||||
session.setAttribute("invitationToken", decodedToken);
|
||||
Optional<UserInvitation> invitation = userInvitationRepository.findByToken(decodedToken);
|
||||
if (invitation.isPresent() && invitation.get().getStatus() == UserInvitationEnums.InvitationStatus.PENDING) {
|
||||
@@ -112,6 +113,7 @@ public class UserRegisterController {
|
||||
@Valid @ModelAttribute("portalUser") PortalUserRegistrationDTO portalUserRegistrationDTO,
|
||||
BindingResult bindingResult,
|
||||
HttpSession session,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model) {
|
||||
try {
|
||||
if (bindingResult.hasErrors()) {
|
||||
@@ -134,10 +136,10 @@ public class UserRegisterController {
|
||||
setModelForError(session, model, agreement, portalUserRegistrationDTO, response.getMessage());
|
||||
return MAIN_USER_REGISTER;
|
||||
}
|
||||
// 성공 시
|
||||
// 성공 시 PRG 패턴 적용: 결과 페이지로 리다이렉트
|
||||
session.removeAttribute("invitationToken");
|
||||
model.addAttribute("message", "회원가입이 완료되었습니다.");
|
||||
return invitationToken != null ? MAIN_EMAIL_COMPLETED : MAIN_REGISTER_RESULT;
|
||||
redirectAttributes.addFlashAttribute("message", "회원가입이 완료되었습니다.");
|
||||
return invitationToken != null ? "redirect:/signup/complete/corporate" : "redirect:/signup/complete";
|
||||
} catch (Exception e) {
|
||||
setModelForError(session, model, agreement, portalUserRegistrationDTO,
|
||||
"처리 중 오류가 발생했습니다: " + e.getMessage());
|
||||
@@ -145,6 +147,28 @@ public class UserRegisterController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개인회원 가입 완료 페이지
|
||||
*/
|
||||
@GetMapping("/signup/complete")
|
||||
public String showRegistrationComplete(Model model) {
|
||||
if (!model.containsAttribute("message")) {
|
||||
return "redirect:/";
|
||||
}
|
||||
return MAIN_REGISTER_RESULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 법인회원 가입 완료 페이지
|
||||
*/
|
||||
@GetMapping("/signup/complete/corporate")
|
||||
public String showCorporateRegistrationComplete(Model model) {
|
||||
if (!model.containsAttribute("message")) {
|
||||
return "redirect:/";
|
||||
}
|
||||
return MAIN_EMAIL_COMPLETED;
|
||||
}
|
||||
|
||||
@GetMapping("/signup/decision")
|
||||
public String showDecisionPage(@RequestParam(name = "invitation", required = false) String invitationToken,
|
||||
HttpSession session,
|
||||
@@ -153,7 +177,8 @@ public class UserRegisterController {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
String decodedToken = new String(Base64.decode(invitationToken));
|
||||
// 8글자 토큰 사용
|
||||
String decodedToken = decodeInvitationToken(invitationToken);
|
||||
session.setAttribute("decisionToken", decodedToken);
|
||||
|
||||
Optional<UserInvitation> invitation = userInvitationRepository.findByToken(decodedToken);
|
||||
@@ -183,6 +208,7 @@ public class UserRegisterController {
|
||||
// 이메일 대신 사용자 이름을 모델에 추가
|
||||
model.addAttribute("userName", portalUser.get().getUserName());
|
||||
model.addAttribute("orgName", org.get().getOrgName());
|
||||
model.addAttribute("invitationCode", decodedToken);
|
||||
model.addAttribute("isInvited", true);
|
||||
|
||||
return "apps/register/userDecisionAccept";
|
||||
@@ -213,6 +239,7 @@ public class UserRegisterController {
|
||||
|
||||
model.addAttribute("userName", SecurityUtil.getPortalAuthenticatedUser().getUsername());
|
||||
model.addAttribute("orgName", org.get().getOrgName());
|
||||
model.addAttribute("invitationCode", invitation.get().getToken());
|
||||
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
|
||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_ORG"));
|
||||
model.addAttribute("registrationType", "corporate");
|
||||
@@ -228,6 +255,7 @@ public class UserRegisterController {
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(name = "invitationToken") String invitationToken,
|
||||
RedirectAttributes redirectAttributes,
|
||||
HttpSession session,
|
||||
Model model) {
|
||||
|
||||
agreementValidator.validate(agreement, bindingResult);
|
||||
@@ -249,6 +277,12 @@ public class UserRegisterController {
|
||||
} else {
|
||||
ValidationResponse response = userRegisterFacade.processInvitation(action, invitation.get());
|
||||
|
||||
// 초대 처리 완료 후 세션에서 초대 관련 속성 제거
|
||||
session.removeAttribute("pendingInvitation");
|
||||
session.removeAttribute("pendingInvitationToken");
|
||||
session.removeAttribute("pendingInvitationOrgName");
|
||||
session.removeAttribute("decisionToken");
|
||||
|
||||
redirectAttributes.addFlashAttribute("success", response.getMessage());
|
||||
return "redirect:/";
|
||||
}
|
||||
@@ -294,4 +328,124 @@ public class UserRegisterController {
|
||||
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT_IND"));
|
||||
model.addAttribute("msgType", "sms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 초대 토큰 반환
|
||||
* @param invitationToken 원본 토큰 (8글자 토큰)
|
||||
* @return 토큰
|
||||
*/
|
||||
private String decodeInvitationToken(String invitationToken) {
|
||||
return invitationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 초대 코드 입력 페이지 표시
|
||||
*/
|
||||
@GetMapping("/signup/invitation-code")
|
||||
public String showInvitationCodeInput(HttpSession session, Model model) {
|
||||
// 브루트포스 방지: 세션에서 시도 횟수 확인
|
||||
Integer failedAttempts = (Integer) session.getAttribute("invitationCodeAttempts");
|
||||
Long lockoutUntil = (Long) session.getAttribute("invitationCodeLockoutUntil");
|
||||
|
||||
if (lockoutUntil != null && lockoutUntil > System.currentTimeMillis()) {
|
||||
// 잠금 상태: 남은 시간 계산
|
||||
long remainingSeconds = (lockoutUntil - System.currentTimeMillis()) / 1000;
|
||||
long remainingMinutes = remainingSeconds / 60;
|
||||
long seconds = remainingSeconds % 60;
|
||||
String lockoutTime = String.format("%d분 %d초", remainingMinutes, seconds);
|
||||
model.addAttribute("lockoutTime", lockoutTime);
|
||||
model.addAttribute("attemptsRemaining", 0);
|
||||
} else {
|
||||
// 잠금 해제: 시도 횟수 표시
|
||||
if (failedAttempts == null) {
|
||||
failedAttempts = 0;
|
||||
}
|
||||
int attemptsRemaining = 5 - failedAttempts;
|
||||
model.addAttribute("attemptsRemaining", attemptsRemaining);
|
||||
}
|
||||
|
||||
return "apps/register/invitationCodeInput";
|
||||
}
|
||||
|
||||
/**
|
||||
* 초대 코드 입력 처리
|
||||
*/
|
||||
@PostMapping("/signup/invitation-code")
|
||||
public String processInvitationCode(
|
||||
@RequestParam("invitationCode") String invitationCode,
|
||||
HttpSession session,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model) {
|
||||
|
||||
// 브루트포스 방지: 잠금 상태 확인
|
||||
Long lockoutUntil = (Long) session.getAttribute("invitationCodeLockoutUntil");
|
||||
if (lockoutUntil != null && lockoutUntil > System.currentTimeMillis()) {
|
||||
long remainingSeconds = (lockoutUntil - System.currentTimeMillis()) / 1000;
|
||||
long remainingMinutes = remainingSeconds / 60;
|
||||
long seconds = remainingSeconds % 60;
|
||||
String lockoutTime = String.format("%d분 %d초", remainingMinutes, seconds);
|
||||
model.addAttribute("lockoutTime", lockoutTime);
|
||||
model.addAttribute("error", "잠금 해제 시간을 기다려 주세요.");
|
||||
return "apps/register/invitationCodeInput";
|
||||
}
|
||||
|
||||
// 초대 코드 정규화 (대문자 변환 및 공백 제거)
|
||||
String normalizedCode = invitationCode.toUpperCase().trim();
|
||||
|
||||
// 초대 코드 검증
|
||||
Optional<UserInvitation> invitationOpt = userInvitationRepository.findByToken(normalizedCode);
|
||||
|
||||
if (!invitationOpt.isPresent() ||
|
||||
invitationOpt.get().getStatus() != UserInvitationEnums.InvitationStatus.PENDING) {
|
||||
|
||||
// 실패 처리: 시도 횟수 증가
|
||||
Integer failedAttempts = (Integer) session.getAttribute("invitationCodeAttempts");
|
||||
if (failedAttempts == null) {
|
||||
failedAttempts = 0;
|
||||
}
|
||||
failedAttempts++;
|
||||
session.setAttribute("invitationCodeAttempts", failedAttempts);
|
||||
|
||||
if (failedAttempts >= 5) {
|
||||
// 5회 실패: 15분 잠금
|
||||
long lockoutTime = System.currentTimeMillis() + (15 * 60 * 1000); // 15분
|
||||
session.setAttribute("invitationCodeLockoutUntil", lockoutTime);
|
||||
session.setAttribute("invitationCodeAttempts", 0); // 시도 횟수 초기화
|
||||
|
||||
model.addAttribute("lockoutTime", "15분 0초");
|
||||
model.addAttribute("error", "잘못된 초대 코드를 5회 입력하여 15분간 잠금되었습니다.");
|
||||
} else {
|
||||
// 실패 메시지와 남은 시도 횟수 표시
|
||||
int attemptsRemaining = 5 - failedAttempts;
|
||||
model.addAttribute("attemptsRemaining", attemptsRemaining);
|
||||
model.addAttribute("error", "유효하지 않은 초대 코드입니다. 다시 확인해 주세요.");
|
||||
}
|
||||
|
||||
return "apps/register/invitationCodeInput";
|
||||
}
|
||||
|
||||
// 성공: 초대 코드 검증 완료
|
||||
UserInvitation invitation = invitationOpt.get();
|
||||
|
||||
// 만료 확인
|
||||
if (invitation.getExpiresOn().isBefore(java.time.LocalDateTime.now())) {
|
||||
model.addAttribute("error", "만료된 초대 코드입니다. 관리자에게 재발송을 요청해 주세요.");
|
||||
return "apps/register/invitationCodeInput";
|
||||
}
|
||||
|
||||
// 시도 횟수 초기화
|
||||
session.removeAttribute("invitationCodeAttempts");
|
||||
session.removeAttribute("invitationCodeLockoutUntil");
|
||||
|
||||
// 기존 사용자 여부 확인
|
||||
Optional<PortalUser> existingUser = portalUserRepository.findPortalUserByEmailAddr(invitation.getInvitationEmail());
|
||||
|
||||
if (existingUser.isPresent()) {
|
||||
// 기존 사용자: 초대 수락 페이지로 이동
|
||||
return "redirect:/signup/decision?invitation=" + normalizedCode;
|
||||
} else {
|
||||
// 신규 사용자: 회원가입 페이지로 이동
|
||||
return "redirect:/signup/portalUser?invitation=" + normalizedCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,9 @@ public class PortalOrgRegistrationDTO extends PortalUserRegistrationDTO {
|
||||
private String orgName;
|
||||
|
||||
//대표자 성명
|
||||
@NotEmpty(message = "{field.required}")
|
||||
private String ceoName;
|
||||
|
||||
//기관사업장소재지
|
||||
@NotEmpty(message = "{field.required}")
|
||||
private String orgAddr;
|
||||
|
||||
//기관업태
|
||||
@@ -44,15 +42,12 @@ public class PortalOrgRegistrationDTO extends PortalUserRegistrationDTO {
|
||||
private String orgIndustryType;
|
||||
|
||||
//서비스명
|
||||
@NotEmpty(message = "{field.required}")
|
||||
private String serviceName;
|
||||
|
||||
//고객센터연락처
|
||||
@NotEmpty(message = "{field.required}")
|
||||
private String scPhoneNumber;
|
||||
|
||||
//기관전화번호
|
||||
@NotEmpty(message = "{field.required}")
|
||||
@PhoneNumber
|
||||
private String orgPhoneNumber;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ public class ValidationResponse {
|
||||
private boolean isPersonalAccount;
|
||||
private boolean isCorporateAccount;
|
||||
|
||||
private String authNumber; // 테스트 환경 인증번호
|
||||
|
||||
public ValidationResponse(boolean isValid, String message) {
|
||||
this.isValid = isValid;
|
||||
this.message = message;
|
||||
|
||||
@@ -41,9 +41,10 @@ public class AuthFacadeImpl implements AuthFacade {
|
||||
}
|
||||
|
||||
try {
|
||||
authNumberService.sendRequestAuthNumber(recipientKey, msgType);
|
||||
String generatedAuthNumber = authNumberService.sendRequestAuthNumber(recipientKey, msgType);
|
||||
response.setValid(true);
|
||||
response.setMessage("인증번호를 발송하였습니다.");
|
||||
response.setAuthNumber(generatedAuthNumber); // 테스트 환경에서 인증번호 표시용
|
||||
} catch (Exception e) {
|
||||
response.setValid(false);
|
||||
response.setMessage(e.getMessage());
|
||||
|
||||
@@ -76,7 +76,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
|
||||
// 2025.10.20 - 휴대폰 번호 중복 무시
|
||||
// PortalUser existingUser = portalUserRepository.findByUserNameAndMobileNumber(orgDTO.getUserName(), orgDTO.getMobileNumber());
|
||||
Optional<PortalUser> existingUser = portalUserRepository.findByUserName(orgDTO.getUserName());
|
||||
Optional<PortalUser> existingUser = portalUserRepository.findByLoginId(orgDTO.getLoginId());
|
||||
|
||||
if(existingUser.isPresent()) {
|
||||
return new ValidationResponse(false, "가입된 계정이 이미 존재합니다.");
|
||||
@@ -181,6 +181,11 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
PortalOrgRegistrationDTO orgDTO,
|
||||
FileInfo uploadedFile) {
|
||||
|
||||
// 사업자등록번호 중복 체크 (이중 방어)
|
||||
if (portalOrgService.existsByCompRegNo(orgDTO.getCompRegNo())) {
|
||||
return new ValidationResponse(false, "이미 등록된 사업자등록번호입니다.");
|
||||
}
|
||||
|
||||
// PortalOrgService를 통한 기관 등록
|
||||
PortalOrg newOrg = portalOrgService.registerOrgFromDTOWithFile(orgDTO, uploadedFile);
|
||||
|
||||
@@ -190,7 +195,8 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT_ORG);
|
||||
|
||||
approvalService.createUserApproval(newUser);
|
||||
sendActivationEmail(newUser);
|
||||
// 11.28 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
||||
// sendActivationEmail(newUser);
|
||||
|
||||
return new ValidationResponse(true, "법인 사용자 등록 신청이 완료되었습니다.");
|
||||
}
|
||||
@@ -218,6 +224,11 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
PortalOrgRegistrationDTO orgDTO,
|
||||
FileInfo uploadedFile) {
|
||||
|
||||
// 사업자등록번호 중복 체크 (이중 방어)
|
||||
if (portalOrgService.existsByCompRegNo(orgDTO.getCompRegNo())) {
|
||||
return new ValidationResponse(false, "이미 등록된 사업자등록번호입니다.");
|
||||
}
|
||||
|
||||
// PortalOrgService를 통한 기관 등록
|
||||
PortalOrg newOrg = portalOrgService.registerOrgFromDTOWithFile(orgDTO, uploadedFile);
|
||||
|
||||
@@ -243,12 +254,18 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
String newEmail,
|
||||
FileInfo uploadedFile) {
|
||||
|
||||
// 사업자등록번호 중복 체크 (이중 방어)
|
||||
if (portalOrgService.existsByCompRegNo(orgDTO.getCompRegNo())) {
|
||||
return new ValidationResponse(false, "이미 등록된 사업자등록번호입니다.");
|
||||
}
|
||||
|
||||
// PortalOrgService를 통한 기관 등록
|
||||
PortalOrg newOrg = portalOrgService.registerOrgFromDTOWithFile(orgDTO, uploadedFile);
|
||||
|
||||
// 새 사용자 생성
|
||||
existingUser.setLoginId(newEmail);
|
||||
existingUser.setEmailAddr(newEmail);
|
||||
// 새 사용자 생성 (이메일 소문자 변환 적용)
|
||||
String normalizedNewEmail = newEmail != null ? newEmail.toLowerCase() : null;
|
||||
existingUser.setLoginId(normalizedNewEmail);
|
||||
existingUser.setEmailAddr(normalizedNewEmail);
|
||||
existingUser.setPortalOrg(newOrg);
|
||||
existingUser.setRoleCode(PortalUserEnums.RoleCode.ROLE_CORP_MANAGER);
|
||||
existingUser.setUserStatus(PortalUserEnums.UserStatus.READY);
|
||||
@@ -285,9 +302,19 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
|
||||
@Override
|
||||
public ResponseEntity<ValidationResponse> checkBusinessNumber(String compRegNo) {
|
||||
boolean isValid = validationService.isValidBusinessNumber(compRegNo);
|
||||
String message = isValid ? "사용 가능한 사업자 등록번호입니다." : "유효하지 않은 사업자 등록번호입니다.";
|
||||
return ResponseEntity.ok(new ValidationResponse(isValid, message));
|
||||
// 1. 형식 검증
|
||||
boolean isValidFormat = validationService.isValidBusinessNumber(compRegNo);
|
||||
if (!isValidFormat) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "유효하지 않은 사업자 등록번호입니다."));
|
||||
}
|
||||
|
||||
// 2. 중복 검증
|
||||
boolean isDuplicate = portalOrgService.existsByCompRegNo(compRegNo);
|
||||
if (isDuplicate) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "이미 등록된 사업자등록번호입니다."));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new ValidationResponse(true, "사용 가능한 사업자 등록번호입니다."));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apps.user.mapper.PortalOrgMapper;
|
||||
import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.DurationParser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||
import com.eactive.apim.portal.invitation.entity.UserInvitation;
|
||||
@@ -18,23 +19,25 @@ import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import com.eactive.apim.portal.user.entity.UserLog;
|
||||
import com.eactive.apim.portal.user.repository.UserLogRepository;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.xerces.impl.dv.util.Base64;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.sound.midi.Receiver;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -48,6 +51,7 @@ public class UserManFacade {
|
||||
private final UserInvitationRepository userInvitationRepository;
|
||||
private final MessageHandlerService messageHandlerService;
|
||||
private final UserLogRepository userLogRepository;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
public Page<PortalUserDTO> getUsers(PortalOrgDTO userOrg, Pageable pageable) {
|
||||
return portalUserRepository
|
||||
@@ -91,6 +95,19 @@ public class UserManFacade {
|
||||
}
|
||||
|
||||
public void sendInvitation(PortalUser sender, String emailAddr) {
|
||||
// 이메일 형식 검증
|
||||
if (emailAddr == null || emailAddr.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("이메일 주소를 입력해주세요.");
|
||||
}
|
||||
|
||||
// 이메일 정규식 검증
|
||||
String emailPattern = "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$";
|
||||
if (!emailAddr.matches(emailPattern)) {
|
||||
throw new IllegalArgumentException("올바른 이메일 형식을 입력해주세요.");
|
||||
}
|
||||
|
||||
// 중복 초대 확인
|
||||
checkDuplicateInvitation(sender.getPortalOrg().getId(), emailAddr);
|
||||
|
||||
//기존 사용자인지 확인
|
||||
Optional<PortalUser> existingUser = portalUserRepository.findPortalUserByEmailAddr(emailAddr);
|
||||
@@ -104,16 +121,32 @@ public class UserManFacade {
|
||||
|
||||
String token = createInvitation(sender, emailAddr);
|
||||
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUserId(emailAddr);
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("manager", sender.getUserName());
|
||||
params.put("company", sender.getPortalOrg().getOrgName());
|
||||
// {"CRPT_NM":"%corpName%", "MNGR_NM":"%managerName%", "CUST_ID":"%loginId%", "AUTH_NO": "%authNumber%", "NAME": "%loginId%"}
|
||||
MessageRecipient recipient;
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("managerName", sender.getUserName());
|
||||
params.put("corpName", sender.getPortalOrg().getOrgName());
|
||||
params.put("authNumber", token); // 8-character invitation code
|
||||
|
||||
// 기존 사용자인 경우 회원 이름 사용, 비회원인 경우 이메일 사용
|
||||
if (existingUser.isPresent()) {
|
||||
params.put("url", "signup/decision?invitation=" + Base64.encode(token.getBytes(StandardCharsets.UTF_8)));
|
||||
PortalUser user = existingUser.get();
|
||||
params.put("loginId", user.getUserName());
|
||||
params.put("NAME", user.getUserName());
|
||||
params.put("url", "signup/decision?invitation=" + token);
|
||||
|
||||
recipient = MessageRecipient.of(user);
|
||||
} else {
|
||||
params.put("url", "signup/portalUser?invitation=" + Base64.encode(token.getBytes(StandardCharsets.UTF_8)));
|
||||
params.put("loginId", emailAddr);
|
||||
params.put("NAME", emailAddr);
|
||||
params.put("url", "signup/portalUser?invitation=" + token);
|
||||
|
||||
String emailUsername = emailAddr.contains("@") ? emailAddr.substring(0, emailAddr.indexOf("@")) : emailAddr;
|
||||
recipient = MessageRecipient.builder()
|
||||
.userId(emailAddr)
|
||||
.username(emailUsername)
|
||||
.build();
|
||||
}
|
||||
|
||||
messageHandlerService.publishEvent(UserInvitationEvent.KEY, recipient, params);
|
||||
@@ -129,18 +162,38 @@ public class UserManFacade {
|
||||
newInvitation.setInvitationDate(LocalDateTime.now());
|
||||
newInvitation.setStatus(InvitationStatus.PENDING);
|
||||
|
||||
// Generate a unique token (you might want to use a more secure method)
|
||||
String token = UUID.randomUUID().toString();
|
||||
// Generate 8-character token for manual entry in closed network
|
||||
// Excludes confusing characters: O, 0, I, 1
|
||||
String token = generateEightCharToken();
|
||||
newInvitation.setToken(token);
|
||||
|
||||
// Set expiration date (e.g., 7 days from now)
|
||||
newInvitation.setExpiresOn(LocalDateTime.now().plusDays(7));
|
||||
// Set expiration date from PTL_PROPERTY (default: 7 days)
|
||||
Duration expiration = getInvitationExpiration();
|
||||
newInvitation.setExpiresOn(LocalDateTime.now().plus(expiration));
|
||||
|
||||
// Save the invitation
|
||||
userInvitationRepository.save(newInvitation);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 폐쇄망에서 사람이 직접 입력할 수 있는 8글자 토큰 생성
|
||||
* 혼동되기 쉬운 문자(O, 0, I, 1)를 제외한 대문자 영문자와 숫자 조합
|
||||
*/
|
||||
private String generateEightCharToken() {
|
||||
// 혼동되기 쉬운 문자 제외: O, 0, I, 1
|
||||
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder token = new StringBuilder(8);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int index = random.nextInt(chars.length());
|
||||
token.append(chars.charAt(index));
|
||||
}
|
||||
|
||||
return token.toString();
|
||||
}
|
||||
|
||||
public void assignManager(PortalAuthenticatedUser portalAuthenticatedUser, String targetUserId) {
|
||||
PortalUser targetUser = portalUserRepository.findById(targetUserId)
|
||||
.orElseThrow(() -> new NotFoundException("ID [" + targetUserId + "] 사용자를 찾을 수 없습니다. "));
|
||||
@@ -251,4 +304,78 @@ public class UserManFacade {
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 초대 재발송
|
||||
* 기존 PENDING 또는 EXPIRED 상태의 초대를 취소하고 새로운 초대를 생성합니다.
|
||||
*/
|
||||
public void resendInvitation(PortalUser sender, String invitationId) {
|
||||
UserInvitation existingInvitation = userInvitationRepository.findById(invitationId)
|
||||
.orElseThrow(() -> new NotFoundException("초대를 찾을 수 없습니다: " + invitationId));
|
||||
|
||||
// 권한 확인: 같은 기관의 초대인지 확인
|
||||
if (!existingInvitation.getOrgId().equals(sender.getPortalOrg().getId())) {
|
||||
throw new IllegalArgumentException("초대를 재발송할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// 재발송 가능한 상태인지 확인
|
||||
if (existingInvitation.getStatus() != InvitationStatus.PENDING
|
||||
&& existingInvitation.getStatus() != InvitationStatus.EXPIRED) {
|
||||
throw new IllegalStateException("재발송 가능한 상태가 아닙니다. 현재 상태: " + existingInvitation.getStatus().getDescription());
|
||||
}
|
||||
|
||||
// 기존 초대를 취소 처리
|
||||
existingInvitation.setStatus(InvitationStatus.CANCELED);
|
||||
userInvitationRepository.save(existingInvitation);
|
||||
|
||||
// 새로운 초대 생성 및 발송
|
||||
sendInvitation(sender, existingInvitation.getInvitationEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 중복 초대 확인
|
||||
* 같은 기관에서 같은 이메일로 이미 PENDING 상태인 초대가 있는지 확인
|
||||
*/
|
||||
private void checkDuplicateInvitation(String orgId, String emailAddr) {
|
||||
// 1. 같은 기관에서 PENDING 상태인 초대가 있는지 확인
|
||||
Optional<UserInvitation> pendingInvitation =
|
||||
userInvitationRepository.findFirstByInvitationEmailAndOrgIdAndStatus(
|
||||
emailAddr, orgId, InvitationStatus.PENDING);
|
||||
|
||||
if (pendingInvitation.isPresent()) {
|
||||
throw new IllegalArgumentException("이미 초대가 진행 중인 이메일 주소입니다.");
|
||||
}
|
||||
|
||||
// 2. 다른 기관에서 PENDING 상태인 초대가 있는지 확인
|
||||
Optional<UserInvitation> otherOrgPendingInvitation =
|
||||
userInvitationRepository.findFirstByInvitationEmailAndStatus(
|
||||
emailAddr, InvitationStatus.PENDING);
|
||||
|
||||
if (otherOrgPendingInvitation.isPresent()
|
||||
&& !otherOrgPendingInvitation.get().getOrgId().equals(orgId)) {
|
||||
throw new IllegalArgumentException("해당 이메일은 다른 기관에서 초대 진행 중입니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PTL_PROPERTY에서 초대 만료 기간을 가져옵니다.
|
||||
* 설정이 없으면 기본값 7일을 반환합니다.
|
||||
*
|
||||
* @return 초대 만료 기간
|
||||
*/
|
||||
private Duration getInvitationExpiration() {
|
||||
try {
|
||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap("invitation");
|
||||
String expirationStr = properties.get("expiration");
|
||||
|
||||
if (expirationStr != null && !expirationStr.trim().isEmpty()) {
|
||||
return DurationParser.parseOrDefault(expirationStr, Duration.ofDays(7));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 설정을 가져오는 데 실패하면 기본값 사용
|
||||
}
|
||||
|
||||
// 기본값: 7일
|
||||
return Duration.ofDays(7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
||||
return new ValidationResponse(false,"입력값을 확인해 주세요.");
|
||||
}
|
||||
|
||||
// 이메일 중복 체크
|
||||
if (portalUserService.existsByLoginId(registrationDTO.getLoginId())) {
|
||||
return new ValidationResponse(false, "이미 사용 중인 이메일입니다.");
|
||||
}
|
||||
|
||||
// 25.10.01 - 휴대폰 번호 중복이여도 가입 가능
|
||||
// PortalUser existingUser = portalUserRepository.findByUserNameAndMobileNumber(registrationDTO.getUserName(), registrationDTO.getMobileNumber());
|
||||
//
|
||||
|
||||
@@ -5,15 +5,31 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
@EMSDataSource
|
||||
public interface PortalOrgRepository extends BaseRepository<PortalOrg, String> {
|
||||
|
||||
Optional<PortalOrg> findByCompRegNo(String compRegNo);
|
||||
|
||||
/**
|
||||
* 사업자등록번호 존재 여부 확인 (중복 데이터가 있어도 안전하게 처리)
|
||||
*/
|
||||
boolean existsByCompRegNo(String compRegNo);
|
||||
|
||||
long countByOrgStatusAndApprovalStatus(PortalOrgEnums.OrgStatus status, PortalOrgEnums.ApprovalStatus approvalStatus);
|
||||
|
||||
Optional<PortalOrg> findById(String orgId);
|
||||
|
||||
/**
|
||||
* 정상 상태이고 승인 완료된 기관의 ID 목록 조회
|
||||
*/
|
||||
@Query("SELECT o.id FROM PortalOrg o WHERE o.orgStatus = :status AND o.approvalStatus = :approvalStatus")
|
||||
List<String> findIdsByOrgStatusAndApprovalStatus(
|
||||
@Param("status") PortalOrgEnums.OrgStatus status,
|
||||
@Param("approvalStatus") PortalOrgEnums.ApprovalStatus approvalStatus);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,4 +58,9 @@ public class PortalOrgService {
|
||||
org.setOrgStatus(OrgStatus.READY);
|
||||
org.setApprovalStatus(ApprovalStatus.PENDING);
|
||||
}
|
||||
|
||||
// 사업자등록번호 중복 체크 (Spring Data JPA의 existsBy 쿼리 사용 - 중복 데이터에도 안전)
|
||||
public boolean existsByCompRegNo(String compRegNo) {
|
||||
return portalOrgRepository.existsByCompRegNo(compRegNo);
|
||||
}
|
||||
}
|
||||
+33
-20
@@ -18,6 +18,16 @@ import com.eactive.apim.portal.template.entity.MessageRequest;
|
||||
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.xerces.impl.dv.util.Base64;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
@@ -28,16 +38,6 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
@@ -55,7 +55,9 @@ public class PortalUserAuthService implements UserDetailsService {
|
||||
@Transactional(noRollbackFor = UsernameNotFoundException.class)
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
try {
|
||||
PortalUser portalUser = findByEmailAddr(username);
|
||||
// 이메일 소문자 변환 적용 (대소문자 구분 없이 로그인 가능하도록)
|
||||
String normalizedUsername = username != null ? username.toLowerCase() : null;
|
||||
PortalUser portalUser = findByEmailAddr(normalizedUsername);
|
||||
RoleCode userRole = portalUser.getRoleCode() == null ? RoleCode.ROLE_USER : portalUser.getRoleCode();
|
||||
List<String> roles = portalProperties.getPortalSecurity().get(userRole);
|
||||
PortalAuthenticatedUser authenticatedUser = portalUserMapper.portalUserToAuthenticatedUser(portalUser);
|
||||
@@ -71,17 +73,22 @@ public class PortalUserAuthService implements UserDetailsService {
|
||||
}
|
||||
}
|
||||
|
||||
public PortalUserDTO findUsersByNameAndMobile(String userName, String mobileNumber) {
|
||||
public List<PortalUserDTO> findAllUsersByNameAndMobile(String userName, String mobileNumber) {
|
||||
try {
|
||||
PortalUser user = portalUserRepository.findByUserNameAndMobileNumber(userName, mobileNumber);
|
||||
if (user == null) {
|
||||
if (mobileNumber == null || !mobileNumber.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
|
||||
throw new UserNotFoundException("유효하지 않은 휴대폰 번호 형식입니다.");
|
||||
}
|
||||
List<PortalUser> users = portalUserRepository.findAllByUserNameAndMobileNumber(userName, mobileNumber);
|
||||
if (users == null || users.isEmpty()) {
|
||||
throw new UserNotFoundException("입력하신 사용자 정보가 올바르지 않습니다. 다시 확인해 주세요.");
|
||||
}
|
||||
PortalUserDTO dto = portalUserMapper.toDTO(user);
|
||||
dto.setMaskedEmailAddr(StringMaskingUtil.maskEmail(dto.getLoginId()));
|
||||
|
||||
dto.setLoginId(null);
|
||||
return dto;
|
||||
return users.stream().map(user -> {
|
||||
PortalUserDTO dto = portalUserMapper.toDTO(user);
|
||||
dto.setMaskedEmailAddr(StringMaskingUtil.maskEmail(dto.getLoginId()));
|
||||
dto.setLoginId(null);
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (UserNotFoundException e) {
|
||||
throw e;
|
||||
@@ -94,13 +101,19 @@ public class PortalUserAuthService implements UserDetailsService {
|
||||
return portalUserRepository.findPortalUserByEmailAddr(emailAddr).orElseThrow(() -> new UserNotFoundException("USER_NOT_FOUND_MESSAGE"));
|
||||
}
|
||||
|
||||
public void initializePassword(String loginId, String userName, String mobileNumber) {
|
||||
public void resetPassword(String loginId, String userName, String mobileNumber) {
|
||||
if (mobileNumber == null || !mobileNumber.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
|
||||
throw new UserNotFoundException("유효하지 않은 휴대폰 번호 형식입니다.");
|
||||
}
|
||||
|
||||
PortalUser portalUser = portalUserRepository.findByLoginIdAndUserNameAndMobileNumber(loginId, userName, mobileNumber)
|
||||
.orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다."));
|
||||
|
||||
String tempPassword = EncryptionUtil.generateNewPasswordForKjbank(loginId);
|
||||
String tempPassword = EncryptionUtil.generateNewPassword();
|
||||
portalUser.setPasswordHash(passwordEncoder.encode(tempPassword));
|
||||
if ("Y".equalsIgnoreCase(portalUser.getAccountLockYn())) {
|
||||
portalUser.setAccountLockYn("N");
|
||||
}
|
||||
|
||||
portalUserRepository.save(portalUser);
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
|
||||
@@ -46,11 +46,15 @@ public class PortalUserService {
|
||||
}
|
||||
|
||||
public Optional<PortalUser> findByLoginId(String loginId) {
|
||||
return portalUserRepository.findByLoginId(loginId);
|
||||
// 이메일 소문자 변환 적용
|
||||
String normalizedLoginId = loginId != null ? loginId.toLowerCase() : null;
|
||||
return portalUserRepository.findByLoginId(normalizedLoginId);
|
||||
}
|
||||
|
||||
public PortalUser findByEmailAddr(String emailAddr) {
|
||||
return portalUserRepository.findPortalUserByEmailAddr(emailAddr)
|
||||
// 이메일 소문자 변환 적용
|
||||
String normalizedEmail = emailAddr != null ? emailAddr.toLowerCase() : null;
|
||||
return portalUserRepository.findPortalUserByEmailAddr(normalizedEmail)
|
||||
.orElseThrow(() -> new IllegalArgumentException("해당 이메일 주소의 사용자를 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
@@ -59,7 +63,9 @@ public class PortalUserService {
|
||||
}
|
||||
|
||||
public boolean existsByLoginId(String loginId) {
|
||||
return portalUserRepository.existsByLoginId(loginId);
|
||||
// 이메일 소문자 변환 적용
|
||||
String normalizedLoginId = loginId != null ? loginId.toLowerCase() : null;
|
||||
return portalUserRepository.existsByLoginId(normalizedLoginId);
|
||||
}
|
||||
|
||||
public boolean checkOrgHasOtherUsers(PortalOrg portalOrg) {
|
||||
@@ -137,11 +143,13 @@ public class PortalUserService {
|
||||
}
|
||||
|
||||
private void mapDtoToEntity(PortalUser user, PortalUserRegistrationDTO dto) {
|
||||
user.setLoginId(dto.getLoginId());
|
||||
// 이메일 소문자 변환 적용 (대소문자 구분 없이 로그인 가능하도록)
|
||||
String normalizedEmail = dto.getLoginId() != null ? dto.getLoginId().toLowerCase() : null;
|
||||
user.setLoginId(normalizedEmail);
|
||||
user.setUserName(dto.getUserName());
|
||||
user.setPasswordHash(passwordEncoder.encode(dto.getPassword()));
|
||||
user.setMobileNumber(dto.getMobileNumber());
|
||||
user.setEmailAddr(dto.getLoginId());
|
||||
user.setEmailAddr(normalizedEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +184,9 @@ public class PortalUserService {
|
||||
}
|
||||
|
||||
public boolean checkPassword(String loginId, String inputPassword) {
|
||||
PortalUser user = portalUserRepository.findByLoginId(loginId)
|
||||
// 이메일 소문자 변환 적용
|
||||
String normalizedLoginId = loginId != null ? loginId.toLowerCase() : null;
|
||||
PortalUser user = portalUserRepository.findByLoginId(normalizedLoginId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다."));
|
||||
return passwordEncoder.matches(inputPassword, user.getPasswordHash());
|
||||
}
|
||||
|
||||
+17
-8
@@ -24,17 +24,26 @@ public class UserRegistrationValidationService {
|
||||
}
|
||||
|
||||
private boolean isValidOrgInfo(PortalOrgRegistrationDTO orgDTO) {
|
||||
return basicValidationService.isValidBusinessNumber(orgDTO.getCompRegNo()) &&
|
||||
basicValidationService.isValidCorpRegNo(orgDTO.getCorpRegNo()) &&
|
||||
basicValidationService.isValidPhoneNumber(orgDTO.getOrgPhoneNumber()) &&
|
||||
basicValidationService.isValidPhoneNumber(orgDTO.getScPhoneNumber());
|
||||
// 사업자등록번호, 법인등록번호는 필수
|
||||
if (!basicValidationService.isValidBusinessNumber(orgDTO.getCompRegNo()) ||
|
||||
!basicValidationService.isValidCorpRegNo(orgDTO.getCorpRegNo())) {
|
||||
return false;
|
||||
}
|
||||
// 회사 전화번호, 고객센터 전화번호는 선택 (입력된 경우에만 형식 검증)
|
||||
if (basicValidationService.isNotEmpty(orgDTO.getOrgPhoneNumber()) &&
|
||||
!basicValidationService.isValidPhoneNumber(orgDTO.getOrgPhoneNumber())) {
|
||||
return false;
|
||||
}
|
||||
if (basicValidationService.isNotEmpty(orgDTO.getScPhoneNumber()) &&
|
||||
!basicValidationService.isValidPhoneNumber(orgDTO.getScPhoneNumber())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isValidRequiredFields(PortalOrgRegistrationDTO orgDTO) {
|
||||
return basicValidationService.isNotEmpty(orgDTO.getOrgName()) &&
|
||||
basicValidationService.isNotEmpty(orgDTO.getCeoName()) &&
|
||||
basicValidationService.isNotEmpty(orgDTO.getOrgAddr()) &&
|
||||
basicValidationService.isNotEmpty(orgDTO.getServiceName());
|
||||
// 법인명만 필수, 나머지(대표자 성명, 소재지, 서비스명)는 선택
|
||||
return basicValidationService.isNotEmpty(orgDTO.getOrgName());
|
||||
}
|
||||
|
||||
private boolean isValidIpWhitelistIfExists(PortalOrgRegistrationDTO orgDTO) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.config.PortalProperties;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalControllerAdvice {
|
||||
@@ -13,6 +15,12 @@ public class GlobalControllerAdvice {
|
||||
@Autowired
|
||||
private PageService pageService;
|
||||
|
||||
@Autowired
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
@Autowired
|
||||
private PortalProperties portalProperties;
|
||||
|
||||
@ModelAttribute("breadcrumb")
|
||||
public List<Map> addBreadcrumbToModel(HttpServletRequest request) {
|
||||
String currentPath = request.getRequestURI();
|
||||
@@ -24,4 +32,30 @@ public class GlobalControllerAdvice {
|
||||
String currentPath = request.getRequestURI();
|
||||
return pageService.getPageName(currentPath);
|
||||
}
|
||||
|
||||
@ModelAttribute("designSurveyEnabled")
|
||||
public boolean isDesignSurveyEnabled() {
|
||||
String value = portalPropertyService.getOrCreateProperty(
|
||||
"Portal",
|
||||
"ui.design-survey",
|
||||
"false",
|
||||
"네비게이션 바 디자인 설문 활성화 여부 (true/false)"
|
||||
);
|
||||
return "true".equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
@ModelAttribute("showTestAuthNotice")
|
||||
public boolean showTestAuthNotice() {
|
||||
return portalProperties.isTestAuthNoticeEnabled();
|
||||
}
|
||||
|
||||
@ModelAttribute("testAuthNumber")
|
||||
public String getTestAuthNumber() {
|
||||
if (portalProperties.isTestAuthNoticeEnabled()) {
|
||||
String authVirtualCode = portalProperties.getAuthVirtualCode();
|
||||
// 고정 인증번호가 있으면 반환, 없으면 "random" 표시
|
||||
return (authVirtualCode != null && !authVirtualCode.isEmpty()) ? authVirtualCode : "random";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -7,8 +7,11 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.eactive.apim.portal.config.PortalProperties;
|
||||
import com.eactive.apim.portal.file.exception.InvalidFileException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -28,9 +31,11 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
*/
|
||||
@Order(2)
|
||||
@ControllerAdvice(annotations = Controller.class)
|
||||
@RequiredArgsConstructor
|
||||
public class PortalGlobalExceptionHandler {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||
private final PortalProperties portalProperties;
|
||||
|
||||
@ExceptionHandler(value = NotFoundException.class)
|
||||
public ModelAndView handleINotFoundException(HttpServletRequest request, NotFoundException ex) {
|
||||
@@ -62,7 +67,7 @@ public class PortalGlobalExceptionHandler {
|
||||
log.error(ex.getMessage());
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.addObject("errorMessage", ex.getMessage());
|
||||
modelAndView.setViewName("common/error/error");
|
||||
modelAndView.setViewName("error");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@@ -79,7 +84,7 @@ public class PortalGlobalExceptionHandler {
|
||||
log.error(ex.getMessage());
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.addObject("errorMessage", ex.getMessage());
|
||||
modelAndView.setViewName("common/error/error");
|
||||
modelAndView.setViewName("error");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@@ -100,7 +105,7 @@ public class PortalGlobalExceptionHandler {
|
||||
log.error("Exception occurred: ", ex);
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.addObject("errorMessage", ex.getMessage());
|
||||
modelAndView.setViewName("common/error/error");
|
||||
modelAndView.setViewName("error");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@@ -111,4 +116,15 @@ public class PortalGlobalExceptionHandler {
|
||||
modelAndView.setViewName("redirect:" + request.getRequestURI());
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = MaxUploadSizeExceededException.class)
|
||||
public ModelAndView handleMaxUploadSizeExceededException(HttpServletRequest request, RedirectAttributes redirectAttributes, MaxUploadSizeExceededException ex) {
|
||||
log.warn("파일 업로드 용량 초과: {}", ex.getMessage());
|
||||
String maxSize = portalProperties.getFile().getMaxSize();
|
||||
String errorMessage = "파일 크기가 허용된 최대 용량(" + maxSize + ")을 초과했습니다.";
|
||||
redirectAttributes.addFlashAttribute("error", errorMessage);
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.setViewName("redirect:" + request.getRequestURI());
|
||||
return modelAndView;
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -2,20 +2,28 @@ package com.eactive.apim.portal.common.exception;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.common.dto.ResponseDTO;
|
||||
import com.eactive.apim.portal.config.PortalProperties;
|
||||
import com.eactive.apim.portal.portaluser.service.AuthNumberException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@Slf4j
|
||||
@Order(1)
|
||||
@RestControllerAdvice(annotations = RestController.class)
|
||||
@RequiredArgsConstructor
|
||||
public class PortalRestExceptionHandler {
|
||||
|
||||
private final PortalProperties portalProperties;
|
||||
|
||||
@ExceptionHandler(value = IntegrationException.class)
|
||||
public ResponseEntity<ResponseDTO> handleHttpRequestMethodNotSupportedException(HttpServletRequest request, IntegrationException ex) {
|
||||
|
||||
@@ -51,4 +59,13 @@ public class PortalRestExceptionHandler {
|
||||
ResponseDTO response = new ResponseDTO(500, "500", ex.getMessage());
|
||||
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<ResponseDTO> handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException ex) {
|
||||
log.warn("파일 업로드 용량 초과 (REST): {}", ex.getMessage());
|
||||
String maxSize = portalProperties.getFile().getMaxSize();
|
||||
String errorMessage = "파일 크기가 허용된 최대 용량(" + maxSize + ")을 초과했습니다.";
|
||||
ResponseDTO response = new ResponseDTO(413, "413", errorMessage);
|
||||
return new ResponseEntity<>(response, HttpStatus.PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 시간 표현 문자열을 Duration으로 파싱하는 유틸리티
|
||||
* 지원 형식: 5m (5분), 2h (2시간), 7d (7일)
|
||||
*/
|
||||
public class DurationParser {
|
||||
|
||||
private static final Pattern DURATION_PATTERN = Pattern.compile("^(\\d+)([mhd])$");
|
||||
|
||||
/**
|
||||
* 시간 표현 문자열을 Duration으로 변환
|
||||
*
|
||||
* @param durationStr 시간 문자열 (예: "5m", "2h", "7d")
|
||||
* @return Duration 객체
|
||||
* @throws IllegalArgumentException 형식이 올바르지 않을 경우
|
||||
*/
|
||||
public static Duration parse(String durationStr) {
|
||||
if (durationStr == null || durationStr.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Duration string cannot be null or empty");
|
||||
}
|
||||
|
||||
String normalized = durationStr.trim().toLowerCase();
|
||||
Matcher matcher = DURATION_PATTERN.matcher(normalized);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid duration format: " + durationStr +
|
||||
". Expected format: <number><unit> (e.g., 5m, 2h, 7d)"
|
||||
);
|
||||
}
|
||||
|
||||
long value = Long.parseLong(matcher.group(1));
|
||||
String unit = matcher.group(2);
|
||||
|
||||
switch (unit) {
|
||||
case "m":
|
||||
return Duration.ofMinutes(value);
|
||||
case "h":
|
||||
return Duration.ofHours(value);
|
||||
case "d":
|
||||
return Duration.ofDays(value);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported time unit: " + unit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 표현 문자열을 일(day) 단위로 변환
|
||||
*
|
||||
* @param durationStr 시간 문자열 (예: "5m", "2h", "7d")
|
||||
* @return 일(day) 단위 long 값
|
||||
*/
|
||||
public static long parseToDays(String durationStr) {
|
||||
return parse(durationStr).toDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본값을 제공하는 안전한 파싱
|
||||
*
|
||||
* @param durationStr 시간 문자열
|
||||
* @param defaultValue 파싱 실패 시 기본값
|
||||
* @return Duration 객체
|
||||
*/
|
||||
public static Duration parseOrDefault(String durationStr, Duration defaultValue) {
|
||||
try {
|
||||
return parse(durationStr);
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* HTTP 요청 관련 유틸리티 클래스
|
||||
*/
|
||||
public class HttpRequestUtil {
|
||||
|
||||
private static final String[] IP_HEADER_CANDIDATES = {
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_X_FORWARDED_FOR",
|
||||
"HTTP_X_FORWARDED",
|
||||
"HTTP_X_CLUSTER_CLIENT_IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_FORWARDED_FOR",
|
||||
"HTTP_FORWARDED",
|
||||
"HTTP_VIA",
|
||||
"REMOTE_ADDR",
|
||||
"X-Real-IP"
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy를 통한 접속도 고려하여 실제 클라이언트 IP 주소를 반환
|
||||
*
|
||||
* @param request HttpServletRequest 객체
|
||||
* @return 클라이언트 IP 주소
|
||||
*/
|
||||
public static String getClientIpAddress(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
String ip = null;
|
||||
|
||||
// 헤더들을 순회하면서 IP 주소 찾기
|
||||
for (String header : IP_HEADER_CANDIDATES) {
|
||||
ip = request.getHeader(header);
|
||||
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 헤더에서 IP를 찾지 못한 경우 기본 메서드 사용
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// X-Forwarded-For 헤더는 여러 IP를 포함할 수 있음 (쉼표로 구분)
|
||||
// 첫 번째 IP가 실제 클라이언트 IP
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy를 통한 접속도 고려하여 실제 클라이언트 호스트명을 반환
|
||||
*
|
||||
* @param request HttpServletRequest 객체
|
||||
* @return 클라이언트 호스트명
|
||||
*/
|
||||
public static String getClientHost(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
String host = request.getHeader("X-Forwarded-Host");
|
||||
if (host == null || host.isEmpty() || "unknown".equalsIgnoreCase(host)) {
|
||||
host = request.getRemoteHost();
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청이 Proxy를 통해 들어왔는지 확인
|
||||
*
|
||||
* @param request HttpServletRequest 객체
|
||||
* @return Proxy를 통한 접속이면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isProxied(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String header : IP_HEADER_CANDIDATES) {
|
||||
String value = request.getHeader(header);
|
||||
if (value != null && !value.isEmpty() && !"unknown".equalsIgnoreCase(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,11 @@ public final class SecurityUtil {
|
||||
|
||||
public static PortalOrgDTO getUserOrg() {
|
||||
PortalOrgDTO portalOrgDTO = new PortalOrgDTO();
|
||||
if (Objects.requireNonNull(getPortalAuthenticatedUser()).getPortalOrg() != null) {
|
||||
portalOrgDTO.setOrgName(getPortalAuthenticatedUser().getPortalOrg().getOrgName());
|
||||
portalOrgDTO.setId(getPortalAuthenticatedUser().getPortalOrg().getId());
|
||||
PortalAuthenticatedUser portalAuthenticatedUser = getPortalAuthenticatedUser();
|
||||
if (Objects.requireNonNull(portalAuthenticatedUser).getPortalOrg() != null) {
|
||||
portalOrgDTO.setOrgName(portalAuthenticatedUser.getPortalOrg().getOrgName());
|
||||
portalOrgDTO.setId(portalAuthenticatedUser.getPortalOrg().getId());
|
||||
portalOrgDTO.setOrgCode(portalAuthenticatedUser.getPortalOrg().getOrgCode());
|
||||
}
|
||||
|
||||
return portalOrgDTO;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
/**
|
||||
* JDK 8 호환성을 위한 문자열 반복 유틸리티
|
||||
*/
|
||||
public class StringRepeatUtil {
|
||||
|
||||
/**
|
||||
* 문자를 지정된 횟수만큼 반복하여 문자열을 생성
|
||||
*
|
||||
* @param ch 반복할 문자
|
||||
* @param count 반복 횟수
|
||||
* @return 반복된 문자열
|
||||
*/
|
||||
public static String repeat(char ch, int count) {
|
||||
if (count <= 0) {
|
||||
return "";
|
||||
}
|
||||
char[] chars = new char[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
chars[i] = ch;
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 지정된 횟수만큼 반복
|
||||
*
|
||||
* @param str 반복할 문자열
|
||||
* @param count 반복 횟수
|
||||
* @return 반복된 문자열
|
||||
*/
|
||||
public static String repeat(String str, int count) {
|
||||
if (str == null || count <= 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(str.length() * count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 사용자 유형 판별 유틸리티
|
||||
* 내부 운영자와 외부 사용자를 구분하는 공통 로직 제공
|
||||
*/
|
||||
public final class UserTypeUtil {
|
||||
|
||||
private UserTypeUtil() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
private static final String INTERNAL_EMAIL_DOMAIN = "@kjbank.com";
|
||||
|
||||
/**
|
||||
* 현재 사용자가 내부 운영자인지 확인
|
||||
* @param user 인증된 사용자 정보
|
||||
* @return 내부 운영자 여부
|
||||
*/
|
||||
public static boolean isInternalUser(PortalAuthenticatedUser user) {
|
||||
if (user == null || StringUtils.isEmpty(user.getEmailAddr())) {
|
||||
return false;
|
||||
}
|
||||
return user.getEmailAddr().toLowerCase().endsWith(INTERNAL_EMAIL_DOMAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 로그인한 사용자가 내부 운영자인지 확인
|
||||
* @return 내부 운영자 여부
|
||||
*/
|
||||
public static boolean isCurrentUserInternal() {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
return isInternalUser(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.eactive.apim.portal.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
|
||||
|
||||
/**
|
||||
* WAS(WebLogic, JEUS 등) 독립적인 Multipart 파일 업로드 설정.
|
||||
* <p>
|
||||
* Spring Boot 내장 Tomcat의 spring.servlet.multipart 설정은 외부 WAS에서 동작하지 않으므로,
|
||||
* CommonsMultipartResolver를 사용하여 모든 환경에서 일관된 파일 업로드 처리를 보장합니다.
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MultipartConfig {
|
||||
|
||||
private final PortalProperties portalProperties;
|
||||
|
||||
/**
|
||||
* Commons FileUpload 기반 MultipartResolver 설정.
|
||||
* <p>
|
||||
* Bean 이름을 "filterMultipartResolver"로 지정하여
|
||||
* PortalConfigWebDispatcherServlet의 MultipartFilter와 연동합니다.
|
||||
* </p>
|
||||
*
|
||||
* @return CommonsMultipartResolver 인스턴스
|
||||
*/
|
||||
@Bean(name = "filterMultipartResolver")
|
||||
public CommonsMultipartResolver filterMultipartResolver() {
|
||||
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
|
||||
|
||||
long maxSizeBytes = portalProperties.getFile().getMaxSizeBytes();
|
||||
resolver.setMaxUploadSize(maxSizeBytes);
|
||||
resolver.setMaxUploadSizePerFile(maxSizeBytes);
|
||||
resolver.setMaxInMemorySize((int) Math.min(maxSizeBytes, Integer.MAX_VALUE));
|
||||
resolver.setDefaultEncoding("UTF-8");
|
||||
|
||||
log.info("CommonsMultipartResolver configured with maxUploadSize: {} bytes ({})",
|
||||
maxSizeBytes, portalProperties.getFile().getMaxSize());
|
||||
|
||||
return resolver;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user