Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b6ee10439 | |||
| 1a95f015dc | |||
| 92589530d1 | |||
| be40973aa3 | |||
| 1e21b85de1 | |||
| 7b8e0575c5 | |||
| 7c0cd371f0 | |||
| eb2cce8002 | |||
| 634c97c7ca | |||
| e32acfa215 | |||
| 8356fc3029 | |||
| d6186de009 | |||
| 2a9980ab16 | |||
| 919b4a0b06 | |||
| b0ecb26a9d | |||
| 0ad5bffa4f | |||
| 766341540d | |||
| 63d4d7268c | |||
| c42a278c8e | |||
| 83352e3669 | |||
| 317b8d781d | |||
| 206e72edde | |||
| 34dce308dc | |||
| b3fc5c06bf | |||
| eb74a99a5a | |||
| 38e5a7f13c | |||
| 3b7c2e5a8f | |||
| 47099ec485 | |||
| 6dbf6af3de | |||
| 5585df9133 | |||
| 2c63fa1557 | |||
| d71af34950 | |||
| fbc145d145 | |||
| f36b1a6478 | |||
| 8f03209f8a | |||
| 8743592fde | |||
| a0685c8689 | |||
| c0ae60f738 | |||
| b45a7a162a | |||
| 07fbe1ba54 | |||
| 4916bdf4af | |||
| 35621b5174 | |||
| b97b0fcf7b | |||
| 6cb98d36bb | |||
| fd229fca43 | |||
| 5e8d08f1af | |||
| e7945233fe | |||
| 815e2bdd04 | |||
| 0629e842a3 | |||
| 6fa2167378 | |||
| 84e873a08b | |||
| 6c231c4189 | |||
| e3b5a44270 | |||
| 829630ae5c | |||
| 2a9694cd85 | |||
| 456b635a32 | |||
| faaf9e8cd6 | |||
| 4749613947 | |||
| 5125dab897 | |||
| 02eb81af67 | |||
| c02a5ff047 | |||
| 4181d114c1 | |||
| daff07c006 | |||
| 1f51c9b21e | |||
| 929980c089 | |||
| d4d5d66727 | |||
| d3e6249a41 | |||
| 68f8c07fc0 | |||
| 87369ce7f7 | |||
| a2f6151a08 | |||
| b29882c0b2 | |||
| 80740a9543 | |||
| cdd68f48f0 | |||
| d2bc61dc31 | |||
| 73deb7f02e | |||
| fedd3e4c50 | |||
| 6c13b6bc63 | |||
| b54fedc280 | |||
| a4316545dc | |||
| b5ffa69eba | |||
| 2106142d93 | |||
| 0f58f043a8 | |||
| 61be6bf108 | |||
| 96d6dd3df1 | |||
| f9b53810d9 | |||
| 41be827b81 | |||
| 78898ab5a8 | |||
| 19c17f4e91 | |||
| c2d84bb024 | |||
| 6434d48b05 | |||
| d5f0f3aab4 | |||
| dfc859c9cf | |||
| 24c669b0be | |||
| 3eedc522e2 | |||
| a361cc3dec | |||
| 1ac9fa5407 | |||
| 9981459691 | |||
| a2878f9cee | |||
| 1f10dda993 | |||
| a6807a37c2 | |||
| 6c10ae12c7 | |||
| 01c4a80182 | |||
| a5611cf775 | |||
| c733e6b200 | |||
| a2813f391c | |||
| f221c20ece | |||
| 4a3b5b43f3 | |||
| 8de0d94063 | |||
| 57d4efb4eb | |||
| c865359819 | |||
| 41391c8df0 | |||
| 9f4874fe44 | |||
| 43d915342e | |||
| ba00b80bfe | |||
| 87763ba325 | |||
| 5b7a4a108d | |||
| 0f581cae23 | |||
| 923bf56e21 | |||
| 9b9bcb5be4 | |||
| bdb249b74b | |||
| 1cb9606602 | |||
| 93f7c210bc | |||
| bb79b55f08 | |||
| 227d4d8abd | |||
| 23bd0c1207 | |||
| 28a9ff1a7f | |||
| 4395757d98 | |||
| 778d579ada | |||
| 3e403788e5 | |||
| 6c5ba6765d | |||
| a97378b84d | |||
| 9c01db9d00 | |||
| 08bd705dec | |||
| 99f765507b | |||
| 7d6aabe122 | |||
| f08da86eb3 | |||
| 045f354e5c | |||
| 4f577c8c14 | |||
| 2a2505d5f7 | |||
| cad574324e | |||
| 4b4ef4196e | |||
| 5402a5354a | |||
| c5b23c3873 | |||
| cc4c1a6b53 | |||
| 83d267faed | |||
| b02e598d62 | |||
| 551a2da717 | |||
| 1c57d348e6 | |||
| 4f9fbefbcc | |||
| c403118289 | |||
| a91b56ed11 | |||
| 8d232734b1 | |||
| 06c18e437b | |||
| 97b9d2161a | |||
| 0c3b63a5b1 | |||
| 582a3e0f69 | |||
| c1ae12e1ca | |||
| 77aea79363 | |||
| aa5e769148 | |||
| ee8e9e3529 | |||
| 82948a13ce | |||
| f79e528fec | |||
| 6f776ed72a | |||
| e242d3ed06 | |||
| d7abe2c2e5 | |||
| 84d92fced9 | |||
| 53468c3b83 | |||
| 198f6fc82a | |||
| 21c500610c | |||
| 687d4798d4 | |||
| 5adfd42751 | |||
| 6694715195 | |||
| fc96c13df2 | |||
| 7265ad6589 | |||
| 02f9d41334 | |||
| fe4b176512 | |||
| 17c27e4398 | |||
| 10ff5d6f2f | |||
| f671f491b7 | |||
| 3d68589ae7 | |||
| 7268aef211 | |||
| 85b2f01ce0 | |||
| 0f255d7337 | |||
| a8323e78f5 | |||
| fa8fce4f2d | |||
| 67ec6d2103 | |||
| b024f8e86f | |||
| fd16300553 | |||
| 4e7044b9fe | |||
| bddd7e3f85 | |||
| 2c29c8a466 | |||
| d2052d0e16 | |||
| 9c4f3cfb04 | |||
| 71f4cb6f79 | |||
| 171feee9ea | |||
| 3f664bcd5c | |||
| c77c0a2f91 | |||
| 815c329e3c | |||
| 7fdd70dbc7 | |||
| 3dd9edc849 | |||
| 6cff96f40a | |||
| a97609d533 | |||
| 4482f6f807 | |||
| 34f5a89f78 | |||
| 9238584cb9 | |||
| 23235bb854 | |||
| 2d67647c8f | |||
| 4824149ba9 | |||
| 4927c301f3 | |||
| d9830fb47d | |||
| 6a1fade68d | |||
| 357d609552 | |||
| cce9753e36 | |||
| 06fdcb8bb6 | |||
| efb88caab5 | |||
| fb28a31da1 | |||
| b38ca6fe0b | |||
| 86336d52f0 | |||
| 53ca4a2a26 | |||
| 36a59b2b64 | |||
| cdf57c130c | |||
| 7e123c088f | |||
| daafbb596a | |||
| 0ffee7a101 | |||
| b47f76a783 | |||
| f591c5c81d | |||
| a0ea62f8cc | |||
| 157c66ca9d | |||
| ba5b2e61a5 | |||
| 5deda2a004 |
@@ -0,0 +1,180 @@
|
||||
name: eapim-portal CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- stage
|
||||
- feats/ci-test
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, linux, eapim-portal]
|
||||
env:
|
||||
JAVA_HOME: /apps/opts/jdk8
|
||||
JAVA_HOME_TOMCAT: /apps/opts/jdk17
|
||||
CATALINA_BASE: /prod/eapim/devportal
|
||||
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
|
||||
CATALINA_PID: /prod/eapim/devportal/temp/catalina.pid
|
||||
DEPLOY_HTTP_PORT: "39130"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: eapim-portal
|
||||
|
||||
steps:
|
||||
- name: Checkout eapim-portal (this branch)
|
||||
# ref 미지정 — 트리거된 브랜치/commit(github.sha) 그대로 빌드.
|
||||
uses: http://172.30.1.50:3000/actions/checkout@v4
|
||||
with:
|
||||
path: eapim-portal
|
||||
|
||||
- name: Checkout elink-portal-common (master, dependency)
|
||||
# 의존 모듈은 항상 master 고정.
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
|
||||
- name: Checkout elink-online-core-jpa (master, dependency)
|
||||
# 의존 모듈은 항상 master 고정.
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
mkdir -p eapim-online
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
|
||||
- name: Verify toolchain
|
||||
run: |
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
- name: Gradle build (no tests)
|
||||
run: gradle clean build -x test --no-daemon
|
||||
|
||||
- name: Upload WAR artifacts
|
||||
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
|
||||
with:
|
||||
name: eapim-portal-wars-${{ github.sha }}
|
||||
path: |
|
||||
eapim-portal/build/libs/eapim-portal.war
|
||||
eapim-portal/build/libs/eapim-portal-boot.war
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish WAR to Gitea Generic Package
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_URL: http://172.30.1.50:3000
|
||||
PKG_OWNER: djb-eapim
|
||||
PKG_NAME: eapim-portal
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
VERSION="$(TZ=Asia/Seoul date +%Y-%m-%d-%H%M)-${SHORT_SHA}"
|
||||
BASE="$GITEA_URL/api/packages/$PKG_OWNER/generic/$PKG_NAME/$VERSION"
|
||||
|
||||
echo "package version: $VERSION"
|
||||
echo "endpoint base : $BASE"
|
||||
|
||||
for WAR in eapim-portal.war eapim-portal-boot.war; do
|
||||
SRC="eapim-portal/build/libs/$WAR"
|
||||
if [ ! -f "$SRC" ]; then
|
||||
echo "::error::missing artifact: $SRC"
|
||||
exit 1
|
||||
fi
|
||||
HTTP=$(curl -sS -o "/tmp/pkg-upload-$WAR.log" -w '%{http_code}' \
|
||||
--user "oauth2:${{ secrets.CI_TOKEN }}" \
|
||||
--upload-file "$SRC" \
|
||||
"$BASE/$WAR")
|
||||
if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
|
||||
echo "::error::upload failed for $WAR (HTTP $HTTP)"
|
||||
cat "/tmp/pkg-upload-$WAR.log" || true
|
||||
exit 1
|
||||
fi
|
||||
echo " uploaded: $WAR (HTTP $HTTP)"
|
||||
done
|
||||
|
||||
echo "published: $GITEA_URL/$PKG_OWNER/-/packages/generic/$PKG_NAME/$VERSION"
|
||||
|
||||
- name: Stop tomcat (pre-clean)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
|
||||
systemctl --user stop eapim-portal 2>/dev/null || true
|
||||
|
||||
# 2) systemd 외부 detached 인스턴스 (eapim-boot-start.sh 등) 정리
|
||||
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null; then
|
||||
JAVA_HOME=$JAVA_HOME_TOMCAT "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
|
||||
for i in $(seq 1 30); do
|
||||
[ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
rm -f "$CATALINA_PID"
|
||||
|
||||
- name: Deploy war as ROOT
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
DEPLOY_DIR="$CATALINA_BASE/webapps"
|
||||
rm -rf "$DEPLOY_DIR/ROOT" "$DEPLOY_DIR/ROOT.war"
|
||||
cp eapim-portal/build/libs/eapim-portal.war "$DEPLOY_DIR/ROOT.war"
|
||||
ls -la "$DEPLOY_DIR"
|
||||
|
||||
- name: Start tomcat and readiness probe (timeout 120s)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||
BEFORE=0
|
||||
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
|
||||
|
||||
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
|
||||
systemctl --user start eapim-portal
|
||||
|
||||
DEADLINE=$(($(date +%s) + 120))
|
||||
LIVE_OK=0
|
||||
STATUS=000
|
||||
BODY=""
|
||||
while [ $(date +%s) -lt $DEADLINE ]; do
|
||||
# Stage 1: liveness — servlet 응답 가능?
|
||||
if [ "$LIVE_OK" = "0" ]; then
|
||||
LIVE=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
http://localhost:$DEPLOY_HTTP_PORT/health 2>/dev/null || echo "000")
|
||||
if [ "$LIVE" = "200" ]; then
|
||||
echo "Liveness OK (/health)"
|
||||
LIVE_OK=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Stage 2: readiness — DB(EMS, Gateway)까지 검증
|
||||
if [ "$LIVE_OK" = "1" ]; then
|
||||
BODY=$(curl -sS -o /tmp/ready-body.json -w '%{http_code}' --max-time 3 \
|
||||
http://localhost:$DEPLOY_HTTP_PORT/health/ready 2>/dev/null || echo "000")
|
||||
STATUS=$BODY
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "Readiness OK (/health/ready)"
|
||||
cat /tmp/ready-body.json
|
||||
echo
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
|
||||
echo "::error::Startup error detected in log"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | tail -80
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "::error::Readiness probe failed within 120s (last HTTP status: $STATUS)"
|
||||
[ -f /tmp/ready-body.json ] && echo "--- last readiness body ---" && cat /tmp/ready-body.json && echo
|
||||
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- key boot log lines ---"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
|
||||
@@ -0,0 +1,60 @@
|
||||
name: eapim-portal E2E Test
|
||||
|
||||
# 수동 트리거 전용. e2e 태그가 붙은 Selenium/통합 테스트만 실행.
|
||||
# 요구 인프라:
|
||||
# - Chrome + libgbm/libxkbcommon-x11 (Selenium 4종)
|
||||
# - Oracle JDBC 또는 testcontainers (SignupFileTest)
|
||||
# 미설치 환경에서는 SessionNotCreated/DB 에러로 일부 케이스가 실패할 수 있음.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: [self-hosted, linux, eapim-portal]
|
||||
env:
|
||||
JAVA_HOME: /apps/opts/jdk8
|
||||
defaults:
|
||||
run:
|
||||
working-directory: eapim-portal
|
||||
|
||||
steps:
|
||||
- name: Checkout eapim-portal (this branch)
|
||||
uses: http://172.30.1.50:3000/actions/checkout@v4
|
||||
with:
|
||||
path: eapim-portal
|
||||
|
||||
- name: Checkout elink-portal-common (master, dependency)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
|
||||
- name: Checkout elink-online-core-jpa (master, dependency)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
mkdir -p eapim-online
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
|
||||
- name: Verify toolchain
|
||||
run: |
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
- name: Gradle e2e test
|
||||
run: gradle test --no-daemon -PincludeE2E=true
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
|
||||
with:
|
||||
name: eapim-portal-e2e-${{ github.sha }}
|
||||
path: |
|
||||
eapim-portal/build/reports/tests/test/
|
||||
eapim-portal/build/test-results/test/
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,63 @@
|
||||
name: eapim-portal Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- stage
|
||||
- feats/ci-test
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: [self-hosted, linux, eapim-portal]
|
||||
env:
|
||||
JAVA_HOME: /apps/opts/jdk8
|
||||
defaults:
|
||||
run:
|
||||
working-directory: eapim-portal
|
||||
|
||||
steps:
|
||||
- name: Checkout eapim-portal (this branch)
|
||||
# ref 미지정 — 트리거된 브랜치/commit(github.sha) 그대로 빌드.
|
||||
uses: http://172.30.1.50:3000/actions/checkout@v4
|
||||
with:
|
||||
path: eapim-portal
|
||||
|
||||
- name: Checkout elink-portal-common (master, dependency)
|
||||
# 의존 모듈은 항상 master 고정.
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
|
||||
- name: Checkout elink-online-core-jpa (master, dependency)
|
||||
# 의존 모듈은 항상 master 고정.
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
mkdir -p eapim-online
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
|
||||
- name: Verify toolchain
|
||||
run: |
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
- name: Gradle test
|
||||
run: gradle test --no-daemon
|
||||
|
||||
- name: Upload test results
|
||||
# 실패해도 결과 리포트는 올려서 원인 분석 가능하게.
|
||||
if: always()
|
||||
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
|
||||
with:
|
||||
name: eapim-portal-test-${{ github.sha }}
|
||||
path: |
|
||||
eapim-portal/build/reports/tests/test/
|
||||
eapim-portal/build/test-results/test/
|
||||
if-no-files-found: warn
|
||||
@@ -83,6 +83,9 @@ file_upload/
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
# SonarScanner 작업 디렉터리
|
||||
.scannerwork/
|
||||
|
||||
|
||||
|
||||
build_knbank.sh
|
||||
@@ -107,4 +110,6 @@ TODO.txt
|
||||
diff
|
||||
|
||||
*rinjae*
|
||||
*gf63*
|
||||
*gf63*
|
||||
*obsidian*
|
||||
design-backup
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
**기술 스택:**
|
||||
- Spring Boot 2.7.18 with Spring MVC and Thymeleaf
|
||||
- Java 8 (targetCompatibility: 1.8)
|
||||
- Java 8 (sourceCompatibility/targetCompatibility: 1.8)
|
||||
- Gradle 8.7 빌드 시스템
|
||||
- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스)
|
||||
- Spring Security (커스텀 인증)
|
||||
@@ -59,7 +59,7 @@ git push origin jenkins_with_weblogic
|
||||
|
||||
### 애플리케이션 실행
|
||||
- gradlew 사용 금지 - offline gradle 단독 실행
|
||||
- msi-gf63 컴퓨터에서는 /c/eactive/workspaces/shell-scripts/kjb-gradle.sh 로 gradle 실행 (jdk, gradle 등 환경 변수 세팅 후 gradle 실행하는 스크립트)
|
||||
- JDK/Gradle 환경 변수는 저장소 루트의 `.envrc`(direnv)로 자동 설정됨 (JAVA_HOME → Zulu JDK 8, GRADLE_USER_HOME → 격리 디렉터리)
|
||||
|
||||
```bash
|
||||
# dev 프로파일로 실행 (기본값)
|
||||
@@ -173,13 +173,13 @@ com.eactive.apim.portal/
|
||||
|
||||
1. **EMS Database** (Portal/Admin)
|
||||
- JNDI: `jdbc/dsOBP_EMS`
|
||||
- Schema: `EMSADM`
|
||||
- Schema: `EMSAPP`
|
||||
- Entities: `com.eactive.apim.portal.apps.*`
|
||||
- 목적: 포털 사용자, 조직, API 키, 승인
|
||||
|
||||
2. **Gateway Database** (API Specs)
|
||||
- JNDI: `jdbc/dsOBP_AGW`
|
||||
- Schema: `AGWADM`
|
||||
- Schema: `AGWAPP`
|
||||
- Entities: `com.eactive.eai.data.entity.onl.*`
|
||||
- 목적: API 명세, 서비스, 메시지
|
||||
|
||||
@@ -265,6 +265,41 @@ API 키는 승인 워크플로우와 함께 관리됩니다:
|
||||
8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가
|
||||
9. `application.yml`의 `pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑)
|
||||
|
||||
### 메뉴/네비게이션 항목 추가하기
|
||||
|
||||
**중요:** 메뉴 정의가 **두 곳으로 분리**되어 있다. yml 한 곳만 고치면 헤더 드롭다운에 안 보이고, 헤더만 고치면 브레드크럼이 비어 보인다. 메뉴 추가/이름 변경/순서 변경 시 **반드시 두 파일을 같이 수정**한다.
|
||||
|
||||
1. **`application.yml`의 `page:` 트리** (브레드크럼·페이지 메타용)
|
||||
- `PageService`가 URL → breadcrumb 매핑에 사용
|
||||
- 부모 메뉴(`service`, `apis`, `community` 등) 아래 `children:`에 새 키를 추가
|
||||
```yaml
|
||||
page:
|
||||
home:
|
||||
children:
|
||||
service:
|
||||
children:
|
||||
oauth2_guide:
|
||||
name: "OAuth2 인증가이드"
|
||||
path: "/service/oauth2-guide"
|
||||
```
|
||||
|
||||
2. **`templates/views/fragment/djbank/header_container.html`** (상단 글로벌 네비)
|
||||
- `<nav class="header-center"><ul class="nav-menu">` 블록에 정적 `<li>`로 하드코딩되어 있다 — yml 트리를 iterate 하지 **않음**
|
||||
- 부모 메뉴의 `<ul class="sub-menu">`에 `<li><a th:href="@{/...}">메뉴명</a></li>` 직접 추가
|
||||
|
||||
3. **라우트도 같이 등록** (`application.yml`의 `portal.pages` 매핑 섹션, 또는 `@GetMapping`)
|
||||
```yaml
|
||||
portal:
|
||||
pages:
|
||||
- path-pattern: /service/oauth2-guide
|
||||
method: GET
|
||||
view-name: apps/service/oauth2-guide
|
||||
```
|
||||
|
||||
4. **검증**: 변경 후 (a) 헤더 드롭다운에 항목이 보이는지, (b) 새 페이지 진입 시 브레드크럼에 메뉴명이 보이는지 둘 다 확인.
|
||||
|
||||
5. **재시작**: `application.yml` 변경은 `PortalPageHandlerMapper`가 `@PostConstruct`에서 한 번만 등록하므로 **자동 라이브 리로드 안 됨** — 수동 재시작 필요. HTML/CSS만 라이브 반영됨.
|
||||
|
||||
### MapStruct DTO 매핑
|
||||
|
||||
타입 안전한 DTO 변환을 위해 MapStruct 사용:
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# JDK 21로 eapim-portal 기동 가이드
|
||||
|
||||
## 환경
|
||||
|
||||
- JDK 21 경로: `D:\installs\jdk-21` (jdk-21.0.10+7 폴더를 복사하여 이름 변경)
|
||||
- Gradle 8.7 (JDK 21 지원)
|
||||
- Spring Boot 2.7.18
|
||||
|
||||
> **주의:** 원본 폴더명(`jdk-21.0.10+7`)에 `+` 문자가 포함되어 있어 CMD에서 JAVA_HOME 설정 시 오류 발생.
|
||||
> 탐색기에서 `D:\installs\jdk-21`로 복사하여 사용.
|
||||
|
||||
---
|
||||
|
||||
## build.gradle 변경 사항
|
||||
|
||||
JDK 17+ 모듈 시스템(JPMS) 호환을 위해 `bootRun` 블록에 `--add-opens` JVM 인수 추가.
|
||||
(Hibernate 5.6.x, Atomikos, JAXB 등의 리플렉션 접근 허용)
|
||||
|
||||
```groovy
|
||||
bootRun {
|
||||
jvmArgs = [
|
||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.math=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.text=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/sun.util.calendar=ALL-UNNAMED',
|
||||
'--add-opens', 'java.rmi/sun.rmi.transport=ALL-UNNAMED',
|
||||
'--add-opens', 'java.sql/java.sql=ALL-UNNAMED',
|
||||
]
|
||||
sourceResources sourceSets.main
|
||||
args = ["--spring.profiles.active=dev"]
|
||||
}
|
||||
```
|
||||
|
||||
> `sourceCompatibility = "1.8"` 은 유지. `var` 키워드 관련 warning이 발생하지만 빌드/실행에는 영향 없음.
|
||||
|
||||
---
|
||||
|
||||
## WAR 빌드 및 실행 (bootWarDev)
|
||||
|
||||
### 1. JAVA_HOME 설정
|
||||
|
||||
```cmd
|
||||
set JAVA_HOME=D:\installs\jdk-21
|
||||
set PATH=%JAVA_HOME%\bin;%PATH%
|
||||
```
|
||||
|
||||
### 2. bootWarDev 빌드
|
||||
|
||||
```cmd
|
||||
gradle bootWarDev
|
||||
```
|
||||
|
||||
출력 파일: `build\libs\eapim-portal-dev.war`
|
||||
|
||||
### 3. 실행
|
||||
|
||||
```cmd
|
||||
chcp 65001
|
||||
D:\installs\jdk-21\bin\java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -jar build\libs\eapim-portal-dev.war --spring.profiles.active=dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 한글 인코딩 설정
|
||||
|
||||
| 항목 | 설정 |
|
||||
|---|---|
|
||||
| CMD 코드페이지 | `chcp 65001` (UTF-8) |
|
||||
| JVM 인수 | `-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8` |
|
||||
|
||||
---
|
||||
|
||||
## bootWarDev vs bootWar 차이
|
||||
|
||||
| | `bootWar` | `bootWarDev` |
|
||||
|---|---|---|
|
||||
| 포함 yml | stage, prod | application.yml + application-dev.yml |
|
||||
| 출력 파일 | `eapim-portal-boot.war` | `eapim-portal-dev.war` |
|
||||
| 용도 | 운영/스테이징 배포 | 로컬 dev 실행 |
|
||||
@@ -0,0 +1,174 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout() // stage별 agent의 자동 SCM checkout 방지 (Deploy 노드는 git 접근 불필요, unstash로만 WAR 수신)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
WEBHOOK_URL = 'http://172.30.1.50:18000/api/hooks/01ba51b01d3c4c98ae8f3183a23a84ed713197857d024b5da4f303458195eb32'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Build (djb-vm)') {
|
||||
agent { label 'djb-vm' }
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
NODE_HOME = '/apps/opts/node-v24'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps { checkout scm }
|
||||
}
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
stage('Verify toolchain') {
|
||||
steps { sh 'set -eu; java -version; gradle --version' }
|
||||
}
|
||||
stage('Test') {
|
||||
steps { sh 'gradle clean test --no-daemon -Pprofile=weblogic' }
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Build WAR') {
|
||||
steps { sh 'gradle build -x test --no-daemon -Pprofile=weblogic' }
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
sha256sum eapim-portal.war > eapim-portal.war.sha256
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal.war.sha256', fingerprint: true
|
||||
stash name: 'war', includes: 'build/libs/eapim-portal.war'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-portal-sbom.xlsx 를 아티팩트로 보관.
|
||||
// 실패해도 배포는 진행하도록 UNSTABLE 로만 표시한다.
|
||||
stage('SBOM') {
|
||||
steps {
|
||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||
sh 'gradle sbomXlsx --no-daemon -Pprofile=weblogic'
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy (weblogic)') {
|
||||
agent { label 'weblogic' }
|
||||
environment {
|
||||
JENKINS_NODE_COOKIE = 'dontKillMe' // background weblogic를 빌드 종료 시 죽이지 않게
|
||||
WL_HOME = '/app/eapim/devportal'
|
||||
WL_DEPLOY_DIR = '/app/eapim/devportal'
|
||||
WL_WAR_NAME = 'eapim-portal.war'
|
||||
WL_HTTP_PORT = '39130'
|
||||
WL_NOHUP = '/logs/weblogic/domains/eapimDomain/nohup.devSvr11.out'
|
||||
}
|
||||
stages {
|
||||
stage('Stop WebLogic') {
|
||||
steps {
|
||||
sh '"$WL_HOME/stopDev11.sh"' // 동기: 완전 종료까지 블록, 미기동 시에도 안전
|
||||
}
|
||||
}
|
||||
stage('Deploy WAR') {
|
||||
steps {
|
||||
unstash 'war'
|
||||
sh '''
|
||||
set -eu
|
||||
cp build/libs/eapim-portal.war "$WL_DEPLOY_DIR/$WL_WAR_NAME"
|
||||
ls -la "$WL_DEPLOY_DIR"
|
||||
'''
|
||||
}
|
||||
}
|
||||
stage('Start WebLogic and readiness') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
"$WL_HOME/startDev11.sh" # nohup & 로 백그라운드 기동, 즉시 리턴
|
||||
|
||||
DEADLINE=$(($(date +%s) + 300))
|
||||
STATUS=000
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
"http://localhost:$WL_HTTP_PORT/health/ready" 2>/dev/null || echo "000")
|
||||
[ "$STATUS" = "200" ] && { echo "Readiness OK (/health/ready)"; break; }
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "Readiness failed within 300s, last HTTP=$STATUS"
|
||||
[ -f "$WL_NOHUP" ] && tail -120 "$WL_NOHUP"
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
node('weblogic') {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) SUCCESS"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
|
||||
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
|
||||
'''
|
||||
}
|
||||
}
|
||||
failure {
|
||||
node('weblogic') {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) FAILURE"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
|
||||
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// SonarQube 정적 보안 분석 전용 파이프라인.
|
||||
//
|
||||
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록의 SONAR_* 만 고치면 된다)
|
||||
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
|
||||
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록
|
||||
// (SONAR_SCANNER_TOOL 이 비어 있으면 흔한 이름을 탐색하고, 없으면 PATH 의 sonar-scanner 사용)
|
||||
// 3) Quality Gate 대기를 쓰려면 SonarQube 서버에
|
||||
// Administration > Configuration > Webhooks 에 <JENKINS_URL>/sonarqube-webhook/ 등록
|
||||
// 4) Job 의 Git 설정에서 shallow clone 을 끄고 tags/history 를 받아와야 blame 기반 "새 코드" 판정이 정확하다.
|
||||
//
|
||||
// 빌드는 JDK 8, 스캐너 실행은 JDK 17 로 분리한다.
|
||||
// (SonarScanner CLI 5.x / SonarQube 10.x 이상은 JRE 17 필수. JDK 8 로 실행하면 UnsupportedClassVersionError)
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/30 * * * *')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||
}
|
||||
|
||||
parameters {
|
||||
booleanParam(
|
||||
name: 'RUN_TESTS',
|
||||
defaultValue: false,
|
||||
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
|
||||
)
|
||||
}
|
||||
|
||||
environment {
|
||||
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
|
||||
// Manage Jenkins > System > SonarQube servers 의 Name
|
||||
SONAR_ENV_NAME = 'SonarQube-Community'
|
||||
// Manage Jenkins > Tools > SonarQube Scanner 에 등록한 이름.
|
||||
// 비워두면 아래 후보 이름들을 순서대로 탐색하고, 그래도 없으면 PATH 의 sonar-scanner 를 쓴다.
|
||||
SONAR_SCANNER_TOOL = ''
|
||||
|
||||
// --- 툴체인 ---
|
||||
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
|
||||
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner 실행용
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/bin:${env.PATH}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
|
||||
// 이 둘이 없으면 컴파일 자체가 불가능하고, 컴파일 산출물이 없으면 Sonar Java 분석이 무력화된다.
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
echo "--- build JDK ---"
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
echo "--- scanner JDK ---"
|
||||
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
|
||||
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
|
||||
echo "SonarScanner 는 JRE 17 이상을 요구한다. 노드에 설치하거나 JAVA_HOME_SCANNER 를 수정할 것."
|
||||
exit 1
|
||||
fi
|
||||
"$JAVA_HOME_SCANNER/bin/java" -version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
// build/generated 잔여 산출물이 남으면 MapStruct Impl / QueryDSL Q클래스가
|
||||
// 중복 생성되어 컴파일이 깨진다. clean 이 파일 잠금 등으로 실패하는 경우가 있어
|
||||
// 생성 소스 디렉터리는 별도로 먼저 지운다.
|
||||
stage('Compile') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
rm -rf build/generated build/classes
|
||||
gradle clean classes testClasses --no-daemon
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
when { expression { return params.RUN_TESTS } }
|
||||
steps {
|
||||
sh 'gradle test --no-daemon'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sonar.java.libraries 용 클래스패스 덤프.
|
||||
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
|
||||
stage('Export analysis classpath') {
|
||||
steps {
|
||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('SonarQube analysis') {
|
||||
steps {
|
||||
script {
|
||||
// 스캐너 위치 결정: 지정 이름 > 흔한 등록 이름 후보 > PATH 의 sonar-scanner
|
||||
def candidates = env.SONAR_SCANNER_TOOL?.trim()
|
||||
? [env.SONAR_SCANNER_TOOL.trim()]
|
||||
: ['SonarScanner', 'SonarQube Scanner', 'sonar-scanner', 'SonarScanner CLI', 'sonarqube-scanner']
|
||||
|
||||
def scannerHome = null
|
||||
for (name in candidates) {
|
||||
try {
|
||||
scannerHome = tool name: name, type: 'hudson.plugins.sonar.SonarRunnerInstallation'
|
||||
echo "SonarScanner tool: '${name}' -> ${scannerHome}"
|
||||
break
|
||||
} catch (ignored) {
|
||||
// 등록되지 않은 이름은 건너뛴다
|
||||
}
|
||||
}
|
||||
|
||||
if (scannerHome == null) {
|
||||
echo 'Jenkins Tools 에 등록된 SonarQube Scanner 를 찾지 못했다. PATH 의 sonar-scanner 로 진행한다.'
|
||||
echo '(Manage Jenkins > Tools 에 등록한 뒤 environment 의 SONAR_SCANNER_TOOL 에 그 이름을 넣으면 확실하다.)'
|
||||
}
|
||||
|
||||
withSonarQubeEnv(env.SONAR_ENV_NAME) {
|
||||
withEnv(["SONAR_SCANNER_HOME=${scannerHome ?: ''}"]) {
|
||||
sh '''
|
||||
set -eu
|
||||
|
||||
# 스캐너는 JDK 17 로 실행한다 (빌드용 JDK 8 과 분리)
|
||||
export JAVA_HOME="$JAVA_HOME_SCANNER"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
|
||||
# 토큰은 커맨드라인(-Dsonar.token)에 노출시키지 않고 환경변수로만 넘긴다.
|
||||
# 플러그인 버전에 따라 SONAR_AUTH_TOKEN 만 주입되는 경우가 있다.
|
||||
if [ -z "${SONAR_TOKEN:-}" ] && [ -n "${SONAR_AUTH_TOKEN:-}" ]; then
|
||||
export SONAR_TOKEN="$SONAR_AUTH_TOKEN"
|
||||
fi
|
||||
|
||||
LIBS=""
|
||||
if [ -f build/sonar/java-libraries.txt ]; then
|
||||
LIBS=$(cat build/sonar/java-libraries.txt)
|
||||
else
|
||||
echo "WARN: build/sonar/java-libraries.txt 없음 - 타입 해석 정확도 저하"
|
||||
fi
|
||||
|
||||
TEST_LIBS=""
|
||||
if [ -f build/sonar/java-test-libraries.txt ]; then
|
||||
TEST_LIBS=$(cat build/sonar/java-test-libraries.txt)
|
||||
fi
|
||||
|
||||
JUNIT_ARG=""
|
||||
if [ -d build/test-results/test ]; then
|
||||
JUNIT_ARG="-Dsonar.junit.reportPaths=build/test-results/test"
|
||||
fi
|
||||
|
||||
# Tools 에 등록된 스캐너가 없으면 PATH 에서 찾는다
|
||||
if [ -n "${SONAR_SCANNER_HOME:-}" ] && [ -x "$SONAR_SCANNER_HOME/bin/sonar-scanner" ]; then
|
||||
SCANNER="$SONAR_SCANNER_HOME/bin/sonar-scanner"
|
||||
elif command -v sonar-scanner >/dev/null 2>&1; then
|
||||
SCANNER="$(command -v sonar-scanner)"
|
||||
else
|
||||
echo "sonar-scanner 실행 파일을 찾지 못했다."
|
||||
echo "Manage Jenkins > Tools > SonarQube Scanner 에 등록하거나 노드 PATH 에 설치할 것."
|
||||
exit 1
|
||||
fi
|
||||
echo "scanner: $SCANNER"
|
||||
|
||||
"$SCANNER" \
|
||||
-Dsonar.projectVersion="${BUILD_NUMBER}" \
|
||||
-Dsonar.java.libraries="$LIBS" \
|
||||
-Dsonar.java.test.libraries="$TEST_LIBS" \
|
||||
$JUNIT_ARG
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
|
||||
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
|
||||
stage('Quality Gate') {
|
||||
steps {
|
||||
script {
|
||||
def qg = null
|
||||
try {
|
||||
timeout(time: 15, unit: 'MINUTES') {
|
||||
qg = waitForQualityGate abortPipeline: false
|
||||
}
|
||||
} catch (err) {
|
||||
unstable("Quality Gate 결과 대기 실패/타임아웃: ${err}")
|
||||
return
|
||||
}
|
||||
|
||||
if (qg == null) {
|
||||
unstable('Quality Gate 결과를 받지 못했다. SonarQube 웹훅 설정을 확인할 것.')
|
||||
} else if (qg.status != 'OK') {
|
||||
unstable("Quality Gate ${qg.status}")
|
||||
} else {
|
||||
echo 'Quality Gate OK'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: '.scannerwork/report-task.txt'
|
||||
script {
|
||||
def report = '.scannerwork/report-task.txt'
|
||||
if (fileExists(report)) {
|
||||
def url = readFile(report).readLines().find { it.startsWith('dashboardUrl=') }
|
||||
if (url) {
|
||||
echo "SonarQube: ${url.substring('dashboardUrl='.length())}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/10 * * * *')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
NODE_HOME = '/apps/opts/node-v24'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
java -version
|
||||
gradle --version
|
||||
node --version || true
|
||||
npm --version || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh 'gradle clean test --no-daemon'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build WAR') {
|
||||
steps {
|
||||
sh 'gradle build -x test --no-daemon'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
for f in eapim-portal.war eapim-portal-boot.war; do
|
||||
sha1sum "$f" > "$f.sha1"
|
||||
sha256sum "$f" > "$f.sha256"
|
||||
md5sum "$f" > "$f.md5"
|
||||
done
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/eapim-portal.war.sha1,build/libs/eapim-portal.war.sha256,build/libs/eapim-portal.war.md5,build/libs/eapim-portal-boot.war.sha1,build/libs/eapim-portal-boot.war.sha256,build/libs/eapim-portal-boot.war.md5', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-portal-sbom.xlsx 를 아티팩트로 보관.
|
||||
// 실패해도 빌드는 진행하도록 UNSTABLE 로만 표시한다.
|
||||
stage('SBOM') {
|
||||
steps {
|
||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||
sh 'gradle sbomXlsx --no-daemon'
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,17 @@ eapim-portal
|
||||
|
||||
### 사전 요구사항
|
||||
|
||||
- JDK 8 이상
|
||||
- Gradle 8.7
|
||||
아래 도구 설치 및 검증 절차는 별도 문서로 분리되어 있습니다. 신규 개발자는 먼저 이 문서를 끝까지 진행하세요.
|
||||
|
||||
📄 **[개발환경 준비 사항](djb-docs/개발환경-준비-사항.md)** — JDK 8 · Gradle 8.7 · Git · Oracle 19c · Node.js & SASS · direnv · IDE 설정
|
||||
|
||||
요약:
|
||||
|
||||
- JDK 8 (Zulu 권장)
|
||||
- Gradle 8.7 (gradlew 사용 금지)
|
||||
- Git
|
||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
||||
- Oracle 19c 접근 (개발: `192.168.240.177:1599`)
|
||||
- Node.js 18 LTS 이상 (SCSS 컴파일용)
|
||||
|
||||
### Git Bash에서 초기 세팅
|
||||
|
||||
@@ -90,10 +97,7 @@ 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. 작업 디렉토리 생성
|
||||
@@ -128,12 +132,6 @@ 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. 최종 폴더 구조 확인
|
||||
# ====================================
|
||||
@@ -142,7 +140,6 @@ git checkout master # 또는 필요한 브랜치
|
||||
# ├── eapim-online/
|
||||
# │ └── elink-online-core-jpa/
|
||||
# ├── elink-portal-common/
|
||||
# └── kjb-safedb/
|
||||
|
||||
# ====================================
|
||||
# 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
@@ -169,10 +166,7 @@ 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. 작업 디렉토리 생성
|
||||
@@ -207,12 +201,6 @@ 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 ====================================
|
||||
@@ -221,7 +209,6 @@ REM ├── eapim-portal\
|
||||
REM ├── eapim-online\
|
||||
REM │ └── elink-online-core-jpa\
|
||||
REM ├── elink-portal-common\
|
||||
REM └── kjb-safedb\
|
||||
|
||||
REM ====================================
|
||||
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
@@ -253,30 +240,19 @@ REM Gradle 디펜던시 다운로드
|
||||
|
||||
- **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
|
||||
```
|
||||
|
||||
### 빌드 및 실행 명령어
|
||||
@@ -310,7 +286,6 @@ kjb-eapim/
|
||||
├── eapim-online/ # Online 프로젝트
|
||||
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
|
||||
├── elink-portal-common/ # 공통 유틸리티
|
||||
└── kjb-safedb/ # SafeDB 암호화 라이브러리
|
||||
```
|
||||
|
||||
### eapim-portal 내부 구조
|
||||
@@ -327,7 +302,6 @@ src/main/java/com/eactive/apim/portal/
|
||||
이 프로젝트는 다음 모듈에 의존합니다 (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 스타일 개발
|
||||
|
||||
@@ -339,8 +313,11 @@ src/main/java/com/eactive/apim/portal/
|
||||
|
||||
### 설치
|
||||
|
||||
Node.js 및 SASS 설치 절차는 [개발환경 준비 사항 § 6. Node.js & SASS 빌드 환경](djb-docs/개발환경-준비-사항.md#6-nodejs--sass-빌드-환경) 을 참고하세요.
|
||||
|
||||
Node.js가 이미 설치되어 있다면 프로젝트 의존성만 설치하면 됩니다:
|
||||
|
||||
```bash
|
||||
# Node.js가 설치된 상태에서
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -532,8 +509,6 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
||||
|
||||
- 사용자 관리 (내부/기업)
|
||||
- API 키 관리 및 승인 워크플로우
|
||||
- API 카탈로그 및 문서화
|
||||
- API 테스트 프록시
|
||||
- 감사 추적 (Hibernate Envers)
|
||||
|
||||
## 배포
|
||||
@@ -543,24 +518,10 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
||||
- WebLogic
|
||||
- Tomcat (내장)
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
./build_docker.sh
|
||||
docker run -p 8080:8080 eapim-portal:latest
|
||||
```
|
||||
|
||||
## 관련 프로젝트
|
||||
|
||||
프로젝트 구조 (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 스크립트 (선택사항)
|
||||
|
||||
## 문서
|
||||
|
||||
- **개발환경 준비 사항**: [`djb-docs/개발환경-준비-사항.md`](djb-docs/개발환경-준비-사항.md) — JDK·Gradle·Node.js·SASS 설치 가이드
|
||||
- **메뉴 관리 개발 가이드**: [`readme-docs/메뉴-관리-개발-가이드.md`](readme-docs/메뉴-관리-개발-가이드.md) — menu.yml/roles.yml 스키마·시딩 규칙·캐시 리로드·admin 포탈메뉴관리 연동
|
||||
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
|
||||
- **사용자 가이드**: `개발자포탈.md` (한글)
|
||||
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
echo "========================"
|
||||
java -version
|
||||
javac -version
|
||||
echo "========================"
|
||||
gradle --version
|
||||
echo "========================"
|
||||
|
||||
gradle clean build -x test --no-daemon
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'war'
|
||||
id 'eclipse'
|
||||
id 'idea'
|
||||
id 'org.cyclonedx.bom' version '3.2.4'
|
||||
id 'org.springframework.boot' version '2.7.18'
|
||||
id 'io.spring.dependency-management' version '1.1.3'
|
||||
}
|
||||
@@ -23,15 +24,36 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 전용 라이브러리 — WebLogic 배포 산출물(war/bootWar)에서 제외한다.
|
||||
// 정보보호 점검에서 Actuator/모니터링 라이브러리가 배포본에 실려 있으면 불필요하게 탐지되므로
|
||||
// 로컬(bootRun/IDE)에서만 classpath 에 오르게 한다. 제외 로직은 아래 war/bootWar 블록.
|
||||
//
|
||||
// ※ Spring Boot 의 developmentOnly 를 쓰지 않는 이유
|
||||
// developmentOnly 는 bootJar/bootWar 에서만 제외된다. 표준 war task 는 runtimeClasspath 를
|
||||
// 그대로 쓰므로 산출물에 실린다 (실측: devtools 가 eapim-portal.war 에 포함되어 있었음).
|
||||
// ※ 별도 configuration(localOnly)을 쓰지 않는 이유
|
||||
// runtimeClasspath 에서 빠지면 IntelliJ 가 모듈 classpath 를 구성할 때도 빠져
|
||||
// IDE 로 기동할 때 Actuator/SBA 가 동작하지 않는다.
|
||||
def localOnlyLibPrefixes = [
|
||||
'spring-boot-devtools',
|
||||
'spring-boot-starter-actuator',
|
||||
'spring-boot-actuator', // spring-boot-actuator, -autoconfigure 모두 매칭
|
||||
'micrometer-', // actuator 전용(runtimeClasspath 상 다른 출처 없음 — 확인함)
|
||||
'spring-boot-admin-',
|
||||
]
|
||||
|
||||
dependencies {
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28"
|
||||
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
|
||||
implementation 'com.eactive.elink.common:elink-common-data:4.5.5'
|
||||
|
||||
// implementation 'io.swagger.core.v3:swagger-core:2.2.25'
|
||||
// implementation 'io.swagger.parser.v3:swagger-parser:2.1.23'
|
||||
implementation fileTree(dir: 'libs/swagger', include: ['*.jar'])
|
||||
// OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체.
|
||||
// swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는
|
||||
// 아직 swagger-core/parser 어느 버전도 미지원. 지원 추가 시 버전만 상향하면 됨. JDK8 호환.
|
||||
implementation 'io.swagger.core.v3:swagger-core:2.2.52' // io.swagger.v3.oas.models.*, io.swagger.v3.core.util.Json
|
||||
implementation 'io.swagger.parser.v3:swagger-parser:2.1.45' // io.swagger.parser.OpenAPIParser, io.swagger.v3.parser.*
|
||||
implementation 'io.swagger:swagger-annotations:1.6.14' // io.swagger.annotations.ApiModelProperty (CommissionSearch)
|
||||
|
||||
implementation project(':elink-online-core-jpa')
|
||||
implementation project(':elink-portal-common')
|
||||
@@ -41,6 +63,23 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
|
||||
implementation('org.springframework.boot:spring-boot-starter-web')
|
||||
implementation('org.springframework.boot:spring-boot-starter-validation')
|
||||
|
||||
// ↓ 로컬 전용. war/bootWar 산출물에서는 localOnlyLibPrefixes 로 제외된다.
|
||||
// runtimeOnly 인 이유: compileClasspath 에서 빠지므로 자바 코드가 이 API 를 참조하면
|
||||
// 컴파일 단계에서 막힌다. implementation 이면 참조가 컴파일에 통과해버리고,
|
||||
// 배포본(actuator/SBA 제외됨)에서 NoClassDefFoundError 로 터진다.
|
||||
// IntelliJ / bootRun 은 runtimeClasspath 기준이라 로컬 기동에는 정상 포함된다.
|
||||
// 설정(application-local*.yml)으로만 사용한다.
|
||||
runtimeOnly('org.springframework.boot:spring-boot-starter-actuator')
|
||||
// Spring Boot Admin client. 2.7.16 = Spring Boot 2.7.x 대응 마지막 계열(Java 8 호환).
|
||||
runtimeOnly('de.codecentric:spring-boot-admin-starter-client:2.7.16')
|
||||
// 위 runtimeOnly 원칙의 유일한 예외. InterceptorsEndpointConfig 가 actuator API
|
||||
// (@Endpoint / InfoContributor)를 참조해야 하므로 compileClasspath 에만 올린다.
|
||||
// compileOnly 는 runtimeClasspath 에 포함되지 않으므로 war/bootWar 산출물에는 영향이 없다.
|
||||
// 해당 클래스는 @ConditionalOnClass 로 actuator 부재 시 로드되지 않으니
|
||||
// 배포본(actuator 제외)에서 NoClassDefFoundError 가 나지 않는다.
|
||||
compileOnly('org.springframework.boot:spring-boot-actuator')
|
||||
|
||||
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
|
||||
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
|
||||
|
||||
@@ -76,9 +115,10 @@ dependencies {
|
||||
// exclude group: 'commons-collections', module: 'commons-collections'
|
||||
}
|
||||
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.3'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.3'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
|
||||
// WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. JDK8 호환.
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.6'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.6'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.6'
|
||||
|
||||
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
|
||||
|
||||
@@ -123,36 +163,41 @@ bootRun {
|
||||
// 디버깅 사용시에는 아래 내용 주석 해제 하세요
|
||||
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
|
||||
|
||||
// JDK 17+ (모듈 시스템) 호환을 위한 설정 - Hibernate 5.x, Atomikos, JAXB 등
|
||||
jvmArgs = [
|
||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.math=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.text=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/sun.util.calendar=ALL-UNNAMED',
|
||||
'--add-opens', 'java.rmi/sun.rmi.transport=ALL-UNNAMED',
|
||||
'--add-opens', 'java.sql/java.sql=ALL-UNNAMED',
|
||||
]
|
||||
|
||||
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
|
||||
args = ["--spring.profiles.active=dev"]
|
||||
|
||||
// 로컬 기동 기본 프로파일.
|
||||
// dev/stage/prod 는 리버스프록시(OHS) 뒤 WAR 배포용이라 server.forward-headers-strategy=framework 가
|
||||
// 걸려 있다. framework 는 신뢰 프록시 목록 없이 X-Forwarded-* 를 그대로 신뢰하므로 앞단이 없는
|
||||
// 로컬 기동에는 쓰지 않는다(로컬은 공통 기본값 native = Tomcat RemoteIpValve, 사설대역만 신뢰).
|
||||
// 다른 프로파일로 띄우려면: gradle bootRun -PbootProfile=dev
|
||||
// ('profile' 이 아니라 'bootProfile' 인 이유: 위 ext 블록이 profile='local' 을 이미 점유하고 있어
|
||||
// findProperty('profile') 은 -P 지정 여부와 무관하게 항상 'local' 을 돌려준다.)
|
||||
def bootRunProfile = (project.findProperty('bootProfile') ?: 'local_rinjaemac').toString()
|
||||
args = ["--spring.profiles.active=" + bootRunProfile]
|
||||
}
|
||||
|
||||
|
||||
// annotation processor 출력 디렉토리는 IntelliJ/Gradle 통합이 자동 등록함.
|
||||
// 명시적으로 srcDir 추가하면 IntelliJ idea 모드에서 MapStruct가 같은 파일을
|
||||
// 중복 생성하려다 javax.annotation.processing.FilerException 발생함.
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir 'src/main/java'
|
||||
srcDir 'build/generated/sources/annotationProcessor/java/main'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
annotationProcessor
|
||||
|
||||
// WebLogic 배포 시 Tyrus WebSocket 필터(weblogic.websocket.tyrus.TyrusServletFilter)와
|
||||
// 충돌 방지: WAR 에 번들된 Tomcat WsSci 가 javax.websocket.server.ServerContainer 속성을
|
||||
// WsServerContainer 로 등록 → WebLogic Tyrus 필터가 TyrusServerContainer 로 캐스팅하다 실패.
|
||||
// 앱은 WebSocket 미사용이므로 Tomcat WebSocket 모듈 제외.
|
||||
all {
|
||||
exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket'
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -162,48 +207,49 @@ compileJava {
|
||||
}
|
||||
|
||||
processResources {
|
||||
exclude { details ->
|
||||
details.file.name.startsWith('application-') &&
|
||||
details.file.name.endsWith('.yml') &&
|
||||
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
}
|
||||
// exclude { details ->
|
||||
// details.file.name.startsWith('application-') &&
|
||||
// details.file.name.endsWith('.yml') &&
|
||||
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
// }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
useJUnitPlatform {
|
||||
if (project.hasProperty('includeE2E') && project.includeE2E.toBoolean()) {
|
||||
includeTags 'e2e'
|
||||
} else {
|
||||
excludeTags 'e2e'
|
||||
}
|
||||
}
|
||||
enabled = true
|
||||
}
|
||||
|
||||
// 로컬 전용 설정 파일. 배포 산출물(WAR)에 실리면 Actuator/SBA 설정이 그대로 노출되어
|
||||
// 정보보호 점검에 불필요하게 걸린다. processResources 는 건드리지 않는다
|
||||
// (bootRun 이 build/resources/main 을 그대로 쓰므로 로컬 기동이 깨진다).
|
||||
def localOnlyResources = ['**/application-local*.yml']
|
||||
|
||||
// 배포 산출물에서 로컬 전용 라이브러리를 걸러낸다. localOnlyLibPrefixes 는 파일 상단 정의.
|
||||
def excludeLocalOnlyLibs = { org.gradle.api.file.FileCollection cp ->
|
||||
cp.filter { f -> !localOnlyLibPrefixes.any { p -> f.name.startsWith(p) } }
|
||||
}
|
||||
|
||||
bootWar {
|
||||
archiveFileName = "eapim-portal-boot.war"
|
||||
mainClass = 'com.eactive.apim.portal.PortalApplication'
|
||||
rootSpec.exclude(localOnlyResources)
|
||||
classpath = excludeLocalOnlyLibs(classpath)
|
||||
}
|
||||
|
||||
task bootWarDev(type: org.springframework.boot.gradle.tasks.bundling.BootWar) {
|
||||
group = 'build'
|
||||
description = 'dev 프로파일용 실행 가능 WAR (application.yml + application-dev.yml 포함)'
|
||||
dependsOn compileJava
|
||||
mainClass = bootWar.mainClass
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
|
||||
// processResources 결과물 포함 (stage, prod yml 제외된 상태)
|
||||
from(processResources.destinationDir)
|
||||
|
||||
// application.yml, application-dev.yml 추가 (중복 시 srcDirs 파일 우선)
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
from(sourceSets.main.resources.srcDirs) {
|
||||
include 'application.yml'
|
||||
include 'application-dev.yml'
|
||||
}
|
||||
|
||||
archiveFileName = "eapim-portal-dev.war"
|
||||
}//gradlew clean bootWarDev, java -jar build/libs/eapim-portal-dev.war --spring.profiles.active=dev
|
||||
|
||||
war {
|
||||
archiveFileName = "eapim-portal.war"
|
||||
|
||||
from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' }
|
||||
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
||||
|
||||
rootSpec.exclude(localOnlyResources)
|
||||
classpath = excludeLocalOnlyLibs(classpath)
|
||||
}
|
||||
|
||||
task printSourceSets {
|
||||
@@ -216,19 +262,5 @@ task printSourceSets {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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+')
|
||||
}
|
||||
}
|
||||
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
|
||||
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
./gradlew bootWar
|
||||
docker build -t eactive-portal .
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SonarScanner 의 sonar.java.libraries / sonar.java.test.libraries 로 넘길
|
||||
// 컴파일 클래스패스를 파일로 덤프한다.
|
||||
//
|
||||
// build.gradle 을 건드리지 않기 위해 init script(-I) 로만 주입한다.
|
||||
// gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon
|
||||
//
|
||||
// 출력(콤마 구분, 절대경로):
|
||||
// build/sonar/java-libraries.txt main 컴파일 클래스패스
|
||||
// build/sonar/java-test-libraries.txt main + test 컴파일 클래스패스
|
||||
//
|
||||
// 이 파일이 없으면 Sonar Java 분석기가 타입을 해석하지 못해
|
||||
// 보안 룰(SQL Injection, XSS 등) 상당수가 침묵한다. 선택 사항이 아니다.
|
||||
|
||||
rootProject { project ->
|
||||
project.plugins.withId('java') {
|
||||
project.tasks.register('exportSonarClasspath') {
|
||||
group = 'verification'
|
||||
description = 'SonarScanner 용 컴파일 클래스패스를 build/sonar 에 덤프한다'
|
||||
|
||||
doLast {
|
||||
def outDir = new File(project.layout.buildDirectory.get().asFile, 'sonar')
|
||||
outDir.mkdirs()
|
||||
|
||||
def toLine = { files ->
|
||||
files.findAll { it.exists() }
|
||||
.collect { it.absolutePath }
|
||||
.unique()
|
||||
.join(',')
|
||||
}
|
||||
|
||||
def mainCp = project.sourceSets.main.compileClasspath.files
|
||||
def testCp = project.sourceSets.test.compileClasspath.files
|
||||
|
||||
new File(outDir, 'java-libraries.txt').text = toLine(mainCp)
|
||||
new File(outDir, 'java-test-libraries.txt').text = toLine(mainCp + testCp)
|
||||
|
||||
logger.lifecycle("sonar classpath dumped: ${outDir}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the war file is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /path/to/DEVPortal.war"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
WAR_FILE="$1"
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
DEST_DIR="/app/apim/deploy/DVPWeb"
|
||||
BACKUP_DIR="/app/apim/backup"
|
||||
|
||||
# Create backup directory if not exists
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup the existing directory
|
||||
if [ -d "$DEST_DIR" ]; then
|
||||
echo "Backing up existing directory..."
|
||||
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
# Create destination directory if not exists
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
# Unzip the file to the dedicated location
|
||||
echo "Extracting WAR file to $DEST_DIR..."
|
||||
unzip $WAR_FILE -d $DEST_DIR
|
||||
|
||||
echo "deploy successful"
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the war file is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /path/to/DEVPortal.war"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
WAR_FILE="$1"
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
DEST_DIR="/app/apim/deploy/DVPWeb"
|
||||
STATIC_DIR="/app/apim/deploy/static"
|
||||
BACKUP_DIR="/app/apim/backup"
|
||||
|
||||
# Create backup directory if not exists
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup the existing directory
|
||||
if [ -d "$STATIC_DIR" ]; then
|
||||
echo "Backing up existing static directory..."
|
||||
mv $STATIC_DIR "${BACKUP_DIR}/dvp_static_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
if [ -d "$DEST_DIR" ]; then
|
||||
echo "Backing up existing directory..."
|
||||
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
|
||||
# Create destination directory if not exists
|
||||
mkdir -p $DEST_DIR
|
||||
mkdir -p $STATIC_DIR
|
||||
|
||||
# unzip the file to the dedicated location
|
||||
echo "Extracting WAR file to $DEST_DIR..."
|
||||
unzip $WAR_FILE -d $DEST_DIR
|
||||
mv $DEST_DIR/static/* $STATIC_DIR/
|
||||
|
||||
echo "deploy successful"
|
||||
@@ -0,0 +1,273 @@
|
||||
# 개발환경 준비 사항
|
||||
|
||||
EAPIM Portal 개발에 필요한 도구와 런타임을 설치하고 검증하는 절차입니다.
|
||||
처음 환경을 세팅하는 신규 개발자는 이 문서를 끝까지 따라 진행한 뒤 [README.md](../README.md)의 "초기 세팅 가이드" 단계를 이어서 수행하세요.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보는 요구사항
|
||||
|
||||
| 구분 | 도구 | 권장 버전 | 비고 |
|
||||
|------|------|-----------|----------------------------------|
|
||||
| 백엔드 | JDK | 1.8 (Zulu 8 권장) | `sourceCompatibility=1.8` |
|
||||
| 빌드 | Gradle | 8.7 | gradlew 사용 금지 — 시스템 gradle 직접 사용 |
|
||||
| 형상관리 | Git | 2.30 이상 | SSH 키 등록 권장 |
|
||||
| DB | Oracle Client/접근 | 19c | 개발은 `192.168.240.177:1598` |
|
||||
| 프론트엔드 빌드 | Node.js | 18 LTS 이상 | SCSS 컴파일 전용 |
|
||||
| 프론트엔드 빌드 | npm | Node.js 함께 설치 | `sass` 패키지 사용 |
|
||||
| (선택) 환경 변수 | direnv | 최신 | `.envrc` 자동 로드 |
|
||||
| (선택) IDE | IntelliJ IDEA | 2023.x 이상 | Lombok / MapStruct 플러그인 |
|
||||
|
||||
---
|
||||
|
||||
## 1. JDK 8 설치
|
||||
|
||||
Java 8 (1.8) 호환이 필요합니다. Azul Zulu JDK 8 사용을 권장합니다.
|
||||
|
||||
### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install --cask zulu@8
|
||||
# 설치 후 경로 확인
|
||||
/usr/libexec/java_home -v 1.8
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. [Azul Zulu JDK 8 다운로드](https://www.azul.com/downloads/?version=java-8-lts&package=jdk)
|
||||
2. MSI 설치 후 `JAVA_HOME` 환경변수 등록
|
||||
3. `PATH`에 `%JAVA_HOME%\bin` 추가
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
java -version # 1.8.x
|
||||
javac -version # 1.8.x
|
||||
echo $JAVA_HOME # macOS/Linux
|
||||
echo %JAVA_HOME% # Windows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Gradle 8.7 설치
|
||||
|
||||
이 프로젝트는 **gradlew 사용을 금지**합니다. 시스템에 설치된 gradle 8.7을 직접 사용합니다.
|
||||
|
||||
### macOS (Homebrew + SDKMAN 권장)
|
||||
|
||||
```bash
|
||||
# SDKMAN 설치 (이미 있으면 생략)
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
|
||||
# Gradle 8.7 설치
|
||||
sdk install gradle 8.7
|
||||
sdk default gradle 8.7
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. [Gradle 8.7 Binary-only 다운로드](https://gradle.org/releases/)
|
||||
2. `C:\gradle\gradle-8.7` 경로에 압축 해제
|
||||
3. `GRADLE_HOME=C:\gradle\gradle-8.7` 환경변수 등록
|
||||
4. `PATH`에 `%GRADLE_HOME%\bin` 추가
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
gradle -v
|
||||
# Gradle 8.7
|
||||
# JVM: 1.8.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. (권장) direnv 사용
|
||||
|
||||
저장소 루트의 `.envrc` 가 `JAVA_HOME`, `GRADLE_USER_HOME` 등을 자동 주입합니다.
|
||||
디렉터리 진입만으로 환경이 격리되므로 다른 프로젝트와 JDK가 섞이지 않습니다.
|
||||
|
||||
### 설치
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install direnv
|
||||
|
||||
# Windows (scoop)
|
||||
scoop install direnv
|
||||
```
|
||||
|
||||
### 셸 훅 등록 (한 번만)
|
||||
|
||||
```bash
|
||||
# zsh
|
||||
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
|
||||
|
||||
# bash
|
||||
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
### 프로젝트에서 허용
|
||||
|
||||
```bash
|
||||
cd /path/to/eapim-portal
|
||||
direnv allow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Git 설치
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install git
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Git for Windows](https://git-scm.com/download/win) 설치. Git Bash가 함께 설치되어 README의 Bash 스니펫을 그대로 사용할 수 있습니다.
|
||||
|
||||
### SSH 키 등록 (사내 Git 서버용)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "your.email@djbank.co.kr"
|
||||
cat ~/.ssh/id_ed25519.pub
|
||||
# 출력된 공개키를 사내 Git 서버(192.168.240.178:18081)에 등록
|
||||
```
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
git --version
|
||||
ssh -T git@192.168.240.178 -p 18081
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Oracle 19c 접근 정보
|
||||
|
||||
개발 환경에서는 별도 Oracle 설치 없이 사내 개발 DB에 접속합니다.
|
||||
|
||||
- **Host**: `192.168.240.177`
|
||||
- **Port**: `1599`
|
||||
- **Schemas**: `EMSADM` (Portal), `AGWADM` (Gateway)
|
||||
- 상세 계정/패스워드는 사내 비밀번호 관리자에게 문의
|
||||
|
||||
스테이징/운영은 JNDI를 통해 접근합니다 — `jdbc/dsOBP_EMS`, `jdbc/dsOBP_AGW`.
|
||||
|
||||
DB 클라이언트 도구는 자유 선택 (DBeaver, DataGrip, SQL Developer 등).
|
||||
|
||||
---
|
||||
|
||||
## 6. Node.js & SASS 빌드 환경
|
||||
|
||||
SCSS 파일을 CSS로 컴파일하기 위해 Node.js 환경이 필요합니다.
|
||||
`src/main/resources/static/css/main.css` 는 SASS 컴파일 결과물이므로 **반드시 SCSS 파일을 수정한 뒤 컴파일**해야 합니다.
|
||||
|
||||
### 6.1 Node.js 설치 (LTS 18 이상)
|
||||
|
||||
#### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install node
|
||||
```
|
||||
|
||||
#### macOS / Linux (nvm 권장 — 버전 관리 용이)
|
||||
|
||||
```bash
|
||||
# nvm 설치
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
exec $SHELL -l
|
||||
|
||||
# Node.js LTS 설치
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
1. [Node.js LTS 다운로드](https://nodejs.org/en/download) 후 MSI 설치
|
||||
2. 또는 [nvm-windows](https://github.com/coreybutler/nvm-windows/releases) 사용
|
||||
|
||||
#### 검증
|
||||
|
||||
```bash
|
||||
node -v # v18.x 이상
|
||||
npm -v # 9.x 이상
|
||||
```
|
||||
|
||||
### 6.2 프로젝트 SASS 의존성 설치
|
||||
|
||||
```bash
|
||||
cd /path/to/eapim-portal
|
||||
npm install
|
||||
```
|
||||
|
||||
`package.json` 의 `devDependencies.sass` (Dart Sass) 가 `node_modules/` 에 설치됩니다.
|
||||
|
||||
### 6.3 SASS 빌드 명령어
|
||||
|
||||
```bash
|
||||
# 1회 컴파일 (개발용, 들여쓰기 유지)
|
||||
npm run sass:build
|
||||
|
||||
# 1회 컴파일 (배포용, minified)
|
||||
npm run sass:build:minified
|
||||
|
||||
# 파일 변경 감지 후 자동 컴파일 (개발 중 권장)
|
||||
npm run sass:watch
|
||||
|
||||
# 빌드 + minified 동시 생성
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 6.4 SASS 컴파일 검증
|
||||
|
||||
```bash
|
||||
ls -lh src/main/resources/static/css/main.css
|
||||
ls -lh src/main/resources/static/css/main.min.css
|
||||
```
|
||||
|
||||
파일이 갱신되었고 브라우저에서 스타일이 정상 적용되면 성공입니다.
|
||||
|
||||
### 6.5 SASS 작업 시 주의사항
|
||||
|
||||
- `src/main/resources/static/css/main.css` **직접 수정 금지** — 재컴파일 시 덮어씌워짐
|
||||
- SCSS 파일은 `src/main/resources/static/sass/` 하위에서만 작성
|
||||
- 새 파일 추가 시 파일명 앞에 언더스코어(`_`) 필수 (`_filename.scss`)
|
||||
- `@import` 대신 `@use` 사용 (Dart Sass 3.0 호환)
|
||||
|
||||
---
|
||||
|
||||
## 7. (선택) IDE 설정
|
||||
|
||||
### IntelliJ IDEA
|
||||
|
||||
1. **Lombok 플러그인** 설치 → `Settings → Plugins → Lombok`
|
||||
2. **Annotation Processing 활성화** → `Settings → Build → Compiler → Annotation Processors → Enable annotation processing`
|
||||
3. **Gradle JVM** 설정 → `Settings → Build → Build Tools → Gradle → Gradle JVM` 을 JDK 1.8 (Zulu) 로 지정
|
||||
4. **MapStruct** 는 별도 플러그인 불필요 (annotation processor 만 활성화되어 있으면 됨)
|
||||
|
||||
### VS Code
|
||||
|
||||
- `Extension Pack for Java`
|
||||
- `Lombok Annotations Support`
|
||||
- `Sass` (Syler) — SCSS 문법 하이라이팅
|
||||
|
||||
---
|
||||
|
||||
## 8. 최종 체크리스트
|
||||
|
||||
아래 명령이 모두 정상 동작하면 개발환경 준비가 완료된 것입니다.
|
||||
|
||||
```bash
|
||||
java -version # 1.8.x
|
||||
gradle -v # 8.7 / JVM 1.8
|
||||
git --version
|
||||
node -v # v18 이상
|
||||
npm -v
|
||||
direnv version # (선택)
|
||||
```
|
||||
|
||||
이후 [README.md](../README.md) 의 "초기 세팅 가이드" 로 이동해 프로젝트 클론과 의존 모듈 설치를 진행하세요.
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<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>DJBank API Portal 이메일 인증</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
table, td { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin:0;padding:0;width:100%;background-color:#eef0f3;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||||
<!-- 미리보기 텍스트 (받은편지함 목록에 노출, 본문에서는 숨김) -->
|
||||
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#eef0f3;">DJBank 개발자포탈 이메일 인증번호 안내입니다.</div>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#eef0f3;">
|
||||
<tr>
|
||||
<td align="center" style="padding:30px 12px;">
|
||||
|
||||
<!-- ============ 카드 (흰 배경 · 테두리 #dfdfdf · 라운드 20) ============ -->
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;background-color:#ffffff;border:1px solid #dfdfdf;border-radius:20px;">
|
||||
|
||||
<!-- ===== Header : DJ Bank 로고 + API Portal ===== -->
|
||||
<tr>
|
||||
<td style="padding:26px 27px 0 27px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td width="20" style="width:20px;font-size:0;line-height:0;"> </td>
|
||||
<td valign="middle" style="line-height:0;">
|
||||
<img src="images/logo-djbank.png" width="144" height="40" alt="DJ Bank" style="display:block;width:144px;height:40px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
<td width="16" style="width:16px;font-size:0;line-height:0;"> </td>
|
||||
<td valign="middle" style="line-height:0;">
|
||||
<img src="images/logo-apiportal.png" width="124" height="21" alt="API Portal" style="display:block;width:124px;height:21px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- ===== 본문 : 상단 파란선(#2A69DE) + 그림자 ===== -->
|
||||
<tr>
|
||||
<td style="padding:7px 27px 0 27px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top:3px solid #2a69de;box-shadow:0px 4px 12px 0px rgba(85,95,108,0.25);">
|
||||
<tr>
|
||||
<td style="padding:26px 29px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
|
||||
<!-- 제목 (subject) : ##변수## 치환 토큰 -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:15px;line-height:25px;color:#000000;font-weight:600;padding-bottom:20px;">##subject:이메일인증##</td>
|
||||
</tr>
|
||||
|
||||
<!-- 인증번호 박스 (#ECF1FF · 라운드 10) -->
|
||||
<tr>
|
||||
<td style="padding-bottom:20px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#ecf1ff;border-radius:10px;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ecf1ff" style="background-color:#ecf1ff;border-radius:10px;padding:16px 20px;font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:14px;line-height:24px;color:#000000;font-weight:700;text-align:center;letter-spacing:2px;"><!-- 동적값: 인증번호 -->123456</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 안내 문구 -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#000000;padding-bottom:20px;">
|
||||
안녕하세요, <!-- 동적값: 회원명 -->김철수 님.<br>
|
||||
DJBank 개발자포탈 회원가입 시 입력하신 이메일을 인증해 주세요.<br>
|
||||
아래 인증번호를 회원가입 화면에 입력하시면 인증이 완료됩니다.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 경고 문구 (#FF2727) -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#ff2727;font-weight:600;">※ 본 메일을 요청하지 않으셨다면 이 메일을 무시해 주시기 바랍니다.</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- ===== Footer : 제주은행 로고 + 안내 (#F1F1ED) ===== -->
|
||||
<tr>
|
||||
<td style="padding:7px 27px 26px 27px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1f1ed" style="background-color:#f1f1ed;border-radius:4px;">
|
||||
<tr>
|
||||
<td style="padding:12px 14px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td valign="middle" width="86" style="width:86px;padding-right:16px;line-height:0;">
|
||||
<img src="images/logo-jejubank.png" width="86" height="24" alt="제주은행" style="display:block;width:86px;height:24px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
<td valign="middle" style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;color:#0d0e11;">
|
||||
<p style="margin:0;font-size:10px;line-height:20px;color:#0d0e11;">본 메일은 발신 전용으로 회신되지 않습니다.</p>
|
||||
<p style="margin:0 0 9px 0;font-size:10px;line-height:20px;color:#0d0e11;">관련 문의사항은 고객센터 (????-????) 또는 홈페이지를 이용하시기 바랍니다.</p>
|
||||
<p style="margin:0 0 9px 0;font-size:10px;line-height:16px;color:#0d0e11;">제주 제주시 1100로 3351 (노형동)<span style="color:#c2c2bc;"> | </span>고객센터 : ????-?????<span style="color:#c2c2bc;"> | </span>사업자 등록번호 : 616-81-00615</p>
|
||||
<p style="margin:0;font-size:10px;line-height:16px;color:#454545;font-weight:500;">Copyright JEJUBANK. ALL Rights Reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- ============ /카드 ============ -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
# gradle 명령을 대체합니다.
|
||||
command gradle $@
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* CycloneDX SBOM(bom.json) -> Excel(xlsx) 변환 태스크.
|
||||
*
|
||||
* gradle sbomXlsx # cyclonedxBom 실행 후 변환
|
||||
* gradle sbomXlsx -PsbomJson=path.json # 기존 bom.json 사용(cyclonedxBom 생략)
|
||||
* gradle sbomXlsx -PsbomOut=out.xlsx # 출력 경로 지정
|
||||
*
|
||||
* buildscript 블록이 이 스크립트에만 적용되므로 POI 의존성이 메인 빌드
|
||||
* classpath 나 WAR 산출물에는 포함되지 않는다.
|
||||
*
|
||||
* 시트: 요약 / WAR 기준
|
||||
* 산출 기준은 war 태스크의 classpath(= runtimeClasspath) 이므로
|
||||
* test·annotationProcessor·developmentOnly·compileOnly 의존은 모두 제외된다.
|
||||
* bom.json 은 라이선스/해시/설명/직접-전이 판별을 위한 메타 소스로만 쓴다.
|
||||
*/
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url "https://nexus.eactive.synology.me:8090/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.apache.poi:poi-ooxml:3.17'
|
||||
}
|
||||
}
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import org.apache.poi.ss.usermodel.BorderStyle
|
||||
import org.apache.poi.ss.usermodel.FillPatternType
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
||||
import org.apache.poi.ss.usermodel.IndexedColors
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment
|
||||
import org.apache.poi.ss.util.CellRangeAddress
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||
|
||||
// 엑셀 셀 문자열 상한(32767)보다 여유를 둔 절단 길이
|
||||
ext.SBOM_CELL_LIMIT = 32000
|
||||
|
||||
task sbomXlsx {
|
||||
group = 'sbom'
|
||||
description = 'CycloneDX bom.json 을 WAR 수록 기준 xlsx 로 변환한다'
|
||||
|
||||
// -PsbomJson 으로 기존 산출물을 지정하면 재생성하지 않는다
|
||||
if (!project.hasProperty('sbomJson')) {
|
||||
dependsOn 'cyclonedxBom'
|
||||
}
|
||||
|
||||
doLast {
|
||||
File src = resolveBomJson(project)
|
||||
File out = project.hasProperty('sbomOut')
|
||||
? project.file(project.property('sbomOut'))
|
||||
: new File(project.buildDir, "reports/sbom/${sbomFileName(project)}")
|
||||
out.parentFile.mkdirs()
|
||||
|
||||
def bom = new JsonSlurper().parse(src, 'UTF-8')
|
||||
def deploy = collectDeployJars(project)
|
||||
def warRows = joinWarRows(deploy.jars, indexComponents(bom))
|
||||
|
||||
def wb = new XSSFWorkbook()
|
||||
def st = createStyles(wb)
|
||||
writeSummarySheet(wb, st, bom, warRows, src, deploy.label)
|
||||
writeWarSheet(wb, st, warRows)
|
||||
|
||||
out.withOutputStream { os -> wb.write(os) }
|
||||
wb.close()
|
||||
|
||||
int unmatched = warRows.count { it.matched == 'N' }
|
||||
logger.lifecycle("SBOM xlsx 생성: ${out.absolutePath} " +
|
||||
"(배포 수록 ${warRows.size()}개, SBOM 미매칭 ${unmatched}개, 원본 ${src.name})")
|
||||
}
|
||||
}
|
||||
|
||||
/** 산출 파일명: 배포 패키지명 기준 (war 있으면 war 파일명, 없으면 project 이름[-버전]) */
|
||||
String sbomFileName(Project p) {
|
||||
def warTask = p.tasks.findByName('war')
|
||||
if (warTask != null) {
|
||||
String archive = warTask.archiveFileName.get()
|
||||
return archive.replaceAll(/\.(war|jar|ear)$/, '') + '-sbom.xlsx'
|
||||
}
|
||||
String ver = (p.version == null || p.version.toString() in ['', 'unspecified']) ? '' : "-${p.version}"
|
||||
return "${p.name}${ver}-sbom.xlsx"
|
||||
}
|
||||
|
||||
/** bom.json 위치 결정: -PsbomJson > cyclonedxBom 산출 경로 후보 */
|
||||
File resolveBomJson(Project p) {
|
||||
if (p.hasProperty('sbomJson')) {
|
||||
File f = p.file(p.property('sbomJson'))
|
||||
if (!f.exists()) {
|
||||
throw new GradleException("bom.json 없음: ${f.absolutePath}")
|
||||
}
|
||||
return f
|
||||
}
|
||||
def candidates = [
|
||||
new File(p.buildDir, 'reports/cyclonedx/bom.json'),
|
||||
new File(p.buildDir, 'reports/bom.json'),
|
||||
]
|
||||
File found = candidates.find { it.exists() }
|
||||
if (found == null) {
|
||||
throw new GradleException(
|
||||
"bom.json 을 찾지 못했다. 확인한 경로: " + candidates*.absolutePath.join(', ') +
|
||||
"\n'gradle cyclonedxBom' 실행 후 재시도하거나 -PsbomJson=<경로> 로 지정한다.")
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 배포물에 packaging 되는 jar 목록.
|
||||
* war 프로젝트는 war 태스크 classpath(= runtimeClasspath), 그 외는 runtimeClasspath 를
|
||||
* 기준으로 하므로 test/annotationProcessor/developmentOnly/compileOnly 는 자동으로 빠진다.
|
||||
*
|
||||
* @return [label: 기준 설명, jars: 행 목록]
|
||||
*/
|
||||
Map collectDeployJars(Project p) {
|
||||
def cfg = p.configurations.findByName('runtimeClasspath')
|
||||
if (cfg == null) {
|
||||
p.logger.warn("[${p.name}] runtimeClasspath 가 없어 배포 기준 시트를 비운다")
|
||||
return [label: '(없음)', jars: []]
|
||||
}
|
||||
|
||||
def warTask = p.tasks.findByName('war')
|
||||
def files
|
||||
String label
|
||||
if (warTask != null) {
|
||||
files = warTask.classpath.files
|
||||
label = 'WAR WEB-INF/lib (war 태스크 classpath)'
|
||||
} else {
|
||||
files = cfg.files
|
||||
label = 'runtimeClasspath (war 태스크 없음)'
|
||||
}
|
||||
|
||||
def coordByFile = [:]
|
||||
cfg.resolvedConfiguration.resolvedArtifacts.each { a ->
|
||||
def id = a.moduleVersion.id
|
||||
coordByFile[a.file] = [group: id.group, name: id.name, version: id.version]
|
||||
}
|
||||
def jars = files.findAll { it.name.endsWith('.jar') }.collect { f ->
|
||||
def c = coordByFile[f]
|
||||
[
|
||||
file : f.name,
|
||||
group : c?.group ?: '',
|
||||
name : c?.name ?: f.name.replaceAll(/\.jar$/, ''),
|
||||
version: c?.version ?: '',
|
||||
coord : c ? "${c.group}:${c.name}:${c.version}".toString() : '',
|
||||
]
|
||||
}.sort { it.file }
|
||||
|
||||
return [label: label, jars: jars]
|
||||
}
|
||||
|
||||
/** bom.json 컴포넌트를 'group:name:version' 키로 색인 (라이선스/해시/설명/직접-전이) */
|
||||
Map indexComponents(bom) {
|
||||
String rootRef = bom.metadata?.component?.'bom-ref'
|
||||
Set directRefs = (bom.dependencies?.find { it.ref == rootRef }?.dependsOn ?: []) as Set
|
||||
|
||||
def index = [:]
|
||||
bom.components?.each { c ->
|
||||
def hashes = [:]
|
||||
c.hashes?.each { h -> hashes[h.alg] = h.content }
|
||||
|
||||
def licenses = (c.licenses ?: []).collect { l ->
|
||||
l.license?.id ?: l.license?.name ?: l.expression ?: ''
|
||||
}.findAll { it }
|
||||
|
||||
index["${c.group ?: ''}:${c.name ?: ''}:${c.version ?: ''}".toString()] = [
|
||||
direct : directRefs.contains(c.'bom-ref') ? '직접' : '전이',
|
||||
licenses : licenses.join('; '),
|
||||
licenseList: licenses.isEmpty() ? ['(미상)'] : licenses,
|
||||
purl : c.purl ?: '',
|
||||
sha256 : hashes['SHA-256'] ?: '',
|
||||
sha1 : hashes['SHA-1'] ?: '',
|
||||
description: c.description ?: '',
|
||||
]
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/** WAR jar 목록에 SBOM 메타를 좌표로 결합 */
|
||||
List joinWarRows(List warJars, Map index) {
|
||||
def result = []
|
||||
warJars.eachWithIndex { j, i ->
|
||||
def m = j.coord ? index[j.coord] : null
|
||||
result << [
|
||||
no : i + 1,
|
||||
file : j.file,
|
||||
group : j.group,
|
||||
name : j.name,
|
||||
version : j.version,
|
||||
direct : m?.direct ?: '',
|
||||
licenses : m?.licenses ?: '',
|
||||
licenseList: m?.licenseList ?: ['(미상)'],
|
||||
purl : m?.purl ?: '',
|
||||
sha256 : m?.sha256 ?: '',
|
||||
sha1 : m?.sha1 ?: '',
|
||||
matched : (m != null) ? 'Y' : 'N',
|
||||
description: m?.description ?: '',
|
||||
]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Map createStyles(wb) {
|
||||
def headFont = wb.createFont()
|
||||
headFont.setBold(true)
|
||||
headFont.setColor(IndexedColors.WHITE.getIndex())
|
||||
|
||||
def head = wb.createCellStyle()
|
||||
head.setFont(headFont)
|
||||
head.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex())
|
||||
head.setFillPattern(FillPatternType.SOLID_FOREGROUND)
|
||||
head.setAlignment(HorizontalAlignment.CENTER)
|
||||
head.setVerticalAlignment(VerticalAlignment.CENTER)
|
||||
head.setBorderBottom(BorderStyle.THIN)
|
||||
|
||||
def body = wb.createCellStyle()
|
||||
body.setVerticalAlignment(VerticalAlignment.TOP)
|
||||
|
||||
def wrap = wb.createCellStyle()
|
||||
wrap.setVerticalAlignment(VerticalAlignment.TOP)
|
||||
wrap.setWrapText(true)
|
||||
|
||||
def labelFont = wb.createFont()
|
||||
labelFont.setBold(true)
|
||||
def label = wb.createCellStyle()
|
||||
label.setFont(labelFont)
|
||||
|
||||
return [head: head, body: body, wrap: wrap, label: label]
|
||||
}
|
||||
|
||||
/** 헤더 행 생성 + 폭 지정 + 틀고정 */
|
||||
def writeHeader(sheet, style, List<String> headers, List<Integer> widths) {
|
||||
def row = sheet.createRow(0)
|
||||
row.setHeightInPoints(20f)
|
||||
headers.eachWithIndex { h, i ->
|
||||
def cell = row.createCell(i)
|
||||
cell.setCellValue(h)
|
||||
cell.setCellStyle(style)
|
||||
sheet.setColumnWidth(i, widths[i] * 256)
|
||||
}
|
||||
sheet.createFreezePane(0, 1)
|
||||
}
|
||||
|
||||
def cellOf(row, int idx, value, style) {
|
||||
def cell = row.createCell(idx)
|
||||
String s = (value == null) ? '' : value.toString()
|
||||
if (s.length() > SBOM_CELL_LIMIT) {
|
||||
s = s.substring(0, SBOM_CELL_LIMIT) + '…(생략)'
|
||||
}
|
||||
cell.setCellValue(s)
|
||||
cell.setCellStyle(style)
|
||||
return cell
|
||||
}
|
||||
|
||||
def writeSummarySheet(wb, st, bom, List warRows, File src, String basisLabel) {
|
||||
def sheet = wb.createSheet('요약')
|
||||
def comp = bom.metadata?.component ?: [:]
|
||||
def tool = bom.metadata?.tools?.components?.getAt(0)
|
||||
Set licenseKinds = warRows.collectMany { it.licenseList } as Set
|
||||
|
||||
def items = [
|
||||
['대상 프로젝트', "${comp.group ?: ''}:${comp.name ?: ''}:${comp.version ?: ''}"],
|
||||
['산출 기준', "${basisLabel} — test/annotationProcessor/compileOnly 제외"],
|
||||
['BOM 포맷', "${bom.bomFormat ?: ''} ${bom.specVersion ?: ''}"],
|
||||
['serialNumber', bom.serialNumber ?: ''],
|
||||
['생성 시각', bom.metadata?.timestamp ?: ''],
|
||||
['생성 도구', tool ? "${tool.name} ${tool.version}" : ''],
|
||||
['원본 파일', src.absolutePath],
|
||||
['배포 수록 jar', warRows.size()],
|
||||
[' └ 직접 의존', warRows.count { it.direct == '직접' }],
|
||||
[' └ 전이 의존', warRows.count { it.direct == '전이' }],
|
||||
[' └ SBOM 미매칭', warRows.count { it.matched == 'N' }],
|
||||
['라이선스 종류', licenseKinds.size()],
|
||||
['라이선스 미상', warRows.count { it.licenses.isEmpty() }],
|
||||
]
|
||||
|
||||
writeHeader(sheet, st.head, ['항목', '값'], [30, 90])
|
||||
items.eachWithIndex { item, i ->
|
||||
def row = sheet.createRow(i + 1)
|
||||
cellOf(row, 0, item[0], st.label)
|
||||
cellOf(row, 1, item[1], st.body)
|
||||
}
|
||||
|
||||
// 라이선스 분포 (요약 하단)
|
||||
def byLicense = [:].withDefault { 0 }
|
||||
warRows.each { r -> r.licenseList.each { lic -> byLicense[lic] = byLicense[lic] + 1 } }
|
||||
def sorted = byLicense.entrySet().sort { a, b -> (b.value <=> a.value) ?: (a.key <=> b.key) }
|
||||
|
||||
int base = items.size() + 2
|
||||
def hdr = sheet.createRow(base)
|
||||
cellOf(hdr, 0, '라이선스', st.head)
|
||||
cellOf(hdr, 1, 'jar 수', st.head)
|
||||
sorted.eachWithIndex { e, i ->
|
||||
def row = sheet.createRow(base + 1 + i)
|
||||
cellOf(row, 0, e.key, st.body)
|
||||
cellOf(row, 1, e.value, st.body)
|
||||
}
|
||||
}
|
||||
|
||||
/** 실제 배포물(WAR WEB-INF/lib) 기준 시트 */
|
||||
def writeWarSheet(wb, st, List warRows) {
|
||||
def sheet = wb.createSheet('WAR 기준')
|
||||
def headers = ['No', 'jar 파일명', 'Group', 'Name', 'Version', '구분',
|
||||
'License', 'purl', 'SHA-256', 'SHA-1', 'SBOM매칭', 'Description']
|
||||
def widths = [6, 46, 32, 34, 16, 7, 30, 60, 40, 30, 10, 60]
|
||||
writeHeader(sheet, st.head, headers, widths)
|
||||
|
||||
warRows.eachWithIndex { r, i ->
|
||||
def row = sheet.createRow(i + 1)
|
||||
cellOf(row, 0, r.no, st.body)
|
||||
cellOf(row, 1, r.file, st.body)
|
||||
cellOf(row, 2, r.group, st.body)
|
||||
cellOf(row, 3, r.name, st.body)
|
||||
cellOf(row, 4, r.version, st.body)
|
||||
cellOf(row, 5, r.direct, st.body)
|
||||
cellOf(row, 6, r.licenses, st.body)
|
||||
cellOf(row, 7, r.purl, st.body)
|
||||
cellOf(row, 8, r.sha256, st.body)
|
||||
cellOf(row, 9, r.sha1, st.body)
|
||||
cellOf(row, 10, r.matched, st.body)
|
||||
cellOf(row, 11, r.description, st.wrap)
|
||||
}
|
||||
if (!warRows.isEmpty()) {
|
||||
sheet.setAutoFilter(new CellRangeAddress(0, warRows.size(), 0, headers.size() - 1))
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
pipeline {
|
||||
// 25.10.01 : 기본 브랜치 staging -> jenkins_with_weblogic 로 변경
|
||||
|
||||
agent { label 'apipod01' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/5 * * * *')
|
||||
}
|
||||
|
||||
environment {
|
||||
// Java 환경 설정
|
||||
JAVA_HOME = '/App/jenkins/jdks/jdk1.8.0_441'
|
||||
PATH = "${JAVA_HOME}/bin:${env.PATH}"
|
||||
|
||||
// Gradle 환경 설정
|
||||
GRADLE_HOME = '/App/jenkins/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/App/jenkins/gradle-home'
|
||||
// GRADLE_OPTS = '--offline'
|
||||
|
||||
// Git 저장소 정보
|
||||
GIT_SERVER = '192.168.240.178:18081'
|
||||
GIT_CREDENTIALS_ID = 'git-ssh-credentials'
|
||||
|
||||
// 프로젝트 디렉토리 구조
|
||||
PROJECT_HOME = '/Data/jenkins/eapim'
|
||||
|
||||
BRANCH = 'jenkins_with_weblogic'
|
||||
}
|
||||
|
||||
|
||||
stages {
|
||||
stage('Git pull') {
|
||||
steps {
|
||||
echo "[portal] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/eapim-portal") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/eapim-portal", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
|
||||
echo "[online-core-jpa] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/eapim-online/elink-online-core-jpa") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/elink-online-core-jpa", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
|
||||
echo "[portal-common] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/elink-portal-common") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/elink-portal-common", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
echo "[kjb-safedb] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/kjb-safedb") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/kjb-safedb", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Build') {
|
||||
steps {
|
||||
tool name: 'Oracle-JDK-1.8', type: 'hudson.model.JDK'
|
||||
// tool name: 'Gradle-8.7', type: 'hudson.tasks.Gradle'
|
||||
|
||||
// echo "[online-core-jpa] Build project"
|
||||
// dir("${PROJECT_HOME}/elink-online-multi/elink-online-core-jpa") {
|
||||
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
// }
|
||||
|
||||
// echo "[portal-common] Build project"
|
||||
// dir("${PROJECT_HOME}/elink-portal-common") {
|
||||
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
// }
|
||||
|
||||
echo "[portal] Build project"
|
||||
dir("${PROJECT_HOME}/eapim-portal") {
|
||||
sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Archive WAR') {
|
||||
steps {
|
||||
echo "[portal] Archive WAR file"
|
||||
dir("${PROJECT_HOME}/eapim-portal/build/libs") {
|
||||
archiveArtifacts artifacts: '*.war', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# 광주은행 eAPIM
|
||||
|
||||
## 커스텀 항목
|
||||
### 포탈 Property (EMS 프로퍼티)
|
||||
- `disable_features.user_email_verify' == true
|
||||
이메일 인증 기능 비활성화(회원 가입시 이메일 인증은 한걸로 처리)
|
||||
|
||||
### 디비 접속 방법 Direct -> JNDI 로 변경
|
||||
- 은행 내부 환경에선 모두 JNDI 사용하기 때문에 서버환경에서 동작하는 profile 내 application.yaml 에서 디비 접속 정보 삭제
|
||||
|
||||
## 서버 운영 중 긴급 로깅이 필요할 경우 JVM 파라미터 추가 하여 재기동
|
||||
- DEBUG 레벨 root 로그 / debug.log 로 파일 작성
|
||||
```
|
||||
-Dlogging.config=classpath:logback-debug.xml
|
||||
```
|
||||
|
||||
## 참고자료
|
||||
### 개발용 파라미터
|
||||
```
|
||||
-Dlogging.config=classpath:logback-rinjae.xml
|
||||
-Dinst.Name=devSvr98
|
||||
-Duser.language=en
|
||||
-Duser.country=US
|
||||
-Dkjb_safedb.mode=fake
|
||||
```
|
||||
@@ -1,113 +0,0 @@
|
||||
# 사업자등록번호 중복 체크 구현 작업 계획서
|
||||
|
||||
## 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] 빌드 검증 완료
|
||||
@@ -1,165 +0,0 @@
|
||||
# 네비게이션 바 디자인 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`로 설정하여 비활성화
|
||||
|
||||
---
|
||||
|
||||
## 문의
|
||||
|
||||
기능 관련 문의는 개발팀에 연락해주세요.
|
||||
@@ -1,105 +0,0 @@
|
||||
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
|
||||
@@ -1,44 +0,0 @@
|
||||
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
|
||||
@@ -1,122 +0,0 @@
|
||||
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
|
||||
@@ -1,44 +0,0 @@
|
||||
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
|
||||
|
Before Width: | Height: | Size: 6.7 MiB |
|
Before Width: | Height: | Size: 574 KiB |
@@ -1,33 +1,30 @@
|
||||
{
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
"node-addon-api": "^7.0.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
@@ -37,25 +34,25 @@
|
||||
"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"
|
||||
"@parcel/watcher-android-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-x64": "2.5.6",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.6",
|
||||
"@parcel/watcher-win32-arm64": "2.5.6",
|
||||
"@parcel/watcher-win32-ia32": "2.5.6",
|
||||
"@parcel/watcher-win32-x64": "2.5.6"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -74,9 +71,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -95,9 +92,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -116,9 +113,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -137,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -158,9 +155,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -179,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -200,9 +197,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -221,9 +218,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -242,9 +239,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -263,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -284,9 +281,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
|
||||
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -305,9 +302,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -325,82 +322,37 @@
|
||||
"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==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 20.19.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==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"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==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -429,32 +381,6 @@
|
||||
"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",
|
||||
@@ -464,57 +390,27 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"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==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
@@ -522,21 +418,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"version": "1.100.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
|
||||
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
@@ -551,284 +447,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,5 @@
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.57.0"
|
||||
}
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 메뉴 관리 개발 가이드
|
||||
|
||||
포탈 GNB/마이페이지 메뉴는 `menu.yml` → DB(PTL_MENU_*) → 캐시 → 템플릿 렌더 구조로 동작하며,
|
||||
노출/배치 관리는 eapim-admin **포탈메뉴관리**(파트너포탈 > 포탈관리 > 메뉴 관리)에서 수행한다.
|
||||
|
||||
## 구성 요소
|
||||
|
||||
| 구성 | 위치 | 역할 |
|
||||
|---|---|---|
|
||||
| `menu.yml` | `src/main/resources/menu.yml` | 기본 메뉴 정의 (id/노출명/path/권한/기본 배치) |
|
||||
| `roles.yml` | `src/main/resources/roles.yml` | 역할 정의 (`portal.portal_security` 이동분) |
|
||||
| 엔티티/공유 서비스 | `elink-portal-common` `com.eactive.apim.portal.menu.*` | PTL_MENU_ITEM·PTL_MENU_PLACEMENT·PTL_ROLE(+AUTHORITY), `PortalMenuDataService` |
|
||||
| 시더 | `djb/menu/MenuSeeder.java` | 부팅 시 yml→DB 적재 (ApplicationReadyEvent) |
|
||||
| 캐시 | `djb/menu/MenuService.java` | role 비의존 트리 스냅샷, TTL 1시간(PTL_PROPERTY) |
|
||||
| 렌더 | `djb/menu/MenuModelAdvice.java` → 모델 `menuView` | 요청별 노출(EXPOSE_ROLES) 필터 |
|
||||
| 접근 제어 | `djb/menu/MenuAccessInterceptor.java` | ACCESS_ROLES 서버측 집행 (경로 정확 일치) |
|
||||
| 내부 API | `djb/menu/MenuInternalController.java` | `POST /internal/menu/reload` (admin 캐시 리로드 수신) |
|
||||
|
||||
메뉴를 소비하는 템플릿: `fragment/djbank/header_container.html`(데스크톱 nav·마이페이지 드롭다운·모바일 drawer),
|
||||
`fragment/djbank/service_sidebar.html`. 모두 `${menuView}` 를 반복 렌더하므로 **메뉴 추가 시 템플릿 수정 불필요**.
|
||||
|
||||
## menu.yml 스키마
|
||||
|
||||
```yaml
|
||||
portal-menu:
|
||||
items:
|
||||
- id: support # kebab-case 필수 (^[a-z0-9-]+$). 변경 금지(변경=신규 항목)
|
||||
name: "고객지원"
|
||||
group: true # 상위 그룹. path 생략 시 클릭 없음(자식 있어야 노출)
|
||||
section: GNB # GNB(기본) | MYPAGE. 자식은 부모 섹션 상속
|
||||
expose-roles: [] # 생략=전체(익명 포함), AUTHENTICATED=로그인자, 그 외 역할코드 any-of
|
||||
children:
|
||||
- { id: support-faq, name: "FAQ", path: /faq_list }
|
||||
- { id: my-page-webhook, name: "Webhook 관리", path: /webhook, icon: fa-bell,
|
||||
expose-roles: [ROLE_WEBHOOK], access-roles: [ROLE_WEBHOOK] }
|
||||
```
|
||||
|
||||
- `expose-roles` = 메뉴 **노출** 조건, `access-roles` = URL **접근** 조건(인터셉터 차단, redirect).
|
||||
- `icon` 은 마이페이지 드롭다운 전용(FontAwesome 클래스).
|
||||
- 정렬은 yml 나열 순서(기본 배치 sort = index×10).
|
||||
|
||||
## 시딩 규칙 (MenuSeeder)
|
||||
|
||||
1. **항목**: id 기준 upsert. yml 값이 바뀌면 DFLT_*(기본값 스냅샷)를 갱신하고,
|
||||
**관리자가 수정하지 않은 필드(현재값==구 기본값)만** 새 기본값을 따라간다.
|
||||
구조 필드(`group`/`section`/`icon`/`new-window`)는 항상 yml 이 이긴다.
|
||||
2. **배치**: `PTL_MENU_PLACEMENT` 가 **비어있을 때만** 기본 배치로 최초 시딩.
|
||||
이후 배치는 admin 이 소유한다 — 재배포/재기동에도 보존됨.
|
||||
3. yml 에서 항목을 제거해도 DB 는 삭제하지 않고 경고 로그만 남긴다(수동 정리).
|
||||
4. 부팅 시딩 주체는 인증 사용자가 없으므로 `CREATED_BY=SYSTEM`.
|
||||
|
||||
## 캐시와 리로드
|
||||
|
||||
- 스냅샷 TTL: PTL_PROPERTY `Portal / menu.cache.ttl-seconds` (기본 3600초).
|
||||
- 즉시 반영: `curl -X POST http://127.0.0.1:39130/internal/menu/reload`
|
||||
(admin 포탈메뉴관리의 [캐시 Reload] 버튼이 동일 호출 수행).
|
||||
- 내부 API 가드: `Portal / menu.internal.allow-ips` 허용 IP 목록(기본 loopback)
|
||||
+ X-Forwarded-For 동반 요청 거부. CSRF 면제(`/internal/menu/**`).
|
||||
- admin 측 호출 URL: `Portal / portal.internal.menu-reload-url`.
|
||||
|
||||
## 새 메뉴 추가 절차
|
||||
|
||||
**기본 메뉴(코드 배포와 함께)**
|
||||
1. 페이지/라우트 준비 (`portal.pages` 또는 `@GetMapping` — 기존 방식 그대로)
|
||||
2. `menu.yml` 에 항목 추가 (필요 시 breadcrumb 용 `page.home` 트리도 갱신 — 별도 체계 유지)
|
||||
3. 재기동 → 시딩 로그 확인 → 헤더/드로어 노출 확인
|
||||
4. 이미 운영 중인 DB 라면 배치는 자동 추가되지 않음(배치 시딩은 최초 1회) —
|
||||
admin 화면에서 미배치 → 원하는 위치로 드래그 후 저장
|
||||
|
||||
**운영자 임시 메뉴(외부 링크 등)**: admin 포탈메뉴관리 [메뉴 추가] → 미배치 생성 → 드래그 배치 → 저장 → 캐시 Reload.
|
||||
커스텀 항목은 미배치 시 삭제된다.
|
||||
|
||||
## 로컬 개발 주의
|
||||
|
||||
- `gradle bootRun` 으로 시딩까지 확인하려면 damo-manager 가 classpath 에 필요:
|
||||
`JAVA_TOOL_OPTIONS="-Xbootclasspath/a:<...>/apache-tomcat-9.0.115-djb/lib/damo-manager.jar"`
|
||||
(미지정 시 감사 컬럼 암호화 컨버터에서 NoClassDefFoundError).
|
||||
- 템플릿/메뉴 반영 확인은 서버 재시작 후 curl 로.
|
||||
- elink-portal-common 수정 후 Q클래스 duplicate 컴파일 오류 시 각 모듈 `build/generated` 삭제 후 재컴파일.
|
||||
|
||||
## 역할(roles.yml) 변경
|
||||
|
||||
- 로그인 권한 확장은 `PortalRolesProperties`(yml 바인딩)를 직접 사용 — DB 미러(PTL_ROLE*)는
|
||||
admin 권한 선택 체크박스 소스 전용.
|
||||
- 역할 추가 시 `roles.yml` 의 `authority-names` 에 한글 라벨을 함께 등록해야 admin 화면에 표기된다.
|
||||
@@ -0,0 +1,112 @@
|
||||
# DJB 커스텀 소스 납품본 수집
|
||||
|
||||
발주사 납품용으로, 기준 시점 이후 **신규로 추가된 커스텀 소스 파일**만 모아
|
||||
디렉터리 구조를 유지한 채 한곳에 복사하고 `MANIFEST.csv` / `SUMMARY.md` / zip 을 만든다.
|
||||
|
||||
## 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|---|---|
|
||||
| `export-custom.sh` | macOS / Linux 실행 스크립트 |
|
||||
| `export-custom.ps1` | Windows PowerShell 실행 스크립트 (실제 로직) |
|
||||
| `export-custom.bat` | Windows cmd 래퍼 — 인자를 `.ps1` 로 그대로 전달 |
|
||||
| `modules.conf` | 대상 모듈 / 소스 경로 / 수집 확장자 |
|
||||
| `export-filter.rules` | 포함·제외 규칙 (`.gitignore` 유사 문법) |
|
||||
| `filelist.txt` | 마지막으로 뽑은 수집 대상 목록 (검토용, 재생성 가능) |
|
||||
| `filelist-no-djb-marker.txt` | 그중 `djb` / `custom` 마커가 **없는** 파일 — 업그레이드 유입분 검토 대상 |
|
||||
|
||||
## 수집 방식
|
||||
|
||||
1. 모듈별로 **컷오프 날짜 직전의 마지막 커밋**을 base 로 잡는다.
|
||||
`git rev-list -1 --before=<CUTOFF> HEAD`
|
||||
컷오프 이전 커밋이 아예 없으면 저장소 전체를 신규로 본다.
|
||||
2. `git diff --name-only --diff-filter=A -M <base> HEAD -- <소스경로>` 로
|
||||
**신규 추가(A)** 파일만 뽑는다. `-M` 이라 단순 rename 은 신규로 잡히지 않는다.
|
||||
3. `modules.conf` 의 확장자 allowlist 로 거른다. (확장자 없는 파일은 제외)
|
||||
4. `export-filter.rules` 로 포함·제외를 최종 결정한다.
|
||||
5. 남은 파일을 `<out>/src/<모듈>/<원래 경로>` 로 복사한다.
|
||||
|
||||
## 사용법
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
./export-custom.sh --dry-run > filelist.txt # 목록만 뽑기 (로그는 stderr)
|
||||
./export-custom.sh --show-excluded # 규칙에 걸려 빠진 목록 + 걸린 규칙
|
||||
./export-custom.sh # 실제 수집
|
||||
./export-custom.sh --zip # 수집 + zip
|
||||
./export-custom.sh -c 2026-05-01 -m eapim-portal
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
.\export-custom.ps1 -DryRun > filelist.txt
|
||||
.\export-custom.ps1 -ShowExcluded
|
||||
.\export-custom.ps1 -Zip
|
||||
.\export-custom.ps1 -Cutoff 2026-05-01 -Module eapim-portal,eapim-admin
|
||||
|
||||
# cmd 에서
|
||||
export-custom.bat -DryRun > filelist.txt
|
||||
```
|
||||
|
||||
### 주요 옵션
|
||||
|
||||
| sh | ps1 | 설명 |
|
||||
|---|---|---|
|
||||
| `-c, --cutoff` | `-Cutoff` | 기준 날짜 (기본 `2026-05-01`) |
|
||||
| `-r, --root` | `-Root` | 저장소 루트. 미지정 시 스크립트 위치 기준 자동 탐지 |
|
||||
| `-o, --out` | `-Out` | 출력 디렉터리 (기본 `<root>/build/djb-custom-export`) |
|
||||
| `-m, --module` | `-Module` | 특정 모듈만 |
|
||||
| `--zip` | `-Zip` | zip 생성 |
|
||||
| `--include-untracked` | `-IncludeUntracked` | 미커밋 신규 파일도 포함 |
|
||||
| `--dry-run` | `-DryRun` | 복사 없이 목록만 |
|
||||
| `--show-excluded` | `-ShowExcluded` | 규칙에 걸려 빠진 목록 |
|
||||
|
||||
## `export-filter.rules` 문법
|
||||
|
||||
`.gitignore` 와 비슷하되 더 단순하다.
|
||||
|
||||
- 한 줄에 패턴 하나. `#` 주석, 빈 줄 무시
|
||||
- 기본은 **모두 포함**. 패턴에 걸리면 제외
|
||||
- `!` 로 시작하면 예외(다시 포함)
|
||||
- **뒤에 오는 규칙이 앞 규칙을 덮는다** — 순서가 중요하다
|
||||
- 패턴에 `/` 가 있으면 `<모듈>/<경로>` 전체와 매칭, 없으면 파일명만 매칭
|
||||
- `/` 로 끝나면 그 디렉터리 하위 전체
|
||||
- `*` 는 `/` 도 포함해 매칭한다 (단순 glob, `**` 없음)
|
||||
|
||||
매칭 대상 경로 예:
|
||||
|
||||
```
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
|
||||
```
|
||||
|
||||
규칙 파일은 세 구역으로 나뉜다.
|
||||
|
||||
1. **업그레이드 유입분 제외** — `filelist-no-djb-marker.txt` 를 검토하며 채운다
|
||||
2. **`!*djb*` 예외** — djb 마커가 붙은 파일은 커스텀이므로 되살린다
|
||||
3. **무조건 제외** — 빌드 산출물·서드파티. 2)보다 뒤에 있어 최종 승리한다
|
||||
|
||||
## 신규지만 커스텀이 아닌 파일 걸러내기
|
||||
|
||||
신규 파일이라도 제품 업그레이드로 딸려 온 것일 수 있다. 판별 순서:
|
||||
|
||||
```bash
|
||||
# 1) 전체 목록
|
||||
./export-custom.sh --dry-run > filelist.txt
|
||||
|
||||
# 2) djb / custom 마커가 없는 것 = 검토 대상
|
||||
grep -vi djb filelist.txt | grep -v '/custom/' > filelist-no-djb-marker.txt
|
||||
|
||||
# 3) 검토 후 업그레이드 유입분을 export-filter.rules 의 1) 구역에 추가
|
||||
|
||||
# 4) 반영 확인
|
||||
./export-custom.sh --show-excluded
|
||||
```
|
||||
|
||||
## 주의
|
||||
|
||||
- 컷오프 이전 커밋이 없으면 base 가 **빈 트리**가 되어 저장소 전체가 신규로 잡힌다.
|
||||
`SUMMARY.md` 의 base 컬럼에 `(컷오프 이전 커밋 없음 …)` 으로 표시된다.
|
||||
- 모듈에 컷오프 직전 커밋이 드문드문 있으면 base 가 컷오프보다 훨씬 이전으로 잡힐 수 있다.
|
||||
실행 로그의 `base:` 날짜를 항상 확인할 것.
|
||||
- 추가된 뒤 삭제·이동된 파일은 현재 트리에 없으므로 건너뛰고, 건수를 `SUMMARY.md` 에 남긴다.
|
||||
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM DJB 커스텀 소스 납품본 수집 (Windows cmd 래퍼)
|
||||
REM
|
||||
REM 실제 로직은 export-custom.ps1 에 있다. 이 배치는 PowerShell 을 띄우고
|
||||
REM 전달받은 인자를 그대로 넘기기만 한다.
|
||||
REM
|
||||
REM 사용 예:
|
||||
REM export-custom.bat
|
||||
REM export-custom.bat -Cutoff 2026-05-01 -Zip
|
||||
REM export-custom.bat -DryRun > filelist.txt
|
||||
REM export-custom.bat -ShowExcluded
|
||||
REM export-custom.bat -Module eapim-portal,eapim-admin
|
||||
REM ---------------------------------------------------------------------------
|
||||
setlocal
|
||||
|
||||
REM 한글 경로/출력이 깨지지 않도록 코드페이지를 UTF-8 로 전환
|
||||
chcp 65001 >nul
|
||||
|
||||
where powershell >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo [오류] PowerShell 을 찾을 수 없습니다. 1>&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0export-custom.ps1" %*
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,278 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
DJB 커스텀 소스 납품본 수집 스크립트 (Windows / PowerShell)
|
||||
|
||||
.DESCRIPTION
|
||||
기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
|
||||
모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
|
||||
MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
|
||||
|
||||
.EXAMPLE
|
||||
.\export-custom.ps1
|
||||
.\export-custom.ps1 -Cutoff 2026-05-01 -Zip
|
||||
.\export-custom.ps1 -DryRun
|
||||
.\export-custom.ps1 -Module eapim-portal,eapim-admin
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Cutoff = '2026-05-01',
|
||||
[string] $Root,
|
||||
[string] $Out,
|
||||
[string[]] $Module,
|
||||
[switch] $Zip,
|
||||
[switch] $IncludeUntracked,
|
||||
[switch] $DryRun,
|
||||
[switch] $ShowExcluded
|
||||
)
|
||||
|
||||
if ($ShowExcluded) { $DryRun = $true }
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# git 출력의 한글이 깨지지 않도록 콘솔 인코딩을 UTF-8로 고정한다.
|
||||
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
|
||||
$OutputEncoding = [Text.Encoding]::UTF8
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ConfFile = Join-Path $ScriptDir 'modules.conf'
|
||||
$RulesFile = Join-Path $ScriptDir 'export-filter.rules'
|
||||
|
||||
# 저장소 루트: 스크립트는 <root>\eapim-portal\script\djb-custom-export\ 에 있다.
|
||||
if (-not $Root) { $Root = (Resolve-Path (Join-Path $ScriptDir '..\..\..')).Path }
|
||||
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
||||
throw "저장소 루트를 찾을 수 없다: $Root"
|
||||
}
|
||||
if (-not $Out) { $Out = Join-Path $Root 'build\djb-custom-export' }
|
||||
if (-not (Test-Path -LiteralPath $ConfFile)) { throw "설정 파일 없음: $ConfFile" }
|
||||
|
||||
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
|
||||
$EmptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
||||
|
||||
# ---- 설정 로드 ------------------------------------------------------------
|
||||
$Modules = @()
|
||||
$AllowExt = @()
|
||||
|
||||
foreach ($line in (Get-Content -LiteralPath $ConfFile -Encoding UTF8)) {
|
||||
$t = $line.Trim()
|
||||
if ($t -eq '' -or $t.StartsWith('#')) { continue }
|
||||
$idx = $t.IndexOf('|')
|
||||
if ($idx -lt 0) { continue }
|
||||
$key = $t.Substring(0, $idx)
|
||||
$rest = $t.Substring($idx + 1)
|
||||
|
||||
switch ($key) {
|
||||
'MODULE' {
|
||||
$i2 = $rest.IndexOf('|')
|
||||
if ($i2 -lt 0) { continue }
|
||||
$Modules += [pscustomobject]@{
|
||||
Name = $rest.Substring(0, $i2)
|
||||
Paths = $rest.Substring($i2 + 1).Split(' ') | Where-Object { $_ -ne '' }
|
||||
}
|
||||
}
|
||||
'EXT' { $AllowExt = $rest.Split(',') | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ -ne '' } }
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 포함/제외 규칙 로드 (.gitignore 유사) --------------------------------
|
||||
$Rules = @()
|
||||
if (Test-Path -LiteralPath $RulesFile) {
|
||||
foreach ($line in (Get-Content -LiteralPath $RulesFile -Encoding UTF8)) {
|
||||
$t = $line.Trim()
|
||||
if ($t -eq '' -or $t.StartsWith('#')) { continue }
|
||||
$Rules += $t
|
||||
}
|
||||
}
|
||||
|
||||
# 마지막으로 매칭된 규칙이 이긴다. 기본 포함, '!' 는 예외(다시 포함).
|
||||
# 반환: @{ Keep = $true/$false; Rule = '<제외시킨 규칙>' }
|
||||
function Test-RuleKeep([string]$RelPath) {
|
||||
$leaf = Split-Path $RelPath -Leaf
|
||||
$keep = $true
|
||||
$matched = ''
|
||||
foreach ($raw in $Rules) {
|
||||
$rule = $raw
|
||||
$neg = $false
|
||||
if ($rule.StartsWith('!')) { $neg = $true; $rule = $rule.Substring(1) }
|
||||
if ($rule.EndsWith('/')) { $rule = $rule + '*' } # 디렉터리 규칙
|
||||
$target = if ($rule.Contains('/')) { $RelPath } else { $leaf }
|
||||
|
||||
if ($target -like $rule) {
|
||||
if ($neg) { $keep = $true; $matched = '' }
|
||||
else { $keep = $false; $matched = $rule }
|
||||
}
|
||||
}
|
||||
return @{ Keep = $keep; Rule = $matched }
|
||||
}
|
||||
|
||||
function Test-AllowedExt([string]$RelPath) {
|
||||
$leaf = Split-Path $RelPath -Leaf
|
||||
if ($leaf -notmatch '\.') { return $false } # 확장자 없는 파일 제외
|
||||
$ext = $leaf.Substring($leaf.LastIndexOf('.') + 1).ToLower()
|
||||
return $AllowExt -contains $ext
|
||||
}
|
||||
|
||||
function Invoke-Git([string]$Dir, [string[]]$GitArgs) {
|
||||
$all = @('-C', $Dir, '-c', 'core.quotepath=false') + $GitArgs
|
||||
$res = & git @all 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { return @() }
|
||||
return @($res)
|
||||
}
|
||||
|
||||
# ---- 준비 -----------------------------------------------------------------
|
||||
$Manifest = Join-Path $Out 'MANIFEST.csv'
|
||||
$Summary = Join-Path $Out 'SUMMARY.md'
|
||||
|
||||
if (-not $DryRun) {
|
||||
if (Test-Path -LiteralPath $Out) { Remove-Item -LiteralPath $Out -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path (Join-Path $Out 'src') -Force | Out-Null
|
||||
# Excel 한글용 UTF-8 BOM
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[IO.File]::WriteAllText($Manifest, "module,path,ext,added_commit,added_date,author`r`n", $utf8Bom)
|
||||
}
|
||||
|
||||
Write-Host "저장소 루트 : $Root"
|
||||
Write-Host "기준 날짜 : $Cutoff (이 날짜 직전 마지막 커밋이 base)"
|
||||
Write-Host "출력 경로 : $Out"
|
||||
if ($DryRun) { Write-Host '모드 : DRY-RUN (복사 안 함)' }
|
||||
Write-Host ''
|
||||
|
||||
$Rows = New-Object System.Collections.Generic.List[string]
|
||||
$SummaryRs = New-Object System.Collections.Generic.List[object]
|
||||
$Total = 0
|
||||
$Missing = 0
|
||||
$Excluded = 0
|
||||
|
||||
# ---- 모듈 순회 ------------------------------------------------------------
|
||||
foreach ($m in $Modules) {
|
||||
if ($Module -and ($Module -notcontains $m.Name)) { continue }
|
||||
|
||||
$modDir = Join-Path $Root $m.Name
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $modDir '.git'))) {
|
||||
Write-Host "[건너뜀] $($m.Name) — git 저장소 아님 ($modDir)"
|
||||
continue
|
||||
}
|
||||
|
||||
# base 커밋 결정
|
||||
$base = (Invoke-Git $modDir @('rev-list', '-1', "--before=$Cutoff", 'HEAD') | Select-Object -First 1)
|
||||
if ([string]::IsNullOrWhiteSpace($base)) {
|
||||
$base = $EmptyTree
|
||||
$baseDesc = '(컷오프 이전 커밋 없음 → 전체를 신규로 간주)'
|
||||
} else {
|
||||
$d = (Invoke-Git $modDir @('log', '-1', '--format=%h %ad %s', '--date=short', $base) | Select-Object -First 1)
|
||||
$baseDesc = if ($d.Length -gt 80) { $d.Substring(0, 80) } else { $d }
|
||||
}
|
||||
|
||||
# 실제 존재하는 소스 경로만 pathspec 으로 사용
|
||||
$spec = @()
|
||||
foreach ($p in $m.Paths) {
|
||||
if (Test-Path -LiteralPath (Join-Path $modDir ($p -replace '/', '\')) -PathType Container) { $spec += $p }
|
||||
}
|
||||
if ($spec.Count -eq 0) {
|
||||
Write-Host "[건너뜀] $($m.Name) — 설정된 소스 경로가 존재하지 않음"
|
||||
continue
|
||||
}
|
||||
|
||||
# 신규(Added) 파일만. -M 으로 rename 은 신규에서 제외한다.
|
||||
$files = @(Invoke-Git $modDir (@('diff', '--name-only', '--diff-filter=A', '-M', $base, 'HEAD', '--') + $spec))
|
||||
|
||||
if ($IncludeUntracked) {
|
||||
$files += @(Invoke-Git $modDir (@('ls-files', '--others', '--exclude-standard', '--') + $spec))
|
||||
}
|
||||
|
||||
$files = $files | Where-Object { $_ -ne '' } | Sort-Object -Unique
|
||||
|
||||
$count = 0
|
||||
$miss = 0
|
||||
foreach ($f in $files) {
|
||||
if (-not (Test-AllowedExt $f)) { continue }
|
||||
|
||||
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
|
||||
$rk = Test-RuleKeep "$($m.Name)/$f"
|
||||
if (-not $rk.Keep) {
|
||||
$Excluded++
|
||||
if ($ShowExcluded) { Write-Output "$($m.Name)/$f`t# $($rk.Rule)" }
|
||||
continue
|
||||
}
|
||||
if ($ShowExcluded) { continue }
|
||||
|
||||
$src = Join-Path $modDir ($f -replace '/', '\')
|
||||
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
|
||||
Write-Warning " [없음] $($m.Name)/$f"
|
||||
$miss++
|
||||
continue
|
||||
}
|
||||
|
||||
$count++
|
||||
# 목록은 Write-Output(성공 스트림) 으로 — 리다이렉트하면 목록만 파일로 떨어진다.
|
||||
if ($DryRun) { Write-Output "$($m.Name)/$f"; continue }
|
||||
|
||||
$dst = Join-Path (Join-Path $Out 'src') (Join-Path $m.Name ($f -replace '/', '\'))
|
||||
$dstDir = Split-Path -Parent $dst
|
||||
if (-not (Test-Path -LiteralPath $dstDir)) { New-Item -ItemType Directory -Path $dstDir -Force | Out-Null }
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
|
||||
$meta = (Invoke-Git $modDir @('log', '-1', '--diff-filter=A', '--format=%h|%ad|%an', '--date=short', '--', $f) | Select-Object -First 1)
|
||||
$cHash = ''; $cDate = ''; $cAuth = ''
|
||||
if ($meta) {
|
||||
$parts = $meta.Split('|')
|
||||
if ($parts.Count -ge 3) { $cHash = $parts[0]; $cDate = $parts[1]; $cAuth = $parts[2] }
|
||||
}
|
||||
$leaf = Split-Path $f -Leaf
|
||||
$ext = if ($leaf -match '\.') { $leaf.Substring($leaf.LastIndexOf('.') + 1) } else { '' }
|
||||
|
||||
$esc = { param($s) '"' + ($s -replace '"', '""') + '"' }
|
||||
$Rows.Add((@(
|
||||
(& $esc $m.Name), (& $esc $f), (& $esc $ext),
|
||||
(& $esc $cHash), (& $esc $cDate), (& $esc $cAuth)
|
||||
) -join ','))
|
||||
}
|
||||
|
||||
$Total += $count
|
||||
$Missing += $miss
|
||||
$SummaryRs.Add([pscustomobject]@{ Module = $m.Name; Count = $count; Base = $baseDesc })
|
||||
Write-Host ("[수집] {0,-22} {1,5} 개 base: {2}" -f $m.Name, $count, $baseDesc)
|
||||
}
|
||||
|
||||
# ---- 출력 ------------------------------------------------------------------
|
||||
if (-not $DryRun) {
|
||||
if ($Rows.Count -gt 0) {
|
||||
[IO.File]::AppendAllText($Manifest, (($Rows -join "`r`n") + "`r`n"), (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
[void]$sb.AppendLine('# DJB 커스텀 소스 납품본')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine("- 생성 일시: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
|
||||
[void]$sb.AppendLine("- 기준 날짜: ``$Cutoff`` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)")
|
||||
[void]$sb.AppendLine('- 수집 규칙: `git diff --diff-filter=A -M <base> HEAD` / 확장자 allowlist / `export-filter.rules` 필터')
|
||||
[void]$sb.AppendLine("- 대상 확장자: ``$($AllowExt -join ',')``")
|
||||
[void]$sb.AppendLine("- 규칙 파일: ``$(Split-Path $RulesFile -Leaf)`` ($($Rules.Count) 개 규칙, 제외 $Excluded 건)")
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('## 모듈별 수집 결과')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('| 모듈 | 파일 수 | base 커밋 |')
|
||||
[void]$sb.AppendLine('|---|---:|---|')
|
||||
foreach ($r in $SummaryRs) { [void]$sb.AppendLine("| $($r.Module) | $($r.Count) | $($r.Base) |") }
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine("**합계: $Total 개**")
|
||||
if ($Missing -gt 0) { [void]$sb.AppendLine("> 추가 후 삭제/이동되어 현재 트리에 없는 파일 $Missing 개는 제외됨.") }
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('## 파일 목록')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('`MANIFEST.csv` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자).')
|
||||
|
||||
[IO.File]::WriteAllText($Summary, $sb.ToString(), (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "MANIFEST : $Manifest"
|
||||
Write-Host "SUMMARY : $Summary"
|
||||
|
||||
if ($Zip) {
|
||||
$zipName = "djb-custom-export-$(Get-Date -Format 'yyyyMMdd').zip"
|
||||
$zipPath = Join-Path (Split-Path -Parent $Out) $zipName
|
||||
if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
|
||||
Compress-Archive -Path (Join-Path $Out '*') -DestinationPath $zipPath
|
||||
Write-Host "ZIP : $zipPath"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# DJB 커스텀 소스 납품본 수집 스크립트 (macOS / Linux)
|
||||
#
|
||||
# 기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
|
||||
# 모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
|
||||
# MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
|
||||
#
|
||||
# 사용법:
|
||||
# ./export-custom.sh # 기본값으로 실행
|
||||
# ./export-custom.sh -c 2026-05-01 --zip
|
||||
# ./export-custom.sh --dry-run
|
||||
# ./export-custom.sh -m eapim-portal -m eapim-admin
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONF_FILE="$SCRIPT_DIR/modules.conf"
|
||||
RULES_FILE="$SCRIPT_DIR/export-filter.rules"
|
||||
|
||||
# ---- 기본값 ---------------------------------------------------------------
|
||||
CUTOFF="2026-05-01"
|
||||
REPO_ROOT=""
|
||||
OUT_DIR=""
|
||||
MAKE_ZIP=0
|
||||
DRY_RUN=0
|
||||
INCLUDE_UNTRACKED=0
|
||||
ONLY_MODULES=""
|
||||
SHOW_EXCLUDED=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
DJB 커스텀 소스 납품본 수집
|
||||
|
||||
옵션:
|
||||
-c, --cutoff <YYYY-MM-DD> 기준 날짜. 이 날짜 직전 마지막 커밋을 base로 삼는다.
|
||||
(기본: 2026-05-01)
|
||||
-r, --root <DIR> 저장소 루트(djb-eapim). 미지정 시 스크립트 위치에서 자동 탐지.
|
||||
-o, --out <DIR> 출력 디렉터리. (기본: <root>/build/djb-custom-export)
|
||||
-m, --module <NAME> 특정 모듈만 수집. 여러 번 지정 가능.
|
||||
--zip 수집 후 zip 아카이브 생성.
|
||||
--include-untracked git에 아직 커밋되지 않은 신규 파일도 포함.
|
||||
--dry-run 복사하지 않고 대상 목록만 stdout 으로 출력(로그는 stderr).
|
||||
--show-excluded --dry-run 과 함께. 규칙에 걸려 제외된 목록을 대신 출력.
|
||||
-h, --help 도움말.
|
||||
|
||||
포함/제외 규칙은 export-filter.rules (.gitignore 유사 문법) 에서 관리한다.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-c|--cutoff) CUTOFF="$2"; shift 2 ;;
|
||||
-r|--root) REPO_ROOT="$2"; shift 2 ;;
|
||||
-o|--out) OUT_DIR="$2"; shift 2 ;;
|
||||
-m|--module) ONLY_MODULES="$ONLY_MODULES $2"; shift 2 ;;
|
||||
--zip) MAKE_ZIP=1; shift ;;
|
||||
--include-untracked) INCLUDE_UNTRACKED=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--show-excluded) SHOW_EXCLUDED=1; DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "알 수 없는 옵션: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- 저장소 루트 탐지 -----------------------------------------------------
|
||||
# 스크립트는 <root>/eapim-portal/script/djb-custom-export/ 에 있다.
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
fi
|
||||
if [ ! -d "$REPO_ROOT" ]; then
|
||||
echo "저장소 루트를 찾을 수 없다: $REPO_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$OUT_DIR" ] && OUT_DIR="$REPO_ROOT/build/djb-custom-export"
|
||||
|
||||
if [ ! -f "$CONF_FILE" ]; then
|
||||
echo "설정 파일 없음: $CONF_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 설정 로드 ------------------------------------------------------------
|
||||
ALLOW_EXT=""
|
||||
EXCLUDE_PAT=""
|
||||
MODULE_LINES=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
key="${line%%|*}"
|
||||
rest="${line#*|}"
|
||||
case "$key" in
|
||||
MODULE) MODULE_LINES="$MODULE_LINES
|
||||
$rest" ;;
|
||||
EXT) ALLOW_EXT="$rest" ;;
|
||||
EXCLUDE) EXCLUDE_PAT="$rest" ;;
|
||||
esac
|
||||
done < "$CONF_FILE"
|
||||
|
||||
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
|
||||
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
is_allowed_ext() {
|
||||
_f="$1"
|
||||
_base="${_f##*/}"
|
||||
case "$_base" in
|
||||
*.*) _ext="${_base##*.}" ;;
|
||||
*) return 1 ;; # 확장자 없는 파일 제외
|
||||
esac
|
||||
# 소문자 변환 (bash 3.2 호환)
|
||||
_ext="$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')"
|
||||
case ",$ALLOW_EXT," in
|
||||
*",$_ext,"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- 포함/제외 규칙 (.gitignore 유사) --------------------------------------
|
||||
RULES=()
|
||||
if [ -f "$RULES_FILE" ]; then
|
||||
while IFS= read -r r || [ -n "$r" ]; do
|
||||
# 앞뒤 공백 제거
|
||||
r="${r#"${r%%[![:space:]]*}"}"
|
||||
r="${r%"${r##*[![:space:]]}"}"
|
||||
case "$r" in
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
RULES[${#RULES[@]}]="$r"
|
||||
done < "$RULES_FILE"
|
||||
fi
|
||||
|
||||
# 경로 하나에 규칙을 순서대로 적용한다. 마지막으로 매칭된 규칙이 이긴다.
|
||||
# 기본은 포함(0), '!' 규칙은 다시 포함, 일반 규칙은 제외(1).
|
||||
# 반환: 0 = 포함, 1 = 제외. 제외시킨 규칙은 MATCHED_RULE 에 담긴다.
|
||||
MATCHED_RULE=""
|
||||
rule_keep() {
|
||||
_path="$1"
|
||||
_leaf="${_path##*/}"
|
||||
_keep=0
|
||||
MATCHED_RULE=""
|
||||
_i=0
|
||||
while [ $_i -lt ${#RULES[@]} ]; do
|
||||
_rule="${RULES[$_i]}"
|
||||
_i=$((_i + 1))
|
||||
|
||||
_neg=0
|
||||
case "$_rule" in
|
||||
'!'*) _neg=1; _rule="${_rule#!}" ;;
|
||||
esac
|
||||
# 디렉터리 규칙: 하위 전체
|
||||
case "$_rule" in
|
||||
*/) _rule="${_rule}*" ;;
|
||||
esac
|
||||
# '/' 가 있으면 전체 경로, 없으면 파일명과 매칭
|
||||
case "$_rule" in
|
||||
*/*) _target="$_path" ;;
|
||||
*) _target="$_leaf" ;;
|
||||
esac
|
||||
|
||||
# shellcheck disable=SC2254 # 의도적으로 unquoted — glob 패턴으로 쓴다
|
||||
case "$_target" in
|
||||
$_rule)
|
||||
if [ $_neg -eq 1 ]; then
|
||||
_keep=0; MATCHED_RULE=""
|
||||
else
|
||||
_keep=1; MATCHED_RULE="$_rule"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return $_keep
|
||||
}
|
||||
|
||||
csv_escape() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
|
||||
}
|
||||
|
||||
# ---- 준비 -----------------------------------------------------------------
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
rm -rf "$OUT_DIR"
|
||||
mkdir -p "$OUT_DIR/src"
|
||||
fi
|
||||
|
||||
MANIFEST="$OUT_DIR/MANIFEST.csv"
|
||||
SUMMARY="$OUT_DIR/SUMMARY.md"
|
||||
TMP_SUMMARY="$(mktemp)"
|
||||
trap 'rm -f "$TMP_SUMMARY"' EXIT
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
printf '\357\273\277' > "$MANIFEST" # Excel 한글용 UTF-8 BOM
|
||||
echo 'module,path,ext,added_commit,added_date,author' >> "$MANIFEST"
|
||||
fi
|
||||
|
||||
# dry-run(목록 뽑기) 모드에서는 파일 목록만 stdout으로 내보내고
|
||||
# 진행 로그는 전부 stderr로 보낸다. 그래야 파이프/리다이렉트로 목록을 바로 가공할 수 있다.
|
||||
if [ "$DRY_RUN" -eq 1 ]; then LOG=2; else LOG=1; fi
|
||||
|
||||
{
|
||||
echo "저장소 루트 : $REPO_ROOT"
|
||||
echo "기준 날짜 : $CUTOFF (이 날짜 직전 마지막 커밋이 base)"
|
||||
echo "출력 경로 : $OUT_DIR"
|
||||
[ "$DRY_RUN" -eq 1 ] && echo "모드 : DRY-RUN (목록만 출력, 복사 안 함)"
|
||||
echo
|
||||
} >&$LOG
|
||||
|
||||
TOTAL=0
|
||||
MISSING=0
|
||||
EXCLUDED=0
|
||||
|
||||
# ---- 모듈 순회 ------------------------------------------------------------
|
||||
# 파이프 대신 process substitution 을 써서 집계 변수가 서브셸에 갇히지 않게 한다.
|
||||
while IFS= read -r mline; do
|
||||
[ -z "$mline" ] && continue
|
||||
MOD="${mline%%|*}"
|
||||
PATHS="${mline#*|}"
|
||||
|
||||
if [ -n "$ONLY_MODULES" ]; then
|
||||
case " $ONLY_MODULES " in
|
||||
*" $MOD "*) : ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
MOD_DIR="$REPO_ROOT/$MOD"
|
||||
if [ ! -d "$MOD_DIR/.git" ]; then
|
||||
echo "[건너뜀] $MOD — git 저장소 아님 ($MOD_DIR)" >&$LOG
|
||||
continue
|
||||
fi
|
||||
|
||||
# base 커밋 결정
|
||||
BASE="$(git -C "$MOD_DIR" -c core.quotepath=false rev-list -1 --before="$CUTOFF" HEAD 2>/dev/null || true)"
|
||||
if [ -z "$BASE" ]; then
|
||||
BASE="$EMPTY_TREE"
|
||||
BASE_DESC="(컷오프 이전 커밋 없음 → 전체를 신규로 간주)"
|
||||
else
|
||||
BASE_DESC="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --format='%h %ad %s' --date=short "$BASE" | cut -c1-80)"
|
||||
fi
|
||||
|
||||
# 실제 존재하는 소스 경로만 pathspec으로 사용
|
||||
SPEC=""
|
||||
for p in $PATHS; do
|
||||
[ -d "$MOD_DIR/$p" ] && SPEC="$SPEC $p"
|
||||
done
|
||||
if [ -z "$SPEC" ]; then
|
||||
echo "[건너뜀] $MOD — 설정된 소스 경로가 존재하지 않음" >&$LOG
|
||||
continue
|
||||
fi
|
||||
|
||||
# 신규(Added) 파일만. -M 으로 rename은 신규에서 제외한다.
|
||||
FILES="$(git -C "$MOD_DIR" -c core.quotepath=false diff --name-only --diff-filter=A -M "$BASE" HEAD -- $SPEC 2>/dev/null || true)"
|
||||
|
||||
if [ "$INCLUDE_UNTRACKED" -eq 1 ]; then
|
||||
UNTRACKED="$(git -C "$MOD_DIR" -c core.quotepath=false ls-files --others --exclude-standard -- $SPEC 2>/dev/null || true)"
|
||||
FILES="$FILES
|
||||
$UNTRACKED"
|
||||
fi
|
||||
|
||||
COUNT=0
|
||||
MISS=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
is_allowed_ext "$f" || continue
|
||||
|
||||
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
|
||||
if ! rule_keep "$MOD/$f"; then
|
||||
EXCLUDED=$((EXCLUDED + 1))
|
||||
[ "$SHOW_EXCLUDED" -eq 1 ] && printf '%s\t# %s\n' "$MOD/$f" "$MATCHED_RULE"
|
||||
continue
|
||||
fi
|
||||
[ "$SHOW_EXCLUDED" -eq 1 ] && continue
|
||||
|
||||
SRC="$MOD_DIR/$f"
|
||||
if [ ! -f "$SRC" ]; then
|
||||
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
|
||||
echo " [없음] $MOD/$f" >&2
|
||||
MISS=$((MISS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
COUNT=$((COUNT + 1))
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "$MOD/$f" # 목록은 stdout으로 (가공하기 쉽게 접두 공백 없음)
|
||||
continue
|
||||
fi
|
||||
|
||||
DST="$OUT_DIR/src/$MOD/$f"
|
||||
mkdir -p "$(dirname "$DST")"
|
||||
cp -p "$SRC" "$DST"
|
||||
|
||||
META="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --diff-filter=A --format='%h|%ad|%an' --date=short -- "$f" 2>/dev/null || true)"
|
||||
C_HASH="${META%%|*}"; _r="${META#*|}"
|
||||
C_DATE="${_r%%|*}"
|
||||
C_AUTH="${_r#*|}"
|
||||
BASE_F="${f##*/}"
|
||||
case "$BASE_F" in *.*) EXT="${BASE_F##*.}" ;; *) EXT="" ;; esac
|
||||
|
||||
{
|
||||
csv_escape "$MOD"; printf ','
|
||||
csv_escape "$f"; printf ','
|
||||
csv_escape "$EXT"; printf ','
|
||||
csv_escape "$C_HASH"; printf ','
|
||||
csv_escape "$C_DATE"; printf ','
|
||||
csv_escape "$C_AUTH"; printf '\n'
|
||||
} >> "$MANIFEST"
|
||||
done < <(printf '%s\n' "$FILES" | sort -u)
|
||||
|
||||
TOTAL=$((TOTAL + COUNT))
|
||||
MISSING=$((MISSING + MISS))
|
||||
printf '%s|%s|%s\n' "$MOD" "$COUNT" "$BASE_DESC" >> "$TMP_SUMMARY"
|
||||
printf '[수집] %-22s %5s 개 base: %s\n' "$MOD" "$COUNT" "$BASE_DESC" >&$LOG
|
||||
done < <(printf '%s\n' "$MODULE_LINES")
|
||||
|
||||
# ---- 요약 ------------------------------------------------------------------
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
{
|
||||
echo "# DJB 커스텀 소스 납품본"
|
||||
echo
|
||||
echo "- 생성 일시: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "- 기준 날짜: \`$CUTOFF\` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)"
|
||||
echo "- 수집 규칙: \`git diff --diff-filter=A -M <base> HEAD\` / 확장자 allowlist / \`export-filter.rules\` 필터"
|
||||
echo "- 대상 확장자: \`$ALLOW_EXT\`"
|
||||
echo "- 규칙 파일: \`$(basename "$RULES_FILE")\` (${#RULES[@]} 개 규칙, 제외 ${EXCLUDED} 건)"
|
||||
echo
|
||||
echo "## 모듈별 수집 결과"
|
||||
echo
|
||||
echo "| 모듈 | 파일 수 | base 커밋 |"
|
||||
echo "|---|---:|---|"
|
||||
while IFS='|' read -r m n d; do
|
||||
[ -z "$m" ] && continue
|
||||
echo "| $m | $n | $d |"
|
||||
done < "$TMP_SUMMARY"
|
||||
echo
|
||||
echo "**합계: ${TOTAL} 개**"
|
||||
[ "$MISSING" -gt 0 ] && echo "> 추가 후 삭제/이동되어 현재 트리에 없는 파일 ${MISSING} 개는 제외됨."
|
||||
echo
|
||||
echo "## 파일 목록"
|
||||
echo
|
||||
echo "\`MANIFEST.csv\` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자)."
|
||||
} > "$SUMMARY"
|
||||
|
||||
echo
|
||||
echo "MANIFEST : $MANIFEST"
|
||||
echo "SUMMARY : $SUMMARY"
|
||||
|
||||
if [ "$MAKE_ZIP" -eq 1 ]; then
|
||||
ZIP_NAME="djb-custom-export-$(date '+%Y%m%d').zip"
|
||||
ZIP_PATH="$(dirname "$OUT_DIR")/$ZIP_NAME"
|
||||
rm -f "$ZIP_PATH"
|
||||
( cd "$OUT_DIR" && zip -qr "$ZIP_PATH" . )
|
||||
echo "ZIP : $ZIP_PATH"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,74 @@
|
||||
# DJB 커스텀 소스 납품 - 포함/제외 규칙
|
||||
#
|
||||
# .gitignore 와 비슷하게 동작한다.
|
||||
# - 한 줄에 패턴 하나. '#' 주석, 빈 줄 무시.
|
||||
# - 기본은 "모두 포함". 패턴에 걸리면 제외된다.
|
||||
# - '!' 로 시작하면 예외(다시 포함).
|
||||
# - 뒤에 오는 규칙이 앞 규칙을 덮는다 (마지막 매칭 승리). → 순서가 중요하다.
|
||||
# - 패턴에 '/' 가 있으면 <모듈>/<경로> 전체와 glob 매칭,
|
||||
# 없으면 파일명(basename)과만 매칭한다.
|
||||
# - '/' 로 끝나면 그 디렉터리 하위 전체를 뜻한다.
|
||||
# - '*' 는 '/' 도 포함해 매칭한다(단순 glob).
|
||||
#
|
||||
# 매칭 대상 경로 예:
|
||||
# eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
|
||||
# eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
|
||||
#
|
||||
# 확인: ./export-custom.sh --dry-run (남는 목록)
|
||||
# ./export-custom.sh --dry-run --show-excluded (규칙에 걸려 빠진 목록)
|
||||
|
||||
# ===========================================================================
|
||||
# 1) 제품 업그레이드로 유입된 신규 파일 — 커스텀 아니므로 제외
|
||||
# filelist-no-djb-marker.txt(djb/custom 마커 없는 신규 파일)를 검토하며 채운다.
|
||||
# ===========================================================================
|
||||
# 예시)
|
||||
# eapim-online/src/main/java/com/eactive/eai/agent/inflow/
|
||||
# eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/
|
||||
|
||||
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/
|
||||
eapim-portal/src/test/
|
||||
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2) 위 제외에 걸렸더라도 되살릴 예외
|
||||
# djb 마커가 붙은 파일은 어떤 경우에도 커스텀이다.
|
||||
# ※ 아래 3)의 빌드 산출물 제외보다 반드시 앞에 와야 한다.
|
||||
# ===========================================================================
|
||||
!*djb*
|
||||
!*DJB*
|
||||
!*Djb*
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3) 무조건 제외 (최종 승리) — 소스가 아니거나 서드파티
|
||||
# ===========================================================================
|
||||
*/build/
|
||||
*/out/
|
||||
*/bin/
|
||||
*/.gradle/
|
||||
*/.metadata/
|
||||
*/node_modules/
|
||||
*/generated/
|
||||
*/design-backup/
|
||||
*/WebContent/plugins/
|
||||
|
||||
# SafeDB(damo) 벤더 API stub — 컴파일용 껍데기라 납품 대상 아님
|
||||
damo-manager/src/stub/
|
||||
|
||||
# 서드파티 폴더 안에 섞여 있는 우리 커스텀 파일은 되살린다
|
||||
# (예: WebContent/plugins/swaggerUI/djb-swagger-i18n.js)
|
||||
!*/WebContent/plugins/*djb*
|
||||
@@ -0,0 +1,157 @@
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDay.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDayId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHour.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHourId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonth.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonthId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwApiStatus.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwAuthClient.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwSvcInfo.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsDayRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsHourRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsMonthRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwApiStatusRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwAuthClientRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwSvcInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/filter/ApiTesterAuditLogger.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorChannel.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorInfoResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorSendResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorVerifyResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpAuthInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpPasswordController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorCleanupScheduler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorContext.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/notice/dto/IncidentAffectedApiDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/partnership/dto/PartnershipApplicationSummaryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginFailureReason.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginType.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/controller/DuplicateLoginController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/DuplicateLoginService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/controller/SessionApiController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/entity/UserSession.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/filter/SessionValidationFilter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/dto/ApiStatisticsPeriodDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/entity/JobInfo.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/JobInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolver.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/ClientGuardService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordChangeEnforcementInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordEnforcementPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/StartupInfoPrinter.java
|
||||
eapim-portal/src/main/resources/menu.yml
|
||||
eapim-portal/src/main/resources/roles.yml
|
||||
eapim-portal/src/main/resources/static/js/api-selector.js
|
||||
eapim-portal/src/main/resources/static/js/password-policy.js
|
||||
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
|
||||
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_toast.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/cancelInvitationPopup.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/terminateRequestPopup.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/twoFactorAuthPopup.html
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolverTest.java
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapServiceImpl.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/ui/SitemapNode.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/authserver/ClientBlockApiController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUIMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUIMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentTimelineUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/inflow/inflow/InflowClientControlManController.java
|
||||
eapim-admin/src/main/resources/apistatus-draft.yml
|
||||
eapim-admin/src/main/resources/logback-rinjae_ma.xml
|
||||
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java
|
||||
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java
|
||||
eapim-admin/WebContent/damo.jsp
|
||||
eapim-admin/WebContent/jsp/common/acl/sitemap/sitemapMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/rule/transform2/transform2ManApiPopup.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/portalmenu/portalMenuMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/kjb/statistics/apiUseStatsMan.jsp
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/IpAddressMatcher.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/PhoneNumberUtil.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/PersonalDataEncryptConverter.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/UuidV7Generator.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
|
||||
elink-portal-common/src/test/java/com/eactive/apim/portal/common/util/IpAddressMatcherTest.java
|
||||
@@ -0,0 +1,173 @@
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/controller/ApiStatusController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ActiveIncidentDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/AffectedApiDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiCurrentStatusDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiOptionDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/DailyStatDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/IssueDateEntryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MaintenanceCardDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MyApiStatusDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/PastIssueCardDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/TimelineEntryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/MyApiStatusQueryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/controller/InquiryCommentController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentCreateRequest.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/InquiryCommentRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacade.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbInquiryStatus.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryClosedException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryCommentNotOwnedException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/support/InquiryCommentPermissionChecker.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuAccessInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuModelAdvice.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuNode.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuSeeder.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuView.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalMenuYmlProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalRolesProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/advice/DjbTestbedExceptionHandler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/config/DjbTestbedGatewayProperty.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedAuthController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedSpecController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialOptionDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialSecretDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbAuthType.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbGatewayMode.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/exception/DjbUnsupportedAuthTypeException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbSwaggerSpecEnricher.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedSpecServerRewriter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookCreatedResult.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookEventTypeDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookRegistrationDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookAlreadyExistsException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookNotFoundException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/mapper/WebhookMapper.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestApiRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestEventRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequest.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApi.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApiId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEvent.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEventId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookEventTypeProvider.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookSecretGenerator.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookService.java
|
||||
eapim-portal/src/main/resources/menu.yml
|
||||
eapim-portal/src/main/resources/roles.yml
|
||||
eapim-portal/src/main/resources/static/favicon.png
|
||||
eapim-portal/src/main/resources/static/img/avatar1.svg
|
||||
eapim-portal/src/main/resources/static/img/avatar2.svg
|
||||
eapim-portal/src/main/resources/static/img/favicon/apple-touch-icon.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-16x16.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-180x180.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-192x192.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-32x32.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-48x48.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-512x512.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon.png
|
||||
eapim-portal/src/main/resources/static/img/icon/icon_check_green.png
|
||||
eapim-portal/src/main/resources/static/img/icon/img_icon.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/api_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/api_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/faq_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/faq_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/feedback_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/notice_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/notice_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/sigdnUp_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/signUp_img.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-djb.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-jjb.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-jjb_white.png
|
||||
eapim-portal/src/main/resources/static/js/api-selector.js
|
||||
eapim-portal/src/main/resources/static/js/djb/api-status-issues.js
|
||||
eapim-portal/src/main/resources/static/js/djb/api-status.js
|
||||
eapim-portal/src/main/resources/static/js/djb/client-guard.js
|
||||
eapim-portal/src/main/resources/static/js/djb/inquiry-comments.js
|
||||
eapim-portal/src/main/resources/static/js/djb/inquiry-view.js
|
||||
eapim-portal/src/main/resources/static/js/djb/toast.js
|
||||
eapim-portal/src/main/resources/static/js/password-policy.js
|
||||
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-response.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippet-panel.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippets.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-testbed.css
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-validator.js
|
||||
eapim-portal/src/main/resources/static/sass/base/_djb-font.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_djb-inquiry-comments.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_toast.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/djb/fragments-inquiry-comments.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/djb/apistatus/index.html
|
||||
eapim-portal/src/main/resources/templates/views/djb/apistatus/issues.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/footer.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/header_container.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/service_sidebar.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/terms_agreements.html
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncident.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApi.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApiId.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentTimeline.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentKind.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentState.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentApiRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentTimelineRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
|
||||
damo-manager/src/main/java/com/eactive/ext/djb/DamoCli.java
|
||||
damo-manager/src/main/java/com/eactive/ext/djb/DamoManager.java
|
||||
@@ -0,0 +1,26 @@
|
||||
# DJB 커스텀 소스 납품 수집 설정
|
||||
#
|
||||
# 형식:
|
||||
# MODULE|<모듈 디렉터리>|<소스 경로들, 공백 구분>
|
||||
# EXT|<수집 대상 확장자, 콤마 구분>
|
||||
#
|
||||
# - 모듈 디렉터리는 저장소 루트(djb-eapim) 기준 상대 경로.
|
||||
# - 소스 경로가 실제로 없으면 조용히 건너뛴다.
|
||||
# - '#'으로 시작하는 줄과 빈 줄은 무시된다.
|
||||
# - 파일 단위 포함/제외는 export-filter.rules 에서 관리한다.
|
||||
|
||||
MODULE|eapim-portal|src/main
|
||||
#MODULE|eapim-admin|src WebContent
|
||||
MODULE|elink-portal-common|src/main
|
||||
MODULE|damo-manager|src
|
||||
|
||||
# eapim-online 은 납품 범위에서 제외한다.
|
||||
# Gateway 제품(elink-online) 본체라 신규 파일 중 무엇이 DJB 커스텀이고
|
||||
# 무엇이 제품 업그레이드분인지 포털 쪽에서 판정할 수 없다.
|
||||
# 규칙 파일이 아니라 여기서 빼는 이유: export-filter.rules 의 '!*djb*' 예외가
|
||||
# 뒤에 있어 DJBEncrypt.java 같은 파일이 되살아나기 때문.
|
||||
#MODULE|eapim-online|src elink-online-common/src elink-online-core/src elink-online-core-jpa/src elink-online-emsclient/src elink-online-transformer/src
|
||||
|
||||
# 수집 대상 확장자 (확장자 없는 파일은 제외된다)
|
||||
# EXT|java,xml,yml,yaml,properties,sql,html,jsp,jspf,tag,js,ts,css,scss,json,txt,md
|
||||
EXT|java,xml,yml,yaml,properties,html,jsp,jspf,tag,js,ts,css,scss,jpg,jpeg,png,gif,svg,ico
|
||||
@@ -254,7 +254,8 @@ CREATE TABLE DVPOWN.PT_MESSAGE_RECIPIENT
|
||||
)
|
||||
;
|
||||
|
||||
create table DVPOWN.PT_TOKEN
|
||||
create table PT_TOKEN
|
||||
(
|
||||
(
|
||||
TOKEN VARCHAR2(255) not null
|
||||
primary key,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
-- =============================================================================
|
||||
-- 세션 타이머 / 유휴 자동 로그아웃 / 중복로그인 처리용 사용자 세션 추적 테이블
|
||||
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
|
||||
-- 엔티티: com.eactive.apim.portal.apps.session.entity.UserSession
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE PTL_USER_SESSION
|
||||
(
|
||||
SESSION_ID VARCHAR2(128) NOT NULL,
|
||||
USER_ID VARCHAR2(36) NOT NULL,
|
||||
LOGIN_ID VARCHAR2(200) NOT NULL,
|
||||
LOGIN_TIME TIMESTAMP(6) NOT NULL,
|
||||
LAST_ACCESS_TIME TIMESTAMP(6) NOT NULL,
|
||||
IP_ADDRESS VARCHAR2(45),
|
||||
USER_AGENT VARCHAR2(500),
|
||||
FORCE_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL,
|
||||
CONSTRAINT PK_PTL_USER_SESSION PRIMARY KEY (SESSION_ID)
|
||||
);
|
||||
|
||||
-- 중복로그인 강제 로그아웃 / 활성 세션 조회는 LOGIN_ID 기준
|
||||
CREATE INDEX IX_PTL_USER_SESSION_LOGIN ON PTL_USER_SESSION (LOGIN_ID);
|
||||
|
||||
COMMENT ON TABLE PTL_USER_SESSION IS '포털 사용자 세션 추적 (타이머/유휴 로그아웃/중복로그인)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.SESSION_ID IS 'HTTP 세션 ID (PK)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.USER_ID IS '포털 사용자 ID';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_ID IS '로그인 ID (이메일, 소문자)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_TIME IS '로그인 시각';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LAST_ACCESS_TIME IS '마지막 접근 시각 (만료 판단 기준)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.IP_ADDRESS IS '접속 IP';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.USER_AGENT IS 'User-Agent';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.FORCE_LOGOUT IS '강제 로그아웃 플래그 (Y/N)';
|
||||
|
||||
-- (선택) 세션 타임아웃(분) 설정값을 미리 지정. 미삽입 시 최초 접근 때 기본값 15로 자동 생성됨.
|
||||
-- INSERT INTO PTL_PROPERTY (PROPERTY_GROUP_NAME, PROPERTY_NAME, PROPERTY_VALUE, PROPERTY_DESC)
|
||||
-- VALUES ('Portal', 'session.timeout.minutes', '15', '세션 타임아웃 시간 (분)');
|
||||
-- COMMIT;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- =============================================================================
|
||||
-- 2FA 인증코드 조회용 결정적 해시 키 컬럼 추가
|
||||
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
|
||||
-- 엔티티: com.eactive.apim.portal.portaluser.entity.TwoFactorAuth
|
||||
--
|
||||
-- 배경: RECIPIENT(이메일/휴대폰) 컬럼에 PersonalDataEncryptConverter 암호화가
|
||||
-- 적용되면서, 값 동등 조회(WHERE RECIPIENT = ?)와 중복정리가 깨져
|
||||
-- 인증코드 검증이 "일치하지 않습니다"로 실패함.
|
||||
-- 해결: RECIPIENT 는 암호화 보관(감사/표시용)을 유지하고, 평문의 결정적 해시
|
||||
-- (HMAC-SHA256, 소문자 hex 64자)를 RECIPIENT_KEY 에 저장하여 조회/정리에 사용.
|
||||
--
|
||||
-- ※ hibernate.hbm2ddl.auto=validate 이므로, 신규 엔티티 배포(재기동) "전"에
|
||||
-- 반드시 이 스크립트를 먼저 실행해야 기동 검증을 통과함.
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE PTL_TWO_FACTOR_AUTH ADD (RECIPIENT_KEY VARCHAR2(64));
|
||||
|
||||
-- 인증코드 조회/중복정리는 RECIPIENT_KEY 기준
|
||||
CREATE INDEX IX_PTL_TWO_FACTOR_AUTH_RKEY ON PTL_TWO_FACTOR_AUTH (RECIPIENT_KEY);
|
||||
|
||||
COMMENT ON COLUMN PTL_TWO_FACTOR_AUTH.RECIPIENT_KEY IS '수신자 결정적 해시 조회키 (HMAC-SHA256 hex). RECIPIENT 암호화로 깨지는 동등조회/중복정리용';
|
||||
|
||||
COMMIT;
|
||||
@@ -4,8 +4,4 @@ include 'elink-online-core-jpa'
|
||||
include 'elink-portal-common'
|
||||
|
||||
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
|
||||
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||
|
||||
|
||||
//include 'kjb-safedb'
|
||||
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
||||
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||
@@ -0,0 +1,61 @@
|
||||
# SonarQube 분석 설정 (SonarScanner CLI 용)
|
||||
#
|
||||
# Jenkinsfile.sonar 가 이 파일을 그대로 읽고, 아래 항목만 실행 시점에 -D 로 덮어쓴다.
|
||||
# sonar.projectVersion : 빌드 번호
|
||||
# sonar.java.libraries : ci/sonar-classpath.gradle 이 덤프한 컴파일 클래스패스
|
||||
# sonar.java.test.libraries : 위 + 테스트 클래스패스
|
||||
# sonar.junit.reportPaths : 테스트를 실행한 경우에만
|
||||
#
|
||||
# 로컬에서 수동 분석 시(스캐너는 JDK 17 필요):
|
||||
# gradle clean classes testClasses
|
||||
# gradle -I ci/sonar-classpath.gradle exportSonarClasspath
|
||||
# sonar-scanner -Dsonar.host.url=... -Dsonar.token=... \
|
||||
# -Dsonar.java.libraries="$(cat build/sonar/java-libraries.txt)"
|
||||
|
||||
sonar.projectKey=djb-eapim-portal
|
||||
sonar.projectName=DJB eAPIM Portal
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# --- 분석 대상 -------------------------------------------------------------
|
||||
# src/main/resources 를 포함시켜 Thymeleaf 템플릿(XSS), yml/properties(하드코딩 시크릿)까지 검사한다.
|
||||
sonar.sources=src/main/java,src/main/resources
|
||||
sonar.tests=src/test/java
|
||||
|
||||
# --- Java ------------------------------------------------------------------
|
||||
# 소스는 Java 8. 스캐너 실행 JRE 는 별개로 17 이상이어야 한다(Jenkinsfile.sonar 참고).
|
||||
sonar.java.source=8
|
||||
sonar.java.binaries=build/classes/java/main
|
||||
sonar.java.test.binaries=build/classes/java/test
|
||||
|
||||
# --- 제외 대상 -------------------------------------------------------------
|
||||
# 서드파티 번들/컴파일 산출물/바이너리. 분석해도 우리가 고칠 수 없는 코드는 뺀다.
|
||||
sonar.exclusions=\
|
||||
**/build/**,\
|
||||
**/node_modules/**,\
|
||||
**/lib/**,\
|
||||
**/libs/**,\
|
||||
**/local_jars/**,\
|
||||
src/main/resources/static/plugins/**,\
|
||||
src/main/resources/static/js/egovframework/**,\
|
||||
src/main/resources/static/js/**/*.min.js,\
|
||||
src/main/resources/static/js/lodash.js,\
|
||||
src/main/resources/static/js/popper.js,\
|
||||
src/main/resources/static/js/daterangepicker.js,\
|
||||
src/main/resources/static/js/json-lint.js,\
|
||||
src/main/resources/static/html/**,\
|
||||
src/main/resources/static/css/**,\
|
||||
src/main/resources/static/sass/**,\
|
||||
src/main/resources/static/font/**,\
|
||||
src/main/resources/static/webfonts/**,\
|
||||
src/main/resources/static/img/**
|
||||
|
||||
# 자동 생성 코드(MapStruct Impl, QueryDSL Q클래스)는 build/ 아래라 이미 제외된다.
|
||||
|
||||
# --- SCM -------------------------------------------------------------------
|
||||
# blame 기반 "새 코드" 판정을 위해 Jenkins Job 에서 shallow clone 을 쓰지 않는다.
|
||||
sonar.scm.provider=git
|
||||
|
||||
# --- 브랜치 ----------------------------------------------------------------
|
||||
# sonar.branch.name 은 SonarQube Developer Edition 이상에서만 동작한다.
|
||||
# Community Edition 에서 지정하면 분석이 실패하므로 기본적으로 설정하지 않는다.
|
||||
# sonar.branch.name=feats/ci-test
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* API Gateway 일별 처리 통계 (AGWAPP.API_STATS_DAY) — 읽기 전용.
|
||||
*
|
||||
* 월별 조회 시 "현재 월"은 아직 API_STATS_MONTH 에 집계되지 않으므로(월 집계 잡은 익월 1일 실행)
|
||||
* 현재 월분은 이 DAY 테이블에서 조회한다.
|
||||
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "API_STATS_DAY")
|
||||
@IdClass(ApiStatsDayId.class)
|
||||
public class ApiStatsDay implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "STAT_TIME", nullable = false)
|
||||
private LocalDate statTime;
|
||||
|
||||
@Id
|
||||
@Column(name = "API_NAME", nullable = false, length = 100)
|
||||
private String apiName;
|
||||
|
||||
@Id
|
||||
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
|
||||
private String gwInstanceId;
|
||||
|
||||
@Id
|
||||
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
|
||||
private String bizDivCode = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "CLIENT_ID", nullable = false, length = 100)
|
||||
private String clientId = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String inboundAdapter = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String outboundAdapter = "NONE";
|
||||
|
||||
@Column(name = "TOTAL_CNT", nullable = false)
|
||||
private Long totalCnt = 0L;
|
||||
|
||||
@Column(name = "SUCCESS_CNT", nullable = false)
|
||||
private Long successCnt = 0L;
|
||||
|
||||
@Column(name = "TIMEOUT_CNT", nullable = false)
|
||||
private Long timeoutCnt = 0L;
|
||||
|
||||
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
|
||||
private Long systemErrCnt = 0L;
|
||||
|
||||
@Column(name = "BIZ_ERR_CNT", nullable = false)
|
||||
private Long bizErrCnt = 0L;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API 통계 일별 복합키
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatsDayId implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private LocalDate statTime;
|
||||
private String apiName;
|
||||
private String gwInstanceId;
|
||||
private String bizDivCode;
|
||||
private String clientId;
|
||||
private String inboundAdapter;
|
||||
private String outboundAdapter;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* API Gateway 시간별 처리 통계 (AGWAPP.API_STATS_HOUR).
|
||||
*
|
||||
* 스키마명은 소스에 하드코딩하지 않는다 — gateway datasource 의 hibernate.default_schema(AGWAPP)로 해석된다.
|
||||
* 포털은 읽기 전용으로만 사용한다(집계 주체는 eapim-admin 스케줄러). 사용 컬럼(카운트)만 매핑한다.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "API_STATS_HOUR")
|
||||
@IdClass(ApiStatsHourId.class)
|
||||
public class ApiStatsHour implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "STAT_TIME", nullable = false)
|
||||
private LocalDateTime statTime;
|
||||
|
||||
@Id
|
||||
@Column(name = "API_NAME", nullable = false, length = 100)
|
||||
private String apiName;
|
||||
|
||||
@Id
|
||||
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
|
||||
private String gwInstanceId;
|
||||
|
||||
@Id
|
||||
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
|
||||
private String bizDivCode = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "CLIENT_ID", nullable = false, length = 100)
|
||||
private String clientId = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String inboundAdapter = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String outboundAdapter = "NONE";
|
||||
|
||||
@Column(name = "TOTAL_CNT", nullable = false)
|
||||
private Long totalCnt = 0L;
|
||||
|
||||
@Column(name = "SUCCESS_CNT", nullable = false)
|
||||
private Long successCnt = 0L;
|
||||
|
||||
@Column(name = "TIMEOUT_CNT", nullable = false)
|
||||
private Long timeoutCnt = 0L;
|
||||
|
||||
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
|
||||
private Long systemErrCnt = 0L;
|
||||
|
||||
@Column(name = "BIZ_ERR_CNT", nullable = false)
|
||||
private Long bizErrCnt = 0L;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API 통계 시간별 복합키
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatsHourId implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private LocalDateTime statTime;
|
||||
private String apiName;
|
||||
private String gwInstanceId;
|
||||
private String bizDivCode;
|
||||
private String clientId;
|
||||
private String inboundAdapter;
|
||||
private String outboundAdapter;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* API Gateway 월별 처리 통계 (AGWAPP.API_STATS_MONTH).
|
||||
*
|
||||
* STAT_TIME 은 YYYYMM 문자열. 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
|
||||
* 포털은 읽기 전용으로만 사용한다. 사용 컬럼(카운트)만 매핑한다.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "API_STATS_MONTH")
|
||||
@IdClass(ApiStatsMonthId.class)
|
||||
public class ApiStatsMonth implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "STAT_TIME", nullable = false, length = 6)
|
||||
private String statTime; // YYYYMM format
|
||||
|
||||
@Id
|
||||
@Column(name = "API_NAME", nullable = false, length = 100)
|
||||
private String apiName;
|
||||
|
||||
@Id
|
||||
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
|
||||
private String gwInstanceId;
|
||||
|
||||
@Id
|
||||
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
|
||||
private String bizDivCode = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "CLIENT_ID", nullable = false, length = 100)
|
||||
private String clientId = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String inboundAdapter = "NONE";
|
||||
|
||||
@Id
|
||||
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
|
||||
private String outboundAdapter = "NONE";
|
||||
|
||||
@Column(name = "TOTAL_CNT", nullable = false)
|
||||
private Long totalCnt = 0L;
|
||||
|
||||
@Column(name = "SUCCESS_CNT", nullable = false)
|
||||
private Long successCnt = 0L;
|
||||
|
||||
@Column(name = "TIMEOUT_CNT", nullable = false)
|
||||
private Long timeoutCnt = 0L;
|
||||
|
||||
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
|
||||
private Long systemErrCnt = 0L;
|
||||
|
||||
@Column(name = "BIZ_ERR_CNT", nullable = false)
|
||||
private Long bizErrCnt = 0L;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API 통계 월별 복합키
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiStatsMonthId implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String statTime; // YYYYMM format
|
||||
private String apiName;
|
||||
private String gwInstanceId;
|
||||
private String bizDivCode;
|
||||
private String clientId;
|
||||
private String inboundAdapter;
|
||||
private String outboundAdapter;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 상태 모니터링 결과 (AGWAPP.API_STATUS).
|
||||
*
|
||||
* <p>eapim-admin 의 {@code ApiStatusMonitorJob} 이 상태 변화가 있을 때만 upsert 한다.
|
||||
* 포털은 읽기 전용으로 "마지막 상태 변경 시각" 표시에 사용한다.</p>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "API_STATUS")
|
||||
@Data
|
||||
public class GwApiStatus {
|
||||
|
||||
/** EAI 서비스명 */
|
||||
@Id
|
||||
@Column(name = "EAISVCNAME", length = 30)
|
||||
private String eaisvcname;
|
||||
|
||||
/** N 정상 / C 점검 / D 지연 / E 장애 */
|
||||
@Column(name = "STATUS_CODE", length = 1)
|
||||
private String statusCode;
|
||||
|
||||
@Column(name = "MODIFIED_BY", length = 20)
|
||||
private String modifiedBy;
|
||||
|
||||
@Column(name = "MODIFIED_DATE")
|
||||
private LocalDateTime modifiedDate;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* GW 인증 Client 정보 (AGWAPP.TSEAIAU01) — 읽기 전용.
|
||||
*
|
||||
* API 통계의 org 스코핑에 사용한다: 통계 테이블에는 org_id 가 없으므로
|
||||
* TSEAIAU01.ORGID 로 org 의 CLIENTID 목록을 구해 API_STATS_*.CLIENT_ID 와 매칭한다.
|
||||
* (admin ApiUseStatsService 도 API_STATS.CLIENT_ID = TSEAIAU01.CLIENTID 로 조인)
|
||||
*
|
||||
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "TSEAIAU01")
|
||||
public class GwAuthClient implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "CLIENTID", nullable = false, length = 256)
|
||||
private String clientId;
|
||||
|
||||
@Column(name = "CLIENTNAME", length = 150)
|
||||
private String clientName;
|
||||
|
||||
@Column(name = "ORGID", length = 38)
|
||||
private String orgId;
|
||||
|
||||
@Column(name = "APPSTATUS", length = 1)
|
||||
private String appStatus;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.eactive.apim.gateway.data.statistics.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* GW 인터페이스(서비스) 정보 (AGWAPP.TSEAIHE01) — 읽기 전용.
|
||||
*
|
||||
* API_STATS_*.API_NAME 은 인터페이스 ID(=EAISVCNAME)이며, 화면 표시용 실제 명칭은 EAISVCDESC 다.
|
||||
* (admin ApiUseStatsService 도 API_STATS.API_NAME = TSEAIHE01.EAISVCNAME 으로 조인)
|
||||
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "TSEAIHE01")
|
||||
public class GwSvcInfo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "EAISVCNAME", nullable = false, length = 30)
|
||||
private String svcName; // 인터페이스 ID (= API_STATS.API_NAME)
|
||||
|
||||
@Column(name = "EAISVCDESC", length = 400)
|
||||
private String svcDesc; // 인터페이스 표시 명칭
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsDay;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsDayId;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* API_STATS_DAY(AGWAPP) 조회 리포지토리.
|
||||
* 월별 조회의 "현재 월" 분(아직 API_STATS_MONTH 미집계)을 일 범위로 합산하는 데 사용한다.
|
||||
*/
|
||||
public interface ApiStatsDayRepository extends JpaRepository<ApiStatsDay, ApiStatsDayId> {
|
||||
|
||||
/**
|
||||
* 전체 요약 (일자 범위, 지정 clientId 집합).
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
|
||||
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
|
||||
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsDay s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startDate AND :endDate")
|
||||
ApiStatisticsSummaryDto findSummary(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
|
||||
/**
|
||||
* API별 통계 (API_NAME 단위 집계).
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
|
||||
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
|
||||
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsDay s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startDate AND :endDate " +
|
||||
"GROUP BY s.apiName " +
|
||||
"ORDER BY SUM(s.totalCnt) DESC")
|
||||
List<ApiStatisticsDetailDto> findDetail(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 일자별 누적 통계 (월별 조회 하단 표에 사용 — 월별이라도 일별로 표기).
|
||||
* 생성자 표현식 안의 FUNCTION 은 Hibernate 5.6 이 타입 해석을 못 하므로 Object[] 로 받는다.
|
||||
* row: [0]=일자(String yyyy-MM-dd), [1]=total, [2]=success, [3]=timeout, [4]=systemErr, [5]=biz
|
||||
*/
|
||||
@Query("SELECT FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD'), " +
|
||||
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
|
||||
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0) " +
|
||||
"FROM ApiStatsDay s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startDate AND :endDate " +
|
||||
"GROUP BY FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD') " +
|
||||
"ORDER BY FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD')")
|
||||
List<Object[]> findPeriodByDay(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsHour;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsHourId;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* API_STATS_HOUR(AGWAPP) 조회 리포지토리.
|
||||
*
|
||||
* 통계 수치는 보존기간이 긴 API_STATS_DAY 를 사용하되, "오늘"은 아직 DAY 로 집계되지 않았으므로
|
||||
* (일 집계 잡은 익일 실행) 오늘분만 HOUR 에서 합산해 DAY 결과에 더한다. 최신 집계 시각도 HOUR 로 산출.
|
||||
*/
|
||||
public interface ApiStatsHourRepository extends JpaRepository<ApiStatsHour, ApiStatsHourId> {
|
||||
|
||||
/**
|
||||
* 전체 요약 (시각 범위, 지정 clientId 집합) — 오늘분 합산용.
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
|
||||
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
|
||||
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsHour s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startTime AND :endTime")
|
||||
ApiStatisticsSummaryDto findSummary(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* API별 통계 (API_NAME 단위 집계) — 오늘분 합산용.
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
|
||||
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
|
||||
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsHour s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startTime AND :endTime " +
|
||||
"GROUP BY s.apiName " +
|
||||
"ORDER BY SUM(s.totalCnt) DESC")
|
||||
List<ApiStatisticsDetailDto> findDetail(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 최신 집계 시각 (안내 문구용). 데이터 없으면 null.
|
||||
*/
|
||||
@Query("SELECT MAX(s.statTime) FROM ApiStatsHour s WHERE s.clientId IN :clientIds")
|
||||
LocalDateTime findLatestStatTime(@Param("clientIds") List<String> clientIds);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
|
||||
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsMonth;
|
||||
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsMonthId;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* API_STATS_MONTH(AGWAPP) 통계 조회 리포지토리 (월별 모드).
|
||||
* STAT_TIME 은 YYYYMM 문자열이므로 문자열 범위 비교로 조회한다.
|
||||
*/
|
||||
public interface ApiStatsMonthRepository extends JpaRepository<ApiStatsMonth, ApiStatsMonthId> {
|
||||
|
||||
/**
|
||||
* 전체 요약 (YYYYMM 범위, 지정 clientId 집합).
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
|
||||
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
|
||||
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsMonth s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startMonth AND :endMonth")
|
||||
ApiStatisticsSummaryDto findSummary(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startMonth") String startMonth,
|
||||
@Param("endMonth") String endMonth);
|
||||
|
||||
/**
|
||||
* API별 통계 (API_NAME 단위 집계).
|
||||
*/
|
||||
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
|
||||
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
|
||||
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
|
||||
"FROM ApiStatsMonth s " +
|
||||
"WHERE s.clientId IN :clientIds " +
|
||||
"AND s.statTime BETWEEN :startMonth AND :endMonth " +
|
||||
"GROUP BY s.apiName " +
|
||||
"ORDER BY SUM(s.totalCnt) DESC")
|
||||
List<ApiStatisticsDetailDto> findDetail(
|
||||
@Param("clientIds") List<String> clientIds,
|
||||
@Param("startMonth") String startMonth,
|
||||
@Param("endMonth") String endMonth);
|
||||
|
||||
/**
|
||||
* 조회 가능한 월 목록 (YYYYMM, 최신순).
|
||||
*/
|
||||
@Query("SELECT DISTINCT s.statTime FROM ApiStatsMonth s " +
|
||||
"WHERE s.clientId IN :clientIds ORDER BY s.statTime DESC")
|
||||
List<String> findAvailableMonths(@Param("clientIds") List<String> clientIds);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.gateway.data.statistics.entity.GwApiStatus;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GwApiStatusRepository extends Repository<GwApiStatus, String> {
|
||||
|
||||
/**
|
||||
* API 상태가 마지막으로 변경된 시각. 데이터가 없으면 empty.
|
||||
*/
|
||||
@Query("SELECT MAX(s.modifiedDate) FROM GwApiStatus s")
|
||||
Optional<LocalDateTime> findLastModifiedDate();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.gateway.data.statistics.entity.GwAuthClient;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
/**
|
||||
* TSEAIAU01(AGWAPP 인증 Client) 조회 — API 통계 org 스코핑용.
|
||||
* PTL_ORG.ID == TSEAIAU01.ORGID (1:N), TSEAIAU01.CLIENTID == API_STATS_*.CLIENT_ID.
|
||||
*/
|
||||
public interface GwAuthClientRepository extends JpaRepository<GwAuthClient, String> {
|
||||
|
||||
/**
|
||||
* org 소속 클라이언트 목록 (앱 선택 드롭다운용).
|
||||
*/
|
||||
List<GwAuthClient> findByOrgIdOrderByClientNameAsc(String orgId);
|
||||
|
||||
/**
|
||||
* org 소속 CLIENTID 목록 (통계 필터용).
|
||||
*/
|
||||
@Query("SELECT c.clientId FROM GwAuthClient c WHERE c.orgId = :orgId")
|
||||
List<String> findClientIdsByOrgId(@Param("orgId") String orgId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.eactive.apim.gateway.data.statistics.repository;
|
||||
|
||||
import com.eactive.apim.gateway.data.statistics.entity.GwSvcInfo;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* TSEAIHE01(AGWAPP 인터페이스 정보) 조회 — API_NAME(인터페이스 ID) → 표시명(EAISVCDESC) 매핑용.
|
||||
*/
|
||||
public interface GwSvcInfoRepository extends JpaRepository<GwSvcInfo, String> {
|
||||
|
||||
List<GwSvcInfo> findBySvcNameIn(Collection<String> svcNames);
|
||||
}
|
||||
@@ -22,6 +22,10 @@ public class PortalApplication extends SpringBootServletInitializer {
|
||||
|
||||
private static final Logger portal_logger = LoggerFactory.getLogger(PortalApplication.class);
|
||||
|
||||
public PortalApplication() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
portal_logger.info("##### PortalApplication Start #####");
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.eactive.apim.portal.apps;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Readiness probe.
|
||||
*
|
||||
* <p>Distinct from {@link HealthCheckController} (liveness — servlet alive?).
|
||||
* This endpoint validates both EMS and Gateway datasources via JDBC
|
||||
* {@code Connection.isValid(timeout)} to confirm the app is ready to serve
|
||||
* requests that depend on the database.
|
||||
*
|
||||
* <p>HTTP 200 + JSON when all checks pass.
|
||||
* <p>HTTP 503 + JSON when any check fails — body still includes the per-component
|
||||
* status so CI/CD logs the failing dependency.
|
||||
*/
|
||||
@RestController
|
||||
public class ReadinessController {
|
||||
|
||||
private static final int VALIDATION_TIMEOUT_SECONDS = 2;
|
||||
|
||||
private final DataSource portalDataSource;
|
||||
private final DataSource gatewayDataSource;
|
||||
|
||||
public ReadinessController(
|
||||
@Qualifier("portalDataSource") DataSource portalDataSource,
|
||||
@Qualifier("gatewayDataSource") DataSource gatewayDataSource) {
|
||||
this.portalDataSource = portalDataSource;
|
||||
this.gatewayDataSource = gatewayDataSource;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/health/ready", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<String> ready() {
|
||||
String ems = check(portalDataSource);
|
||||
String gw = check(gatewayDataSource);
|
||||
boolean ok = "UP".equals(ems) && "UP".equals(gw);
|
||||
|
||||
String body = "{"
|
||||
+ "\"status\":\"" + (ok ? "UP" : "DOWN") + "\","
|
||||
+ "\"ems\":\"" + ems + "\","
|
||||
+ "\"gateway\":\""+ gw + "\""
|
||||
+ "}";
|
||||
|
||||
return ResponseEntity.status(ok ? 200 : 503)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
private static String check(DataSource ds) {
|
||||
if (ds == null) return "DOWN:NO_DATASOURCE";
|
||||
try (Connection c = ds.getConnection()) {
|
||||
return c.isValid(VALIDATION_TIMEOUT_SECONDS) ? "UP" : "DOWN:INVALID";
|
||||
} catch (SQLException e) {
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage();
|
||||
return "DOWN:" + msg.replace('"', '\'').replace('\n', ' ').replace('\r', ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,11 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
@Controller
|
||||
@RequestMapping("/agreements")
|
||||
public class AgreementsController {
|
||||
public static final String TERMS_AGREEMENTS = "fragment/kjbank/terms_agreements";
|
||||
public static final String TERMS_AGREEMENTS = "fragment/djbank/terms_agreements";
|
||||
|
||||
/** 개인정보처리방침은 약관 페이지에서 분리되어 제주은행 공식 사이트 외부 링크로 대체됨 */
|
||||
public static final String PRIVACY_POLICY_EXTERNAL_URL =
|
||||
"https://www.jejubank.co.kr/hmpg/csct/secuCenr/ptctPlcy/procsPlcy/ctnt.do";
|
||||
|
||||
private final AgreementsFacade agreementsFacade;
|
||||
|
||||
@@ -28,28 +32,49 @@ public class AgreementsController {
|
||||
public String showTerms(@RequestParam(required = false) String tab,
|
||||
@RequestParam(required = false) String publishedOn,
|
||||
Model model) {
|
||||
String currentTab = tab != null ? tab : "terms";
|
||||
|
||||
// 구 개인정보처리방침 탭(tab=privacy)은 외부 링크로 이동했으므로 외부 URL로 리다이렉트(북마크 호환)
|
||||
if ("privacy".equals(currentTab)) {
|
||||
return "redirect:" + PRIVACY_POLICY_EXTERNAL_URL;
|
||||
}
|
||||
|
||||
AgreementType type;
|
||||
if ("privacy".equals(tab)) {
|
||||
type = AgreementType.PRIVACY_POLICY;
|
||||
} else {
|
||||
type = AgreementType.TERMS_OF_USE;
|
||||
switch (currentTab) {
|
||||
case "privacy-collect":
|
||||
// 개인정보수집동의서 = PRIVACY_COLLECT (신설항목)
|
||||
type = AgreementType.PRIVACY_COLLECT;
|
||||
break;
|
||||
case "notification":
|
||||
type = AgreementType.NOTIFICATION_CONSENT;
|
||||
break;
|
||||
case "terms":
|
||||
default:
|
||||
currentTab = "terms";
|
||||
type = AgreementType.TERMS_OF_USE;
|
||||
break;
|
||||
}
|
||||
|
||||
List<AgreementsDTO> agreementsList = agreementsFacade.getAgreementsList(String.valueOf(type));
|
||||
model.addAttribute("agreementsList", agreementsList);
|
||||
|
||||
AgreementsDTO selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
|
||||
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
|
||||
// 해당 약관 종류가 아직 시드되지 않았을 수 있으므로 빈 리스트 방어
|
||||
AgreementsDTO selectedAgreement = null;
|
||||
if (agreementsList != null && !agreementsList.isEmpty()) {
|
||||
selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
|
||||
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
|
||||
}
|
||||
|
||||
model.addAttribute("selectedAgreement", selectedAgreement);
|
||||
model.addAttribute("selectedDate", publishedOn);
|
||||
|
||||
model.addAttribute("isTermsOfUse", type == AgreementType.TERMS_OF_USE);
|
||||
model.addAttribute("isPrivacyPolicy", type == AgreementType.PRIVACY_POLICY);
|
||||
model.addAttribute("isPrivacyCollect", type == AgreementType.PRIVACY_COLLECT);
|
||||
model.addAttribute("isNotification", type == AgreementType.NOTIFICATION_CONSENT);
|
||||
model.addAttribute("agreementTitle", type.getDescription());
|
||||
model.addAttribute("agreementType", type.getCode());
|
||||
|
||||
model.addAttribute("currentTab", tab != null ? tab : "terms");
|
||||
model.addAttribute("currentTab", currentTab);
|
||||
|
||||
return TERMS_AGREEMENTS;
|
||||
}
|
||||
|
||||
@@ -71,8 +71,9 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
|
||||
}
|
||||
|
||||
// 2. 개인정보수집동의서 처리
|
||||
if (privacyCollectType != null && privacyCollectType.isPrivacyCollect()) {
|
||||
// 2. 개인정보수집동의서 처리 (개인정보수집동의서 = PRIVACY_COLLECT 신설항목 포함)
|
||||
if (privacyCollectType != null
|
||||
&& (privacyCollectType.isPrivacyCollect() || privacyCollectType == AgreementType.PRIVACY_COLLECT)) {
|
||||
Optional<Agreements> privacyCollect = agreementsService
|
||||
.findLastesBeforeDate(
|
||||
privacyCollectType,
|
||||
@@ -199,6 +200,10 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
||||
defaultAgreement.setName("개인정보처리방침");
|
||||
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||
break;
|
||||
case "PRIVACY_COLLECT":
|
||||
defaultAgreement.setName("개인정보수집동의서");
|
||||
defaultAgreement.setContents("현재 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||
break;
|
||||
case "PRIVACY_COLLECT_IND":
|
||||
defaultAgreement.setName("개인용 개인정보수집동의서");
|
||||
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusCatalogService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -30,6 +32,7 @@ public class ApiController {
|
||||
private final ApiService apiService;
|
||||
private final ApiServiceService apiServiceService;
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
private final ApiStatusCatalogService apiStatusCatalogService;
|
||||
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
|
||||
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
||||
|
||||
@@ -38,7 +41,24 @@ public class ApiController {
|
||||
if (id == null) {
|
||||
return "redirect:/apis/common";
|
||||
}
|
||||
populateDetailModel(id, model);
|
||||
model.addAttribute("activeTab", "api-info");
|
||||
return "apps/apis/mainApiDetail";
|
||||
}
|
||||
|
||||
// 테스트베드를 API 정보와 별도 URL로 분리(딥링크·북마크 가능). 미인증 사용자도 페이지 진입은
|
||||
// 허용하되, 실제 테스트베드(Swagger)는 인증 사용자에게만 렌더하고 미인증에는 로그인 안내를 노출한다.
|
||||
@GetMapping("/detail/testbed")
|
||||
public String apidetailTestbed(@RequestParam(value = "id", required = false) String id, ModelMap model) {
|
||||
if (id == null) {
|
||||
return "redirect:/apis/common";
|
||||
}
|
||||
populateDetailModel(id, model);
|
||||
model.addAttribute("activeTab", "testbed");
|
||||
return "apps/apis/mainApiDetail";
|
||||
}
|
||||
|
||||
private void populateDetailModel(String id, ModelMap model) {
|
||||
ApiSpecInfoDto api = apiService.selectDetail(id);
|
||||
if (api == null) {
|
||||
throw new NotFoundException(NOT_FOUND_MESSAGE);
|
||||
@@ -46,10 +66,16 @@ public class ApiController {
|
||||
|
||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(new ApiGroupSearch());
|
||||
|
||||
// 상세 타이틀에 노출할 현재 API의 그룹명 세팅(selectDetail은 apiGroupName을 채우지 않음)
|
||||
ApiServiceDTO apiGroup = apiServiceService.findApiServiceByApiId(id);
|
||||
if (apiGroup != null) {
|
||||
api.setApiGroupName(apiGroup.getGroupName());
|
||||
}
|
||||
|
||||
model.addAttribute("apiSpecInfo", api);
|
||||
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
||||
model.addAttribute("services", searchResult.get("services"));
|
||||
return "apps/apis/mainApiDetail";
|
||||
model.addAttribute("authenticated", SecurityUtil.isAuthenticated());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -62,12 +88,18 @@ public class ApiController {
|
||||
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
||||
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
|
||||
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
|
||||
// 카드의 현재 상태 태그 노출 여부 (PTL_PROPERTY djb.apistatus.api-list-status-badge)
|
||||
model.addAttribute("apiStatusBadgeEnabled", apiStatusCatalogService.isApiListStatusBadgeEnabled());
|
||||
|
||||
return "apps/apis/mainApiList";
|
||||
}
|
||||
|
||||
@GetMapping("/testbed/api")
|
||||
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
||||
if (!SecurityUtil.isAuthenticated()) {
|
||||
return "redirect:/login?reason=auth";
|
||||
}
|
||||
String selectedApiServiceName = "API 서비스 선택";
|
||||
String selectedServiceId = "";
|
||||
boolean idExists = false;
|
||||
@@ -93,6 +125,10 @@ public class ApiController {
|
||||
|
||||
@GetMapping("/testbed")
|
||||
public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
||||
if (!SecurityUtil.isAuthenticated()) {
|
||||
return "redirect:/login?reason=auth";
|
||||
}
|
||||
String selectedApiServiceName = "API 서비스 선택";
|
||||
boolean idExists = false;
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ package com.eactive.apim.portal.apps.apis.controller;
|
||||
|
||||
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiSpecService;
|
||||
import com.eactive.apim.portal.djb.testbed.service.DjbTestbedSpecServerRewriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
@@ -22,35 +22,46 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/apis")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TestbedSpecController {
|
||||
|
||||
@Autowired
|
||||
private ApiSpecInfoService apiSpecInfoService;
|
||||
private final ApiSpecInfoService apiSpecInfoService;
|
||||
private final DjbTestbedSpecServerRewriter serverRewriter;
|
||||
|
||||
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
|
||||
private static final String DEFAULT_SPEC_PATH = "swagger/default-token-api-spec.json";
|
||||
|
||||
@GetMapping(value = "/{id}/swagger.json", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<String> getSwagger(@PathVariable String id) {
|
||||
public ResponseEntity<String> getSwagger(@PathVariable String id, HttpServletRequest request) {
|
||||
String json = buildSpecJson(id, request);
|
||||
return json == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(json);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/{id}/swagger.yaml", produces = "application/x-yaml")
|
||||
public ResponseEntity<String> getSwaggerYaml(@PathVariable String id, HttpServletRequest request) {
|
||||
String json = buildSpecJson(id, request);
|
||||
return json == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(serverRewriter.toYaml(json));
|
||||
}
|
||||
|
||||
/** default 토큰 spec 또는 저장 spec(서버 → GW 주소 치환)을 JSON 으로 반환. 없으면 null. */
|
||||
private String buildSpecJson(String id, HttpServletRequest request) {
|
||||
if (DEFAULT_TOKEN_API_ID.equals(id)) {
|
||||
try {
|
||||
// 기본 토큰 spec 은 서버 치환 대상이 아니다. path 가 포탈 mock 토큰 경로라
|
||||
// GW 호스트를 붙이면 실재하지 않는 주소가 된다(servers 없음 → 문서 origin 사용).
|
||||
Resource resource = new ClassPathResource(DEFAULT_SPEC_PATH);
|
||||
String content = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
|
||||
return ResponseEntity.ok().body(content);
|
||||
return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read default token api spec file", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<ApiSpecInfo> spec = apiSpecInfoService.findById(id);
|
||||
|
||||
if (!spec.isPresent()) {
|
||||
StringUtils.hasText(spec.get().getTestbedSpec());
|
||||
if (!spec.isPresent() || !StringUtils.hasText(spec.get().getTestbedSpec())) {
|
||||
return null;
|
||||
}
|
||||
return ResponseEntity.ok().body(spec.get().getTestbedSpec());
|
||||
|
||||
return serverRewriter.rewriteServerToGateway(spec.get().getTestbedSpec(), request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.eactive.apim.portal.apps.apis.filter;
|
||||
|
||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||
import com.eactive.apim.portal.djb.testbed.config.DjbTestbedGatewayProperty;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -10,6 +12,9 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,15 +25,85 @@ public class APISender {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(APISender.class);
|
||||
|
||||
public String requestPost(String uri, String requestBody) throws IOException {
|
||||
/** 응답 charset 을 Content-Type 에서 얻지 못할 때 사용할 기본값. */
|
||||
private static final String DEFAULT_CHARSET = "UTF-8";
|
||||
|
||||
/**
|
||||
* 프록시 대상(GW/mock) 응답을 상태코드까지 포함해 전달하기 위한 홀더.
|
||||
*
|
||||
* <p>본문만 반환하면 대상이 4xx/5xx 를 내려도 호출측이 200 으로 되돌려주게 되므로
|
||||
* 상태코드와 Content-Type 을 함께 담는다.</p>
|
||||
*/
|
||||
public static class ApiResponse {
|
||||
|
||||
private final int status;
|
||||
private final String body;
|
||||
private final String contentType;
|
||||
private final Map<String, List<String>> headers;
|
||||
|
||||
public ApiResponse(int status, String body, String contentType, Map<String, List<String>> headers) {
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
this.contentType = contentType;
|
||||
this.headers = headers == null
|
||||
? Collections.<String, List<String>>emptyMap()
|
||||
: Collections.unmodifiableMap(headers);
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 대상 응답의 Content-Type 원본 (없으면 null). */
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/** 대상 응답 헤더 (상태줄 의사헤더인 null 키는 제외). 값은 헤더당 복수 가능. */
|
||||
public Map<String, List<String>> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApiResponse{status=" + status + ", contentType=" + contentType
|
||||
+ ", bodyLen=" + (body == null ? 0 : body.length())
|
||||
+ ", headers=" + headers.keySet() + "}";
|
||||
}
|
||||
}
|
||||
|
||||
// 테스트베드 프록시 연결/응답 타임아웃 — DjbTestbedGatewayProperty(djb.gateway.timeout, 단위: 초) 단일 기준.
|
||||
private final DjbTestbedGatewayProperty gatewayProperty;
|
||||
|
||||
public APISender(DjbTestbedGatewayProperty gatewayProperty) {
|
||||
this.gatewayProperty = gatewayProperty;
|
||||
}
|
||||
|
||||
/** connect/read 타임아웃 적용. 반드시 connect(getOutputStream/getResponseCode) 이전에 호출. */
|
||||
private void applyTimeout(HttpURLConnection connection) {
|
||||
int t = gatewayProperty.timeoutMillis();
|
||||
connection.setConnectTimeout(t);
|
||||
connection.setReadTimeout(t);
|
||||
}
|
||||
|
||||
public ApiResponse requestPost(String uri, String requestBody) throws IOException {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("APISender POST(json) 요청 - uri={}, bodyLen={}, body={}",
|
||||
uri, requestBody == null ? 0 : requestBody.length(), StringMaskingUtil.maskFormBody(requestBody));
|
||||
}
|
||||
|
||||
HttpURLConnection connection = getHttpURLConnection(uri, requestBody);
|
||||
|
||||
String response = getResponse(connection);
|
||||
ApiResponse response = getResponse(connection);
|
||||
|
||||
connection.disconnect();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(response);
|
||||
logger.debug("APISender POST(json) 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -54,10 +129,11 @@ public class APISender {
|
||||
return uriBuilder.toString();
|
||||
}
|
||||
|
||||
public String requestGet(String uri, Map<String, String> headers, Map<String, String[]> params) throws IOException {
|
||||
public ApiResponse requestGet(String uri, Map<String, String> headers, Map<String, String[]> params) throws IOException {
|
||||
|
||||
URL endpoint = new URL(appendUriAndParams(uri, params));
|
||||
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
|
||||
applyTimeout(connection);
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
@@ -69,19 +145,24 @@ public class APISender {
|
||||
}
|
||||
connection.setDoOutput(true);
|
||||
|
||||
String response = getResponse(connection);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("APISender GET 요청 - uri={}", appendUriAndParams(uri, params));
|
||||
}
|
||||
|
||||
ApiResponse response = getResponse(connection);
|
||||
connection.disconnect();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(response);
|
||||
logger.debug("APISender GET 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public String requestPost(String uri, Map<String, String> headers, Map<String, String[]> params, String requestBody) throws IOException {
|
||||
public ApiResponse requestPost(String uri, Map<String, String> headers, Map<String, String[]> params, String requestBody) throws IOException {
|
||||
|
||||
URL endpoint = new URL(appendUriAndParams(uri, params));
|
||||
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
|
||||
applyTimeout(connection);
|
||||
connection.setRequestMethod("POST");
|
||||
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
@@ -93,42 +174,86 @@ public class APISender {
|
||||
}
|
||||
connection.setDoOutput(true);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
// client_secret 등 민감 파라미터는 마스킹. body 비어있으면 상위에서 본문 전송 유실.
|
||||
logger.debug("APISender POST 요청 - uri={}, bodyLen={}, body={}",
|
||||
uri, requestBody == null ? 0 : requestBody.length(), StringMaskingUtil.maskFormBody(requestBody));
|
||||
}
|
||||
|
||||
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
|
||||
byte[] requestBodyBytes = requestBody.getBytes(StandardCharsets.UTF_8);
|
||||
outputStream.write(requestBodyBytes);
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
String response = getResponse(connection);
|
||||
ApiResponse response = getResponse(connection);
|
||||
connection.disconnect();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(response);
|
||||
logger.debug("APISender POST 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
private static String getResponse(HttpURLConnection connection) throws IOException {
|
||||
/**
|
||||
* 대상 응답을 상태코드·Content-Type·본문으로 읽는다.
|
||||
*
|
||||
* <p>4xx/5xx 는 {@code getInputStream()} 이 IOException 을 던지므로 errorStream 으로 본문을 읽고,
|
||||
* 본문이 아예 없는 응답(errorStream == null)은 빈 문자열로 처리한다.</p>
|
||||
*/
|
||||
private static ApiResponse getResponse(HttpURLConnection connection) throws IOException {
|
||||
int responseCode = connection.getResponseCode();
|
||||
String contentType = connection.getContentType();
|
||||
StringBuilder response = new StringBuilder();
|
||||
|
||||
try (InputStream stream = (responseCode < 400) ? connection.getInputStream() : connection.getErrorStream(); InputStreamReader isr = new InputStreamReader(stream);
|
||||
BufferedReader reader = new BufferedReader(isr)) {
|
||||
InputStream stream = (responseCode < 400) ? connection.getInputStream() : connection.getErrorStream();
|
||||
if (stream != null) {
|
||||
try (InputStreamReader isr = new InputStreamReader(stream, charsetOf(contentType));
|
||||
BufferedReader reader = new BufferedReader(isr)) {
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
return new ApiResponse(responseCode, response.toString(), contentType, copyHeaders(connection));
|
||||
}
|
||||
|
||||
private static HttpURLConnection getHttpURLConnection(String uri, String requestBody) throws IOException {
|
||||
/** 대상 응답 헤더 복사. {@code getHeaderFields()} 의 null 키(상태줄)는 제외. */
|
||||
private static Map<String, List<String>> copyHeaders(HttpURLConnection connection) {
|
||||
Map<String, List<String>> headers = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
|
||||
if (entry.getKey() != null) {
|
||||
headers.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Content-Type 의 charset 파라미터를 파싱. 없거나 인식 불가면 UTF-8. */
|
||||
private static String charsetOf(String contentType) {
|
||||
if (contentType != null) {
|
||||
for (String part : contentType.split(";")) {
|
||||
String token = part.trim();
|
||||
if (token.toLowerCase().startsWith("charset=")) {
|
||||
String charset = token.substring("charset=".length()).replace("\"", "").trim();
|
||||
if (!charset.isEmpty() && java.nio.charset.Charset.isSupported(charset)) {
|
||||
return charset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return DEFAULT_CHARSET;
|
||||
}
|
||||
|
||||
private HttpURLConnection getHttpURLConnection(String uri, String requestBody) throws IOException {
|
||||
URL endpoint = new URL(uri);
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
|
||||
applyTimeout(connection);
|
||||
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.eactive.apim.portal.apps.apis.filter;
|
||||
|
||||
import com.eactive.apim.portal.common.util.HttpRequestUtil;
|
||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
/**
|
||||
* API 테스트베드(/api/call-api) 감사(audit) 로그 기록기.
|
||||
*
|
||||
* <p>logback 의 {@code eapim.portal.apitester.audit} 로거(전용 파일, 1년 보관)로 기록한다.
|
||||
* 요청 1건당 REQ/RES 두 줄을 같은 auditId 로 남긴다.</p>
|
||||
*
|
||||
* <p>마스킹 정책:</p>
|
||||
* <ul>
|
||||
* <li>secret 계열 키(client_secret, password, api_key, authorization 등)의 값은 전체 마스킹</li>
|
||||
* <li>그 외 파라미터/JSON 값은 앞 일부만 남기고 마스킹</li>
|
||||
* <li>JSON 이 아닌 본문은 전체 길이(byte)와 앞 {@value #NON_JSON_PREVIEW_LENGTH}글자만 남기고 마스킹</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class ApiTesterAuditLogger {
|
||||
|
||||
private static final Logger auditLogger = LoggerFactory.getLogger("eapim.portal.apitester.audit");
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/** 값 전체를 마스킹할 키(소문자 비교) */
|
||||
private static final Set<String> SECRET_KEYS = new HashSet<>(Arrays.asList(
|
||||
"client_secret", "clientsecret", "secret", "password", "passwd", "pwd",
|
||||
"api_key", "apikey", "access_token", "refresh_token", "authorization"));
|
||||
|
||||
/** 감사 로그에 남길 주요 요청 헤더 화이트리스트 */
|
||||
private static final String[] AUDIT_HEADERS = {
|
||||
"content-type", "accept", "referer", "origin", "x-forwarded-for",
|
||||
"original-api-id", "authorization"};
|
||||
|
||||
/** 마스킹된 JSON 본문 로그 최대 길이(초과분 절단) — 대용량 본문의 로그 파일 비대화 방지 */
|
||||
private static final int JSON_LOG_MAX_LENGTH = 2000;
|
||||
|
||||
/** JSON 이 아닌 본문의 노출 프리뷰 글자 수 */
|
||||
private static final int NON_JSON_PREVIEW_LENGTH = 8;
|
||||
|
||||
private ApiTesterAuditLogger() {
|
||||
}
|
||||
|
||||
/** REQ/RES 두 줄을 연결하는 짧은 감사 ID */
|
||||
public static String newAuditId() {
|
||||
return UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 수신 시점 기록. 감사 로그 실패가 프록시 동작을 막지 않도록 예외는 삼킨다.
|
||||
*
|
||||
* @param targetUrl original-url 헤더 값 (없으면 null)
|
||||
* @param gatewayMode 게이트웨이 모드명 (판별 전이면 "-")
|
||||
* @param tokenRequest OAuth 토큰 발급 요청 여부
|
||||
* @param body 이미 읽어 둔 요청 본문 (없으면 null/빈 문자열)
|
||||
*/
|
||||
public static void logRequest(String auditId, HttpServletRequest request, String targetUrl,
|
||||
String gatewayMode, boolean tokenRequest, String body) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
sb.append("REQ [").append(auditId).append(']');
|
||||
sb.append(" ip=").append(HttpRequestUtil.getClientIpAddress(request));
|
||||
sb.append(" proxied=").append(HttpRequestUtil.isProxied(request));
|
||||
sb.append(" user=").append(currentUser());
|
||||
sb.append(" method=").append(request.getMethod());
|
||||
sb.append(" mode=").append(gatewayMode);
|
||||
sb.append(" token=").append(tokenRequest);
|
||||
sb.append(" target=").append(targetUrl == null ? "-" : maskQueryValues(sanitize(targetUrl)));
|
||||
sb.append(" ua=\"").append(sanitize(request.getHeader("User-Agent"))).append('"');
|
||||
sb.append(" headers=").append(buildHeaderSummary(request));
|
||||
sb.append(" body=").append(buildBodySummary(request.getContentType(), tokenRequest, body));
|
||||
auditLogger.info(sb.toString());
|
||||
} catch (Exception e) {
|
||||
auditLogger.warn("REQ [{}] 감사 로그 기록 실패: {}", auditId, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** 처리 완료 시점 기록. type 은 처리 분기(TOKEN_GW/TOKEN_MOCK/SAMPLE/GW/MOCK 등). */
|
||||
public static void logResult(String auditId, int status, String type, long elapsedMillis) {
|
||||
auditLogger.info("RES [{}] status={} type={} elapsedMs={}", auditId, status, type, elapsedMillis);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 요청 정보 구성
|
||||
// =========================================================================
|
||||
|
||||
/** 로그인 사용자 식별자(마스킹). 미인증이면 anonymous. */
|
||||
private static String currentUser() {
|
||||
try {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated() || "anonymousUser".equals(auth.getName())) {
|
||||
return "anonymous";
|
||||
}
|
||||
String name = auth.getName();
|
||||
return name.contains("@") ? StringMaskingUtil.maskEmail(name) : partialMask(name);
|
||||
} catch (Exception e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** 화이트리스트 헤더만 {k:"v"} 형태로 요약. secret 계열 헤더 값은 마스킹. */
|
||||
private static String buildHeaderSummary(HttpServletRequest request) {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (String name : AUDIT_HEADERS) {
|
||||
String value = request.getHeader(name);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
sb.append(", ");
|
||||
}
|
||||
first = false;
|
||||
sb.append(name).append(":\"").append(maskHeaderValue(name, sanitize(value))).append('"');
|
||||
}
|
||||
return sb.append('}').toString();
|
||||
}
|
||||
|
||||
/** Authorization 등 인증 헤더는 스킴만 남기고 토큰부 마스킹. */
|
||||
private static String maskHeaderValue(String name, String value) {
|
||||
if (!SECRET_KEYS.contains(name.toLowerCase())) {
|
||||
return value;
|
||||
}
|
||||
int space = value.indexOf(' ');
|
||||
if (space > 0) {
|
||||
return value.substring(0, space) + " " + partialMask(value.substring(space + 1).trim());
|
||||
}
|
||||
return partialMask(value);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 본문 마스킹
|
||||
// =========================================================================
|
||||
|
||||
private static String buildBodySummary(String contentType, boolean tokenRequest, String body) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
// 토큰 발급: form 필드 단위 마스킹 (client_secret 전체 마스킹)
|
||||
if (tokenRequest) {
|
||||
return "\"" + maskFormBody(body) + "\"";
|
||||
}
|
||||
// 일반 요청: JSON 이면 값 단위 부분 마스킹, 그 외(비 JSON)는 길이 + 프리뷰만
|
||||
if (contentType != null && contentType.toLowerCase().contains("json")) {
|
||||
String maskedJson = tryMaskJson(body);
|
||||
if (maskedJson != null) {
|
||||
return maskedJson;
|
||||
}
|
||||
}
|
||||
return nonJsonSummary(body);
|
||||
}
|
||||
|
||||
/** k=v&k=v 형태 본문의 값 단위 마스킹. secret 키는 전체 마스킹. */
|
||||
private static String maskFormBody(String body) {
|
||||
StringBuilder sb = new StringBuilder(body.length());
|
||||
String[] pairs = body.split("&");
|
||||
for (int i = 0; i < pairs.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append('&');
|
||||
}
|
||||
int eq = pairs[i].indexOf('=');
|
||||
if (eq < 0) {
|
||||
sb.append(partialMask(pairs[i]));
|
||||
continue;
|
||||
}
|
||||
String key = pairs[i].substring(0, eq);
|
||||
String value = pairs[i].substring(eq + 1);
|
||||
sb.append(key).append('=');
|
||||
sb.append(SECRET_KEYS.contains(key.toLowerCase()) ? "*****" : partialMask(value));
|
||||
}
|
||||
return sanitize(sb.toString());
|
||||
}
|
||||
|
||||
/** URL 쿼리스트링 값 단위 마스킹 (경로는 그대로). */
|
||||
private static String maskQueryValues(String url) {
|
||||
int qs = url.indexOf('?');
|
||||
if (qs < 0) {
|
||||
return url;
|
||||
}
|
||||
return url.substring(0, qs) + "?" + maskFormBody(url.substring(qs + 1));
|
||||
}
|
||||
|
||||
/** JSON 파싱 성공 시 값 단위 마스킹 문자열, 실패 시 null. */
|
||||
private static String tryMaskJson(String body) {
|
||||
try {
|
||||
JsonNode masked = maskJsonNode(OBJECT_MAPPER.readTree(body));
|
||||
String out = OBJECT_MAPPER.writeValueAsString(masked);
|
||||
if (out.length() > JSON_LOG_MAX_LENGTH) {
|
||||
out = out.substring(0, JSON_LOG_MAX_LENGTH) + "...(truncated)";
|
||||
}
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** JSON 트리의 leaf 값을 재귀적으로 마스킹. secret 키 필드는 전체 마스킹. */
|
||||
private static JsonNode maskJsonNode(JsonNode node) {
|
||||
if (node.isObject()) {
|
||||
ObjectNode obj = (ObjectNode) node;
|
||||
Iterator<String> names = obj.fieldNames();
|
||||
Set<String> fieldNames = new HashSet<>();
|
||||
while (names.hasNext()) {
|
||||
fieldNames.add(names.next());
|
||||
}
|
||||
for (String field : fieldNames) {
|
||||
if (SECRET_KEYS.contains(field.toLowerCase())) {
|
||||
obj.set(field, TextNode.valueOf("*****"));
|
||||
} else {
|
||||
obj.set(field, maskJsonNode(obj.get(field)));
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (node.isArray()) {
|
||||
ArrayNode arr = (ArrayNode) node;
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
arr.set(i, maskJsonNode(arr.get(i)));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
if (node.isNull() || node.isMissingNode()) {
|
||||
return node;
|
||||
}
|
||||
return TextNode.valueOf(partialMask(node.asText()));
|
||||
}
|
||||
|
||||
/** 비 JSON 본문: 전체 길이(byte)와 앞 몇 글자만 노출. */
|
||||
private static String nonJsonSummary(String body) {
|
||||
int bytes = body.getBytes(StandardCharsets.UTF_8).length;
|
||||
String preview = body.length() <= NON_JSON_PREVIEW_LENGTH
|
||||
? body : body.substring(0, NON_JSON_PREVIEW_LENGTH);
|
||||
return "(non-json,bytes=" + bytes + ",preview=\"" + sanitize(preview) + "***\")";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 공통 helper
|
||||
// =========================================================================
|
||||
|
||||
/** 앞 일부(최대 4자)만 남기고 마스킹. 2자 이하는 전체 마스킹. */
|
||||
private static String partialMask(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int len = value.length();
|
||||
if (len <= 2) {
|
||||
return stars(len);
|
||||
}
|
||||
int visible = Math.min(4, Math.max(1, len / 3));
|
||||
return value.substring(0, visible) + "***";
|
||||
}
|
||||
|
||||
private static String stars(int count) {
|
||||
char[] arr = new char[count];
|
||||
Arrays.fill(arr, '*');
|
||||
return new String(arr);
|
||||
}
|
||||
|
||||
/** 제어문자·개행·따옴표를 치환해 한 줄 로그 형식을 보존. */
|
||||
private static String sanitize(String value) {
|
||||
if (value == null) {
|
||||
return "-";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(value.length());
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '"') {
|
||||
sb.append('\'');
|
||||
} else if (c == '\r' || c == '\n' || c == '\t') {
|
||||
sb.append(' ');
|
||||
} else if (c < 0x20) {
|
||||
sb.append('?');
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||