Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1d6181210 | |||
| bac9a54dee | |||
| 603cba65a7 | |||
| 889d77b990 | |||
| bf2dad6ba7 | |||
| 3a1b0cb9c1 | |||
| a13279a777 | |||
| 5299352235 | |||
| 8b4bc425ec | |||
| 06dc148515 | |||
| 3cfd6b0b04 | |||
| 7a0f88bfb1 | |||
| 6d7b292b8a | |||
| 20ad6f8394 | |||
| 2bae0e350c | |||
| 45e8fa2903 | |||
| 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
|
# Ignore Gradle build output directory
|
||||||
build
|
build
|
||||||
|
|
||||||
|
# SonarScanner 작업 디렉터리
|
||||||
|
.scannerwork/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build_knbank.sh
|
build_knbank.sh
|
||||||
@@ -107,4 +110,6 @@ TODO.txt
|
|||||||
diff
|
diff
|
||||||
|
|
||||||
*rinjae*
|
*rinjae*
|
||||||
*gf63*
|
*gf63*
|
||||||
|
*obsidian*
|
||||||
|
design-backup
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
**기술 스택:**
|
**기술 스택:**
|
||||||
- Spring Boot 2.7.18 with Spring MVC and Thymeleaf
|
- Spring Boot 2.7.18 with Spring MVC and Thymeleaf
|
||||||
- Java 8 (targetCompatibility: 1.8)
|
- Java 8 (sourceCompatibility/targetCompatibility: 1.8)
|
||||||
- Gradle 8.7 빌드 시스템
|
- Gradle 8.7 빌드 시스템
|
||||||
- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스)
|
- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스)
|
||||||
- Spring Security (커스텀 인증)
|
- Spring Security (커스텀 인증)
|
||||||
@@ -59,7 +59,7 @@ git push origin jenkins_with_weblogic
|
|||||||
|
|
||||||
### 애플리케이션 실행
|
### 애플리케이션 실행
|
||||||
- gradlew 사용 금지 - offline gradle 단독 실행
|
- 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
|
```bash
|
||||||
# dev 프로파일로 실행 (기본값)
|
# dev 프로파일로 실행 (기본값)
|
||||||
@@ -173,20 +173,21 @@ com.eactive.apim.portal/
|
|||||||
|
|
||||||
1. **EMS Database** (Portal/Admin)
|
1. **EMS Database** (Portal/Admin)
|
||||||
- JNDI: `jdbc/dsOBP_EMS`
|
- JNDI: `jdbc/dsOBP_EMS`
|
||||||
- Schema: `EMSADM`
|
- Schema: `EMSAPP`
|
||||||
- Entities: `com.eactive.apim.portal.apps.*`
|
- Entities: `com.eactive.apim.portal.apps.*`
|
||||||
- 목적: 포털 사용자, 조직, API 키, 승인
|
- 목적: 포털 사용자, 조직, API 키, 승인
|
||||||
|
|
||||||
2. **Gateway Database** (API Specs)
|
2. **Gateway Database** (API Specs)
|
||||||
- JNDI: `jdbc/dsOBP_AGW`
|
- JNDI: `jdbc/dsOBP_AGW`
|
||||||
- Schema: `AGWADM`
|
- Schema: `AGWAPP`
|
||||||
- Entities: `com.eactive.eai.data.entity.onl.*`
|
- Entities: `com.eactive.eai.data.entity.onl.*`
|
||||||
- 목적: API 명세, 서비스, 메시지
|
- 목적: API 명세, 서비스, 메시지
|
||||||
|
|
||||||
설정: `config/PortalDatasourceConfiguration.java`
|
설정: `config/PortalDatasourceConfiguration.java`
|
||||||
- 각 데이터베이스별 별도 EntityManager
|
- 각 데이터베이스별 별도 EntityManager
|
||||||
- Atomikos JTA를 통한 분산 트랜잭션
|
- **JTA/XA 미사용**. EntityManagerFactory 별 로컬 트랜잭션 (`config/PortalConfigTransaction.java`)
|
||||||
- 트랜잭션 로그: `/Log/eapim/portal/`
|
- `transactionManager` (@Primary) → EMS
|
||||||
|
- `gatewayTransactionManager` → Gateway
|
||||||
|
|
||||||
### 설정 프로파일
|
### 설정 프로파일
|
||||||
|
|
||||||
@@ -265,6 +266,41 @@ API 키는 승인 워크플로우와 함께 관리됩니다:
|
|||||||
8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가
|
8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가
|
||||||
9. `application.yml`의 `pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑)
|
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 매핑
|
### MapStruct DTO 매핑
|
||||||
|
|
||||||
타입 안전한 DTO 변환을 위해 MapStruct 사용:
|
타입 안전한 DTO 변환을 위해 MapStruct 사용:
|
||||||
@@ -324,7 +360,13 @@ return queryFactory.selectFrom(user)
|
|||||||
적절한 propagation과 함께 `@Transactional` 사용:
|
적절한 propagation과 함께 `@Transactional` 사용:
|
||||||
- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성)
|
- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성)
|
||||||
- 읽기 전용 작업: 최적화를 위해 `@Transactional(readOnly = true)`
|
- 읽기 전용 작업: 최적화를 위해 `@Transactional(readOnly = true)`
|
||||||
- 다중 데이터베이스: Atomikos JTA가 자동으로 분산 트랜잭션 처리
|
- **다중 데이터베이스: 분산 트랜잭션(JTA/XA) 없음.** EMS·Gateway 각각 독립 로컬 트랜잭션이다.
|
||||||
|
- 무지정 `@Transactional` = EMS(`transactionManager`)
|
||||||
|
- **Gateway 엔티티(`com.eactive.apim.gateway.*`, `com.eactive.eai.data.entity.onl.*`)를 다루는 서비스는
|
||||||
|
`@Transactional("gatewayTransactionManager")` 를 명시**한다. 안 하면 게이트웨이 EntityManager 가
|
||||||
|
리포지토리 호출 단위로 닫혀 지연 로딩에서 `LazyInitializationException` 이 난다
|
||||||
|
(예: `ApiServiceService` — `ApiGroup.apiGroupApiList`).
|
||||||
|
- 두 DB 를 한 원자 단위로 묶어야 하는 작업은 만들지 않는다. 현재 Gateway 는 조회 전용이다.
|
||||||
|
|
||||||
### 에러 처리
|
### 에러 처리
|
||||||
|
|
||||||
|
|||||||
@@ -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,501 @@
|
|||||||
|
// 보안 검사 통합 파이프라인 — OWASP Dependency-Check(SCA) + SonarQube(SAST).
|
||||||
|
//
|
||||||
|
// 두 검사는 보는 대상이 다르다. 하나만 돌리면 절반만 본다.
|
||||||
|
// SonarQube : 우리가 쓴 코드의 결함 (SQL Injection, XSS, 하드코딩 시크릿 …)
|
||||||
|
// Dependency-Check : 우리가 쓰는 라이브러리의 알려진 취약점 (CVE)
|
||||||
|
//
|
||||||
|
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록만 고치면 된다)
|
||||||
|
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
|
||||||
|
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록 (없으면 PATH 탐색)
|
||||||
|
// 3) 노드에 Dependency-Check CLI 설치 (DC_HOME) — 설치 방법은 17-보안 가이드 문서 참고
|
||||||
|
// 4) NVD API Key 를 Secret text credential 로 등록 (기본 ID: nvd-api-key)
|
||||||
|
// 키가 없으면 NVD 갱신이 극심하게 느려진다(수 시간). 폐쇄망은 UPDATE_NVD=false 로 운영.
|
||||||
|
// 5) Quality Gate 대기를 쓰려면 SonarQube 에 <JENKINS_URL>/sonarqube-webhook/ 웹훅 등록
|
||||||
|
//
|
||||||
|
// JDK 분리: 빌드는 JDK 8, 스캐너/Dependency-Check 실행은 JDK 17.
|
||||||
|
// (SonarScanner CLI 5.x 는 JRE 17 필수, Dependency-Check 9.x 이상은 JRE 11 이상 필수)
|
||||||
|
pipeline {
|
||||||
|
agent { label 'djb-vm' }
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
// 코드 변경 감지
|
||||||
|
pollSCM('H/30 * * * *')
|
||||||
|
// 코드가 그대로여도 새 CVE 는 계속 공개된다 → 야간 정기 재검사
|
||||||
|
cron('H 3 * * *')
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
booleanParam(
|
||||||
|
name: 'RUN_DEPENDENCY_CHECK',
|
||||||
|
defaultValue: true,
|
||||||
|
description: 'OWASP Dependency-Check(SCA) 실행'
|
||||||
|
)
|
||||||
|
booleanParam(
|
||||||
|
name: 'RUN_SONAR',
|
||||||
|
defaultValue: true,
|
||||||
|
description: 'SonarQube 정적 분석(SAST) 실행'
|
||||||
|
)
|
||||||
|
booleanParam(
|
||||||
|
name: 'RUN_TESTS',
|
||||||
|
defaultValue: false,
|
||||||
|
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
|
||||||
|
)
|
||||||
|
booleanParam(
|
||||||
|
name: 'UPDATE_NVD',
|
||||||
|
defaultValue: true,
|
||||||
|
description: 'NVD 취약점 DB 갱신. 폐쇄망(외부 인터넷 불가)이면 반드시 끈다 — 캐시된 DB 로만 검사한다.'
|
||||||
|
)
|
||||||
|
string(
|
||||||
|
name: 'FAIL_ON_CVSS',
|
||||||
|
defaultValue: '11',
|
||||||
|
description: '이 CVSS 점수 이상이면 검사 실패로 표시. 11 = 실패시키지 않음(리포트만). 예: 7 = High 이상'
|
||||||
|
)
|
||||||
|
booleanParam(
|
||||||
|
name: 'FAIL_BUILD_ON_FINDING',
|
||||||
|
defaultValue: false,
|
||||||
|
description: 'FAIL_ON_CVSS 위반 시 빌드를 FAILURE 로 만든다. 끄면 UNSTABLE 로만 표시.'
|
||||||
|
)
|
||||||
|
booleanParam(
|
||||||
|
name: 'SCAN_JS',
|
||||||
|
defaultValue: true,
|
||||||
|
description: '정적 JS 라이브러리(static/js, static/plugins)도 RetireJS 로 검사. 폐쇄망에서 오류나면 끈다.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
|
||||||
|
SONAR_ENV_NAME = 'SonarQube-Community'
|
||||||
|
SONAR_SCANNER_TOOL = ''
|
||||||
|
// NVD API Key 를 담은 Secret text credential ID. 없으면 키 없이 진행한다(느림).
|
||||||
|
NVD_API_KEY_CRED = 'nvd-api-key'
|
||||||
|
|
||||||
|
// --- 툴체인 ---
|
||||||
|
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
|
||||||
|
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner / dependency-check 실행용
|
||||||
|
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'
|
||||||
|
|
||||||
|
// --- Dependency-Check ---
|
||||||
|
// CLI 설치 경로. 비어 있거나 없으면 Jenkins Tools 등록분 > PATH 순으로 탐색한다.
|
||||||
|
DC_HOME = '/apps/opts/dependency-check'
|
||||||
|
// NVD 캐시(H2 DB). 워크스페이스 밖에 두어야 빌드마다 1GB 이상을 다시 받지 않는다.
|
||||||
|
// 여러 Job 이 공유하므로 disableConcurrentBuilds 를 켠 채로 쓴다.
|
||||||
|
DC_DATA = '/apps/opts/dependency-check-data'
|
||||||
|
DC_TOOL_NAME = ''
|
||||||
|
DC_REPORT_DIR = 'build/reports/dependency-check'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
|
||||||
|
// 이 둘이 없으면 컴파일도 의존성 해석도 불가능하다.
|
||||||
|
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/dependency-check JDK ---"
|
||||||
|
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
|
||||||
|
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
|
||||||
|
echo "SonarScanner 는 JRE 17, Dependency-Check 는 JRE 11 이상을 요구한다."
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배포본에 실리는 것과 같은 목록(runtimeClasspath)만 모은다.
|
||||||
|
// 이게 없으면 Dependency-Check 가 스캔할 jar 가 없어 "취약점 0건" 이라는 거짓 안심을 준다.
|
||||||
|
stage('Export dependency jars') {
|
||||||
|
when { expression { return params.RUN_DEPENDENCY_CHECK } }
|
||||||
|
steps {
|
||||||
|
sh 'gradle -I ci/dependency-check-classpath.gradle exportDependencyJars --no-daemon'
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
COUNT=$(find build/dependency-check/libs -name '*.jar' -type f | wc -l | tr -d ' ')
|
||||||
|
echo "scan target jars: $COUNT"
|
||||||
|
if [ "$COUNT" -eq 0 ]; then
|
||||||
|
echo "수집된 jar 가 0개다. 의존성 해석이 실패했는지 확인할 것."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('OWASP Dependency-Check') {
|
||||||
|
when { expression { return params.RUN_DEPENDENCY_CHECK } }
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
// CLI 위치 결정: DC_HOME > Jenkins Tools 등록분 > PATH
|
||||||
|
def dcHome = null
|
||||||
|
if (env.DC_HOME?.trim() && fileExists("${env.DC_HOME}/bin/dependency-check.sh")) {
|
||||||
|
dcHome = env.DC_HOME.trim()
|
||||||
|
echo "Dependency-Check: ${dcHome} (DC_HOME)"
|
||||||
|
} else {
|
||||||
|
def candidates = env.DC_TOOL_NAME?.trim()
|
||||||
|
? [env.DC_TOOL_NAME.trim()]
|
||||||
|
: ['dependency-check', 'Dependency-Check', 'OWASP Dependency-Check', 'dependency-check-cli']
|
||||||
|
for (name in candidates) {
|
||||||
|
try {
|
||||||
|
dcHome = tool name: name,
|
||||||
|
type: 'org.jenkinsci.plugins.DependencyCheck.tools.DependencyCheckInstallation'
|
||||||
|
echo "Dependency-Check tool: '${name}' -> ${dcHome}"
|
||||||
|
break
|
||||||
|
} catch (ignored) {
|
||||||
|
// 등록되지 않은 이름은 건너뛴다
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dcHome == null) {
|
||||||
|
echo 'Jenkins Tools 에 등록된 Dependency-Check 가 없다. PATH 의 dependency-check.sh 로 진행한다.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVD API Key credential 이 있는지 먼저 확인한다.
|
||||||
|
// (없는 credential 로 withCredentials 를 감싸면 파이프라인 자체가 죽는다)
|
||||||
|
def hasNvdKey = true
|
||||||
|
try {
|
||||||
|
withCredentials([string(credentialsId: env.NVD_API_KEY_CRED, variable: 'NVD_PROBE')]) {
|
||||||
|
// 존재 확인만 한다
|
||||||
|
}
|
||||||
|
} catch (ignored) {
|
||||||
|
hasNvdKey = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNvdKey) {
|
||||||
|
echo "NVD API Key credential('${env.NVD_API_KEY_CRED}') 이 없다. 키 없이 진행한다."
|
||||||
|
echo 'NVD 갱신이 매우 느려진다(수 시간). https://nvd.nist.gov/developers/request-an-api-key 에서 발급 권장.'
|
||||||
|
}
|
||||||
|
|
||||||
|
def dcScript = '''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Dependency-Check 는 JRE 11 이상 필요 (빌드용 JDK 8 과 분리)
|
||||||
|
export JAVA_HOME="$JAVA_HOME_SCANNER"
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
|
||||||
|
mkdir -p "$DC_DATA" "$DC_REPORT_DIR"
|
||||||
|
|
||||||
|
if [ -n "${DC_RESOLVED_HOME:-}" ] && [ -x "$DC_RESOLVED_HOME/bin/dependency-check.sh" ]; then
|
||||||
|
DC="$DC_RESOLVED_HOME/bin/dependency-check.sh"
|
||||||
|
elif command -v dependency-check.sh >/dev/null 2>&1; then
|
||||||
|
DC="$(command -v dependency-check.sh)"
|
||||||
|
else
|
||||||
|
echo "dependency-check.sh 를 찾지 못했다."
|
||||||
|
echo "노드에 CLI 를 설치하고 DC_HOME 을 맞추거나, Manage Jenkins > Tools 에 등록할 것."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "dependency-check: $DC"
|
||||||
|
|
||||||
|
ARGS=""
|
||||||
|
|
||||||
|
# 폐쇄망/오프라인: 캐시된 NVD DB 로만 검사한다.
|
||||||
|
if [ "${UPDATE_NVD_FLAG}" != "true" ]; then
|
||||||
|
ARGS="$ARGS --noupdate"
|
||||||
|
if [ ! -d "$DC_DATA" ] || [ -z "$(ls -A "$DC_DATA" 2>/dev/null)" ]; then
|
||||||
|
echo "NVD 캐시가 비어 있는데 갱신이 꺼져 있다: $DC_DATA"
|
||||||
|
echo "인터넷 되는 곳에서 1회 적재한 data 디렉터리를 복사해 둘 것."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${NVD_API_KEY:-}" ]; then
|
||||||
|
ARGS="$ARGS --nvdApiKey ${NVD_API_KEY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OSS Index 분석기는 외부 서비스(ossindex.sonatype.org)를 호출한다.
|
||||||
|
# 갱신을 끈 환경(=외부 통신 불가)에서는 같이 끈다.
|
||||||
|
if [ "${UPDATE_NVD_FLAG}" != "true" ]; then
|
||||||
|
ARGS="$ARGS --disableOssIndex"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 자바 프로젝트에 불필요한 분석기 — 실행 시간만 늘린다.
|
||||||
|
ARGS="$ARGS --disableAssembly --disableNodeAudit --disableNodeJS"
|
||||||
|
|
||||||
|
SCAN_ARGS="--scan build/dependency-check/libs"
|
||||||
|
if [ "${SCAN_JS_FLAG}" = "true" ]; then
|
||||||
|
# 번들된 JS 라이브러리(jQuery, summernote 등)의 알려진 취약점 — RetireJS
|
||||||
|
[ -d src/main/resources/static/js ] && SCAN_ARGS="$SCAN_ARGS --scan src/main/resources/static/js"
|
||||||
|
[ -d src/main/resources/static/plugins ] && SCAN_ARGS="$SCAN_ARGS --scan src/main/resources/static/plugins"
|
||||||
|
else
|
||||||
|
ARGS="$ARGS --disableRetireJS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUPPRESSION=""
|
||||||
|
if [ -f ci/dependency-check-suppressions.xml ]; then
|
||||||
|
SUPPRESSION="--suppression ci/dependency-check-suppressions.xml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 토큰/키가 콘솔에 남지 않도록 실행 명령 자체는 출력하지 않는다.
|
||||||
|
set +x
|
||||||
|
"$DC" \
|
||||||
|
--project "DJB eAPIM Portal" \
|
||||||
|
$SCAN_ARGS \
|
||||||
|
--out "$DC_REPORT_DIR" \
|
||||||
|
--format HTML --format XML --format JSON \
|
||||||
|
--data "$DC_DATA" \
|
||||||
|
--failOnCVSS "${FAIL_ON_CVSS_VALUE}" \
|
||||||
|
$SUPPRESSION \
|
||||||
|
$ARGS
|
||||||
|
'''
|
||||||
|
|
||||||
|
def dcEnv = [
|
||||||
|
"DC_RESOLVED_HOME=${dcHome ?: ''}",
|
||||||
|
"UPDATE_NVD_FLAG=${params.UPDATE_NVD}",
|
||||||
|
"SCAN_JS_FLAG=${params.SCAN_JS}",
|
||||||
|
"FAIL_ON_CVSS_VALUE=${params.FAIL_ON_CVSS}"
|
||||||
|
]
|
||||||
|
|
||||||
|
// 정책: 기본은 UNSTABLE 로만 표시하고 뒤의 Sonar 분석까지 마친다.
|
||||||
|
// FAIL_BUILD_ON_FINDING 을 켜면 임계치 위반이 빌드 실패가 된다.
|
||||||
|
def onFinding = params.FAIL_BUILD_ON_FINDING ? 'FAILURE' : 'UNSTABLE'
|
||||||
|
catchError(buildResult: onFinding, stageResult: 'FAILURE') {
|
||||||
|
withEnv(dcEnv) {
|
||||||
|
if (hasNvdKey) {
|
||||||
|
withCredentials([string(credentialsId: env.NVD_API_KEY_CRED, variable: 'NVD_API_KEY')]) {
|
||||||
|
sh dcScript
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sh dcScript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts allowEmptyArchive: true,
|
||||||
|
artifacts: 'build/reports/dependency-check/dependency-check-report.*'
|
||||||
|
script {
|
||||||
|
// OWASP Dependency-Check 플러그인이 설치돼 있으면 추이 그래프/경고를 남긴다.
|
||||||
|
// 없어도 리포트는 위에서 아카이브되므로 실패로 보지 않는다.
|
||||||
|
try {
|
||||||
|
dependencyCheckPublisher pattern: 'build/reports/dependency-check/dependency-check-report.xml'
|
||||||
|
} catch (ignored) {
|
||||||
|
echo 'OWASP Dependency-Check 플러그인 미설치 — 아카이브된 HTML 리포트로 확인할 것.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sonar.java.libraries 용 클래스패스 덤프.
|
||||||
|
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
|
||||||
|
stage('Export analysis classpath') {
|
||||||
|
when { expression { return params.RUN_SONAR } }
|
||||||
|
steps {
|
||||||
|
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||||
|
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('SonarQube analysis') {
|
||||||
|
when { expression { return params.RUN_SONAR } }
|
||||||
|
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)에 노출시키지 않고 환경변수로만 넘긴다.
|
||||||
|
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
|
||||||
|
|
||||||
|
# SonarQube 에 Dependency-Check 플러그인이 설치돼 있으면 CVE 결과도 함께 올린다.
|
||||||
|
# 플러그인이 없으면 스캐너가 모르는 속성으로 무시한다(경고만).
|
||||||
|
DC_ARG=""
|
||||||
|
if [ -f "$DC_REPORT_DIR/dependency-check-report.json" ]; then
|
||||||
|
DC_ARG="-Dsonar.dependencyCheck.jsonReportPath=$DC_REPORT_DIR/dependency-check-report.json"
|
||||||
|
if [ -f "$DC_REPORT_DIR/dependency-check-report.html" ]; then
|
||||||
|
DC_ARG="$DC_ARG -Dsonar.dependencyCheck.htmlReportPath=$DC_REPORT_DIR/dependency-check-report.html"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 \
|
||||||
|
$DC_ARG
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
|
||||||
|
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
|
||||||
|
stage('Quality Gate') {
|
||||||
|
when { expression { return params.RUN_SONAR } }
|
||||||
|
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())}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileExists('build/reports/dependency-check/dependency-check-report.html')) {
|
||||||
|
echo "Dependency-Check 리포트: ${env.BUILD_URL}artifact/build/reports/dependency-check/dependency-check-report.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
- Git
|
||||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
- Oracle 19c 접근 (개발: `192.168.240.177:1599`)
|
||||||
|
- Node.js 18 LTS 이상 (SCSS 컴파일용)
|
||||||
|
|
||||||
### Git Bash에서 초기 세팅
|
### 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"
|
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
|
||||||
|
|
||||||
# Gradle 명령어 설정
|
# Gradle 명령어 설정
|
||||||
# 일반 환경: gradle
|
|
||||||
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
|
|
||||||
export GRADLE_CMD="gradle"
|
export GRADLE_CMD="gradle"
|
||||||
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
# 2. 작업 디렉토리 생성
|
# 2. 작업 디렉토리 생성
|
||||||
@@ -128,12 +132,6 @@ git clone ${GIT_REPO_BASE}/elink-portal-common.git
|
|||||||
cd elink-portal-common
|
cd elink-portal-common
|
||||||
git checkout master # 또는 필요한 브랜치
|
git checkout master # 또는 필요한 브랜치
|
||||||
|
|
||||||
# kjb-safedb 클론
|
|
||||||
cd $WORKSPACE_DIR
|
|
||||||
git clone ${GIT_REPO_BASE}/kjb-safedb.git
|
|
||||||
cd kjb-safedb
|
|
||||||
git checkout master # 또는 필요한 브랜치
|
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
# 6. 최종 폴더 구조 확인
|
# 6. 최종 폴더 구조 확인
|
||||||
# ====================================
|
# ====================================
|
||||||
@@ -142,7 +140,6 @@ git checkout master # 또는 필요한 브랜치
|
|||||||
# ├── eapim-online/
|
# ├── eapim-online/
|
||||||
# │ └── elink-online-core-jpa/
|
# │ └── elink-online-core-jpa/
|
||||||
# ├── elink-portal-common/
|
# ├── elink-portal-common/
|
||||||
# └── kjb-safedb/
|
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
# 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
# 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 set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
|
||||||
|
|
||||||
REM Gradle 명령어 설정
|
REM Gradle 명령어 설정
|
||||||
REM 일반 환경: gradle
|
|
||||||
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
|
||||||
set GRADLE_CMD=gradle
|
set GRADLE_CMD=gradle
|
||||||
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
|
||||||
|
|
||||||
REM ====================================
|
REM ====================================
|
||||||
REM 2. 작업 디렉토리 생성
|
REM 2. 작업 디렉토리 생성
|
||||||
@@ -207,12 +201,6 @@ git clone %GIT_REPO_BASE%/elink-portal-common.git
|
|||||||
cd elink-portal-common
|
cd elink-portal-common
|
||||||
git checkout master
|
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 ====================================
|
||||||
REM 6. 최종 폴더 구조 확인
|
REM 6. 최종 폴더 구조 확인
|
||||||
REM ====================================
|
REM ====================================
|
||||||
@@ -221,7 +209,6 @@ REM ├── eapim-portal\
|
|||||||
REM ├── eapim-online\
|
REM ├── eapim-online\
|
||||||
REM │ └── elink-online-core-jpa\
|
REM │ └── elink-online-core-jpa\
|
||||||
REM ├── elink-portal-common\
|
REM ├── elink-portal-common\
|
||||||
REM └── kjb-safedb\
|
|
||||||
|
|
||||||
REM ====================================
|
REM ====================================
|
||||||
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||||
@@ -253,30 +240,19 @@ REM Gradle 디펜던시 다운로드
|
|||||||
|
|
||||||
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
||||||
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
||||||
- **SafeDB**: 암호화 기능이 필요한 경우 kjb-safedb 모듈 설정 확인
|
|
||||||
|
|
||||||
## 빌드 및 실행
|
## 빌드 및 실행
|
||||||
|
|
||||||
### Gradle 명령어 설정
|
### Gradle 명령어 설정
|
||||||
|
|
||||||
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
|
|
||||||
|
|
||||||
**Git Bash:**
|
**Git Bash:**
|
||||||
```bash
|
```bash
|
||||||
# 일반 환경
|
|
||||||
export GRADLE_CMD="gradle"
|
export GRADLE_CMD="gradle"
|
||||||
|
|
||||||
# msi-gf63 환경
|
|
||||||
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows CMD:**
|
**Windows CMD:**
|
||||||
```cmd
|
```cmd
|
||||||
REM 일반 환경
|
|
||||||
set GRADLE_CMD=gradle
|
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 프로젝트
|
├── eapim-online/ # Online 프로젝트
|
||||||
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
|
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
|
||||||
├── elink-portal-common/ # 공통 유틸리티
|
├── elink-portal-common/ # 공통 유틸리티
|
||||||
└── kjb-safedb/ # SafeDB 암호화 라이브러리
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### eapim-portal 내부 구조
|
### eapim-portal 내부 구조
|
||||||
@@ -327,7 +302,6 @@ src/main/java/com/eactive/apim/portal/
|
|||||||
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
|
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
|
||||||
- `eapim-online/elink-online-core-jpa`: Gateway 데이터 모델 (../eapim-online/elink-online-core-jpa)
|
- `eapim-online/elink-online-core-jpa`: Gateway 데이터 모델 (../eapim-online/elink-online-core-jpa)
|
||||||
- `elink-portal-common`: 공통 유틸리티 (../elink-portal-common)
|
- `elink-portal-common`: 공통 유틸리티 (../elink-portal-common)
|
||||||
- `kjb-safedb`: SafeDB 암호화 라이브러리 (../kjb-safedb)
|
|
||||||
|
|
||||||
## SASS / CSS 스타일 개발
|
## 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
|
```bash
|
||||||
# Node.js가 설치된 상태에서
|
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -532,8 +509,6 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
|||||||
|
|
||||||
- 사용자 관리 (내부/기업)
|
- 사용자 관리 (내부/기업)
|
||||||
- API 키 관리 및 승인 워크플로우
|
- API 키 관리 및 승인 워크플로우
|
||||||
- API 카탈로그 및 문서화
|
|
||||||
- API 테스트 프록시
|
|
||||||
- 감사 추적 (Hibernate Envers)
|
- 감사 추적 (Hibernate Envers)
|
||||||
|
|
||||||
## 배포
|
## 배포
|
||||||
@@ -543,24 +518,10 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
|||||||
- WebLogic
|
- WebLogic
|
||||||
- Tomcat (내장)
|
- 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` (한글)
|
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
|
||||||
- **사용자 가이드**: `개발자포탈.md` (한글)
|
- **사용자 가이드**: `개발자포탈.md` (한글)
|
||||||
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
|
- **빌드 스크립트**: `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 'war'
|
||||||
id 'eclipse'
|
id 'eclipse'
|
||||||
id 'idea'
|
id 'idea'
|
||||||
|
id 'org.cyclonedx.bom' version '3.2.4'
|
||||||
id 'org.springframework.boot' version '2.7.18'
|
id 'org.springframework.boot' version '2.7.18'
|
||||||
id 'io.spring.dependency-management' version '1.1.3'
|
id 'io.spring.dependency-management' version '1.1.3'
|
||||||
}
|
}
|
||||||
@@ -23,24 +24,61 @@ 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 {
|
dependencies {
|
||||||
annotationProcessor "org.projectlombok:lombok:1.18.28"
|
annotationProcessor "org.projectlombok:lombok:1.18.28"
|
||||||
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
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 'com.eactive.elink.common:elink-common-data:4.5.5'
|
||||||
|
|
||||||
// implementation 'io.swagger.core.v3:swagger-core:2.2.25'
|
// OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체.
|
||||||
// implementation 'io.swagger.parser.v3:swagger-parser:2.1.23'
|
// swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는
|
||||||
implementation fileTree(dir: 'libs/swagger', include: ['*.jar'])
|
// 아직 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-online-core-jpa')
|
||||||
implementation project(':elink-portal-common')
|
implementation project(':elink-portal-common')
|
||||||
// implementation project(':kjb-safedb')
|
// implementation project(':kjb-safedb')
|
||||||
|
|
||||||
implementation('org.springframework.boot:spring-boot-starter')
|
implementation('org.springframework.boot:spring-boot-starter')
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
|
|
||||||
implementation('org.springframework.boot:spring-boot-starter-web')
|
implementation('org.springframework.boot:spring-boot-starter-web')
|
||||||
implementation('org.springframework.boot:spring-boot-starter-validation')
|
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: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
|
||||||
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
|
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
|
||||||
|
|
||||||
@@ -76,9 +114,10 @@ dependencies {
|
|||||||
// exclude group: 'commons-collections', module: 'commons-collections'
|
// exclude group: 'commons-collections', module: 'commons-collections'
|
||||||
}
|
}
|
||||||
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.3'
|
// WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. JDK8 호환.
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.3'
|
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.6'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
|
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'
|
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
|
||||||
|
|
||||||
@@ -124,21 +163,40 @@ bootRun {
|
|||||||
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
|
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
|
||||||
|
|
||||||
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
|
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 {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
java {
|
java {
|
||||||
srcDir 'src/main/java'
|
srcDir 'src/main/java'
|
||||||
srcDir 'build/generated/sources/annotationProcessor/java/main'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
annotationProcessor
|
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 {
|
compileJava {
|
||||||
@@ -148,20 +206,39 @@ compileJava {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
exclude { details ->
|
// exclude { details ->
|
||||||
details.file.name.startsWith('application-') &&
|
// details.file.name.startsWith('application-') &&
|
||||||
details.file.name.endsWith('.yml') &&
|
// details.file.name.endsWith('.yml') &&
|
||||||
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform {
|
||||||
|
if (project.hasProperty('includeE2E') && project.includeE2E.toBoolean()) {
|
||||||
|
includeTags 'e2e'
|
||||||
|
} else {
|
||||||
|
excludeTags 'e2e'
|
||||||
|
}
|
||||||
|
}
|
||||||
enabled = true
|
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 {
|
bootWar {
|
||||||
archiveFileName = "eapim-portal-boot.war"
|
archiveFileName = "eapim-portal-boot.war"
|
||||||
|
mainClass = 'com.eactive.apim.portal.PortalApplication'
|
||||||
|
rootSpec.exclude(localOnlyResources)
|
||||||
|
classpath = excludeLocalOnlyLibs(classpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
war {
|
war {
|
||||||
@@ -170,6 +247,8 @@ war {
|
|||||||
from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' }
|
from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' }
|
||||||
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
||||||
|
|
||||||
|
rootSpec.exclude(localOnlyResources)
|
||||||
|
classpath = excludeLocalOnlyLibs(classpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
task printSourceSets {
|
task printSourceSets {
|
||||||
@@ -182,19 +261,5 @@ task printSourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
|
||||||
// CryptoCli 실행 task (bcrypt 지원)
|
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
|
||||||
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+')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
./gradlew bootWar
|
|
||||||
docker build -t eactive-portal .
|
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// OWASP Dependency-Check 가 스캔할 서드파티 jar 를 한 디렉터리로 모은다.
|
||||||
|
//
|
||||||
|
// build.gradle 을 건드리지 않기 위해 init script(-I) 로만 주입한다.
|
||||||
|
// gradle -I ci/dependency-check-classpath.gradle exportDependencyJars --no-daemon
|
||||||
|
//
|
||||||
|
// 출력:
|
||||||
|
// build/dependency-check/libs/*.jar runtimeClasspath 의 외부 의존 jar (원본 파일명 유지)
|
||||||
|
// build/dependency-check/jars.txt 수집 목록(경로 1줄씩) — 진단용
|
||||||
|
//
|
||||||
|
// 왜 디렉터리로 모으는가
|
||||||
|
// Dependency-Check CLI 는 --scan <경로> 로 파일/디렉터리를 받는다. Gradle 캐시를 통째로 스캔하면
|
||||||
|
// 이 프로젝트가 쓰지 않는 버전까지 잡히고, 프로젝트 디렉터리만 스캔하면 의존 jar 가 아예 안 잡힌다.
|
||||||
|
// 실제 배포본에 실리는 것과 같은 목록(runtimeClasspath)만 모아야 결과가 배포 산출물과 일치한다.
|
||||||
|
//
|
||||||
|
// 파일명을 유지해야 하는 이유
|
||||||
|
// Dependency-Check 는 jar 안의 POM/MANIFEST 외에 파일명에서도 CPE(제품/버전)를 추론한다.
|
||||||
|
// 이름을 바꾸면 탐지 정확도가 떨어진다.
|
||||||
|
|
||||||
|
rootProject { project ->
|
||||||
|
project.plugins.withId('java') {
|
||||||
|
project.tasks.register('exportDependencyJars') {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Dependency-Check 스캔용 런타임 의존 jar 를 build/dependency-check/libs 에 모은다'
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
def outRoot = new File(project.layout.buildDirectory.get().asFile, 'dependency-check')
|
||||||
|
def outDir = new File(outRoot, 'libs')
|
||||||
|
project.delete(outDir)
|
||||||
|
outDir.mkdirs()
|
||||||
|
|
||||||
|
// 컴포지트/서브프로젝트의 build 디렉터리 산출물(우리가 만든 jar)은 제외한다.
|
||||||
|
// 자체 코드는 SonarQube 가 보는 영역이고, SCA 대상은 외부 라이브러리다.
|
||||||
|
def buildDirs = project.allprojects.collect {
|
||||||
|
it.layout.buildDirectory.get().asFile.absolutePath
|
||||||
|
}
|
||||||
|
def isOwnArtifact = { File f ->
|
||||||
|
buildDirs.any { f.absolutePath.startsWith(it + File.separator) }
|
||||||
|
}
|
||||||
|
|
||||||
|
def jars = project.configurations.runtimeClasspath.files
|
||||||
|
.findAll { it.isFile() && it.name.endsWith('.jar') && !isOwnArtifact(it) }
|
||||||
|
.unique()
|
||||||
|
.sort { it.name }
|
||||||
|
|
||||||
|
project.copy {
|
||||||
|
from jars
|
||||||
|
into outDir
|
||||||
|
}
|
||||||
|
|
||||||
|
new File(outRoot, 'jars.txt').text =
|
||||||
|
jars.collect { it.absolutePath }.join(System.lineSeparator()) + System.lineSeparator()
|
||||||
|
|
||||||
|
def totalMb = (jars.sum { it.length() } ?: 0L) / (1024 * 1024)
|
||||||
|
logger.lifecycle("dependency-check scan target: ${jars.size()} jars, ${totalMb as int} MB -> ${outDir}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
OWASP Dependency-Check 오탐(false positive) 억제 목록.
|
||||||
|
|
||||||
|
규칙
|
||||||
|
1. 억제는 "오탐"에만 쓴다. 진짜 취약점을 조용히 숨기는 용도로 쓰지 않는다.
|
||||||
|
실제 취약하지만 당장 못 올리는 경우는 억제 대신 만료일(until)을 넣어 재검토를 강제한다.
|
||||||
|
2. 항목마다 <notes> 에 판단 근거와 판단자/일자를 남긴다. 근거 없는 억제는 리뷰에서 거절한다.
|
||||||
|
3. 범위를 좁게 잡는다. cve 단건 + 특정 파일(sha1/packageUrl)이 기본이고,
|
||||||
|
cpe 나 정규식 filePath 로 넓게 억제하지 않는다.
|
||||||
|
|
||||||
|
작성법
|
||||||
|
Dependency-Check HTML 리포트의 각 취약점 옆 "Suppress" 버튼을 누르면 해당 항목의
|
||||||
|
<suppress> 블록이 그대로 생성된다. 그것을 이 파일에 붙여넣고 <notes> 만 채우면 된다.
|
||||||
|
|
||||||
|
참고: https://dependency-check.github.io/DependencyCheck/general/suppression.html
|
||||||
|
-->
|
||||||
|
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||||
|
|
||||||
|
<!-- 예시 (실제 억제 시 주석을 풀고 값 교체)
|
||||||
|
<suppress until="2026-12-31Z">
|
||||||
|
<notes><![CDATA[
|
||||||
|
오탐 근거: 해당 CVE 는 X 기능을 사용할 때만 성립하는데 이 앱은 해당 API 를 호출하지 않음.
|
||||||
|
확인: 홍길동 / 2026-08-13 / 호출부 grep 결과 0건.
|
||||||
|
]]></notes>
|
||||||
|
<packageUrl regex="true">^pkg:maven/org\.example/example-lib@.*$</packageUrl>
|
||||||
|
<cve>CVE-2026-00000</cve>
|
||||||
|
</suppress>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</suppressions>
|
||||||
@@ -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",
|
"name": "eapim-portal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "eapim-portal",
|
"name": "eapim-portal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
|
||||||
"playwright": "^1.57.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sass": "^1.69.5"
|
"sass": "^1.69.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher": {
|
"node_modules/@parcel/watcher": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^1.0.3",
|
"detect-libc": "^2.0.3",
|
||||||
"is-glob": "^4.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": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
@@ -37,25 +34,25 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@parcel/watcher-android-arm64": "2.5.1",
|
"@parcel/watcher-android-arm64": "2.5.6",
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.6",
|
||||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
"@parcel/watcher-darwin-x64": "2.5.6",
|
||||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
"@parcel/watcher-freebsd-x64": "2.5.6",
|
||||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
"@parcel/watcher-linux-arm-glibc": "2.5.6",
|
||||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
"@parcel/watcher-linux-arm-musl": "2.5.6",
|
||||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
|
||||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
"@parcel/watcher-linux-arm64-musl": "2.5.6",
|
||||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
"@parcel/watcher-linux-x64-glibc": "2.5.6",
|
||||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
"@parcel/watcher-linux-x64-musl": "2.5.6",
|
||||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
"@parcel/watcher-win32-arm64": "2.5.6",
|
||||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
"@parcel/watcher-win32-ia32": "2.5.6",
|
||||||
"@parcel/watcher-win32-x64": "2.5.1"
|
"@parcel/watcher-win32-x64": "2.5.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-android-arm64": {
|
"node_modules/@parcel/watcher-android-arm64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
|
||||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -74,9 +71,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
|
||||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -95,9 +92,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-darwin-x64": {
|
"node_modules/@parcel/watcher-darwin-x64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
|
||||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -116,9 +113,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
|
||||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -137,9 +134,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
|
||||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -158,9 +155,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
|
||||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -179,9 +176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
|
||||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -200,9 +197,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
|
||||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -221,9 +218,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
|
||||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -242,9 +239,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
|
||||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -263,9 +260,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-win32-arm64": {
|
"node_modules/@parcel/watcher-win32-arm64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
|
||||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -284,9 +281,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-win32-ia32": {
|
"node_modules/@parcel/watcher-win32-ia32": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
|
||||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -305,9 +302,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher-win32-x64": {
|
"node_modules/@parcel/watcher-win32-x64": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
|
||||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -325,82 +322,37 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16.0"
|
"node": ">= 20.19.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"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": {
|
"engines": {
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -429,32 +381,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
@@ -464,57 +390,27 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 20.19.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -522,21 +418,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.97.1",
|
"version": "1.100.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
|
||||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"immutable": "^5.0.2",
|
"immutable": "^5.1.5",
|
||||||
"source-map-js": ">=0.6.2 <2.0.0"
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sass": "sass.js"
|
"sass": "sass.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@parcel/watcher": "^2.4.1"
|
"@parcel/watcher": "^2.4.1"
|
||||||
@@ -551,284 +447,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"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"
|
"sass": "^1.69.5"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC"
|
||||||
"dependencies": {
|
|
||||||
"playwright": "^1.57.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
TOKEN VARCHAR2(255) not null
|
||||||
primary key,
|
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'
|
include 'elink-portal-common'
|
||||||
|
|
||||||
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
|
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")
|
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||||
|
|
||||||
|
|
||||||
//include 'kjb-safedb'
|
|
||||||
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 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/ 아래라 이미 제외된다.
|
||||||
|
|
||||||
|
# Thymeleaf 인라인 표현식([[${...}]], /*[[${...}]]*/)은 JS 파서가 읽지 못해
|
||||||
|
# "Failed to parse file ... Unexpected token" 으로 분석이 중단된다.
|
||||||
|
# JS 분석에서만 빼고 HTML(web) 센서는 그대로 두어 템플릿 XSS 룰은 유지한다.
|
||||||
|
# 기본값이 **/node_modules/** 이므로 재정의 시 함께 명시해야 한다.
|
||||||
|
sonar.javascript.exclusions=\
|
||||||
|
**/node_modules/**,\
|
||||||
|
src/main/resources/templates/**
|
||||||
|
|
||||||
|
# --- 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);
|
private static final Logger portal_logger = LoggerFactory.getLogger(PortalApplication.class);
|
||||||
|
|
||||||
|
public PortalApplication() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
portal_logger.info("##### PortalApplication Start #####");
|
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
|
@Controller
|
||||||
@RequestMapping("/agreements")
|
@RequestMapping("/agreements")
|
||||||
public class AgreementsController {
|
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;
|
private final AgreementsFacade agreementsFacade;
|
||||||
|
|
||||||
@@ -28,28 +32,49 @@ public class AgreementsController {
|
|||||||
public String showTerms(@RequestParam(required = false) String tab,
|
public String showTerms(@RequestParam(required = false) String tab,
|
||||||
@RequestParam(required = false) String publishedOn,
|
@RequestParam(required = false) String publishedOn,
|
||||||
Model model) {
|
Model model) {
|
||||||
|
String currentTab = tab != null ? tab : "terms";
|
||||||
|
|
||||||
|
// 구 개인정보처리방침 탭(tab=privacy)은 외부 링크로 이동했으므로 외부 URL로 리다이렉트(북마크 호환)
|
||||||
|
if ("privacy".equals(currentTab)) {
|
||||||
|
return "redirect:" + PRIVACY_POLICY_EXTERNAL_URL;
|
||||||
|
}
|
||||||
|
|
||||||
AgreementType type;
|
AgreementType type;
|
||||||
if ("privacy".equals(tab)) {
|
switch (currentTab) {
|
||||||
type = AgreementType.PRIVACY_POLICY;
|
case "privacy-collect":
|
||||||
} else {
|
// 개인정보수집동의서 = PRIVACY_COLLECT (신설항목)
|
||||||
type = AgreementType.TERMS_OF_USE;
|
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));
|
List<AgreementsDTO> agreementsList = agreementsFacade.getAgreementsList(String.valueOf(type));
|
||||||
model.addAttribute("agreementsList", agreementsList);
|
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("selectedAgreement", selectedAgreement);
|
||||||
model.addAttribute("selectedDate", publishedOn);
|
model.addAttribute("selectedDate", publishedOn);
|
||||||
|
|
||||||
model.addAttribute("isTermsOfUse", type == AgreementType.TERMS_OF_USE);
|
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("agreementTitle", type.getDescription());
|
||||||
model.addAttribute("agreementType", type.getCode());
|
model.addAttribute("agreementType", type.getCode());
|
||||||
|
|
||||||
model.addAttribute("currentTab", tab != null ? tab : "terms");
|
model.addAttribute("currentTab", currentTab);
|
||||||
|
|
||||||
return TERMS_AGREEMENTS;
|
return TERMS_AGREEMENTS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
|||||||
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
|
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 개인정보수집동의서 처리
|
// 2. 개인정보수집동의서 처리 (개인정보수집동의서 = PRIVACY_COLLECT 신설항목 포함)
|
||||||
if (privacyCollectType != null && privacyCollectType.isPrivacyCollect()) {
|
if (privacyCollectType != null
|
||||||
|
&& (privacyCollectType.isPrivacyCollect() || privacyCollectType == AgreementType.PRIVACY_COLLECT)) {
|
||||||
Optional<Agreements> privacyCollect = agreementsService
|
Optional<Agreements> privacyCollect = agreementsService
|
||||||
.findLastesBeforeDate(
|
.findLastesBeforeDate(
|
||||||
privacyCollectType,
|
privacyCollectType,
|
||||||
@@ -199,6 +200,10 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
|
|||||||
defaultAgreement.setName("개인정보처리방침");
|
defaultAgreement.setName("개인정보처리방침");
|
||||||
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
break;
|
break;
|
||||||
|
case "PRIVACY_COLLECT":
|
||||||
|
defaultAgreement.setName("개인정보수집동의서");
|
||||||
|
defaultAgreement.setContents("현재 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
|
break;
|
||||||
case "PRIVACY_COLLECT_IND":
|
case "PRIVACY_COLLECT_IND":
|
||||||
defaultAgreement.setName("개인용 개인정보수집동의서");
|
defaultAgreement.setName("개인용 개인정보수집동의서");
|
||||||
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
|||||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
|
||||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
@@ -30,6 +36,7 @@ public class ApiController {
|
|||||||
private final ApiService apiService;
|
private final ApiService apiService;
|
||||||
private final ApiServiceService apiServiceService;
|
private final ApiServiceService apiServiceService;
|
||||||
private final ApiSearchFacade apiSearchFacade;
|
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_ID = "default-token-api-spec";
|
||||||
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
||||||
|
|
||||||
@@ -38,7 +45,24 @@ public class ApiController {
|
|||||||
if (id == null) {
|
if (id == null) {
|
||||||
return "redirect:/apis/common";
|
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);
|
ApiSpecInfoDto api = apiService.selectDetail(id);
|
||||||
if (api == null) {
|
if (api == null) {
|
||||||
throw new NotFoundException(NOT_FOUND_MESSAGE);
|
throw new NotFoundException(NOT_FOUND_MESSAGE);
|
||||||
@@ -46,28 +70,58 @@ public class ApiController {
|
|||||||
|
|
||||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(new ApiGroupSearch());
|
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("apiSpecInfo", api);
|
||||||
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
||||||
model.addAttribute("services", searchResult.get("services"));
|
model.addAttribute("services", searchResult.get("services"));
|
||||||
return "apps/apis/mainApiDetail";
|
model.addAttribute("authenticated", SecurityUtil.isAuthenticated());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String apiList(@ModelAttribute ApiGroupSearch search, Model model) {
|
public String apiList(@ModelAttribute ApiGroupSearch search, @PageableDefault Pageable pageable, Model model) {
|
||||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
|
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<ApiSpecInfoDto> allApis = (List<ApiSpecInfoDto>) searchResult.get("apis");
|
||||||
|
Page<ApiSpecInfoDto> apiPage = slicePage(allApis, pageable);
|
||||||
|
|
||||||
model.addAttribute("search", search);
|
model.addAttribute("search", search);
|
||||||
model.addAttribute("services", searchResult.get("services"));
|
model.addAttribute("services", searchResult.get("services"));
|
||||||
model.addAttribute("apis", searchResult.get("apis"));
|
model.addAttribute("apis", apiPage.getContent());
|
||||||
|
model.addAttribute("page", apiPage);
|
||||||
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
|
||||||
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
|
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
|
||||||
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
|
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";
|
return "apps/apis/mainApiList";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apiSearchFacade.searchApis() 는 다른 소비처(API Status 필터 목록 등)와 계약을 공유하므로
|
||||||
|
* 항상 전체 List 를 돌려준다. 목록 화면 렌더링에서만 결과를 잘라 Page 로 감싼다.
|
||||||
|
*/
|
||||||
|
private Page<ApiSpecInfoDto> slicePage(List<ApiSpecInfoDto> apis, Pageable pageable) {
|
||||||
|
int total = apis == null ? 0 : apis.size();
|
||||||
|
int fromIndex = Math.min(pageable.getPageNumber() * pageable.getPageSize(), total);
|
||||||
|
int toIndex = Math.min(fromIndex + pageable.getPageSize(), total);
|
||||||
|
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
|
||||||
|
|
||||||
|
return new PageImpl<>(content, pageable, total);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/testbed/api")
|
@GetMapping("/testbed/api")
|
||||||
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
|
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||||
|
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
||||||
|
if (!SecurityUtil.isAuthenticated()) {
|
||||||
|
return "redirect:/login?reason=auth";
|
||||||
|
}
|
||||||
String selectedApiServiceName = "API 서비스 선택";
|
String selectedApiServiceName = "API 서비스 선택";
|
||||||
String selectedServiceId = "";
|
String selectedServiceId = "";
|
||||||
boolean idExists = false;
|
boolean idExists = false;
|
||||||
@@ -93,6 +147,10 @@ public class ApiController {
|
|||||||
|
|
||||||
@GetMapping("/testbed")
|
@GetMapping("/testbed")
|
||||||
public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) {
|
public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||||
|
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
||||||
|
if (!SecurityUtil.isAuthenticated()) {
|
||||||
|
return "redirect:/login?reason=auth";
|
||||||
|
}
|
||||||
String selectedApiServiceName = "API 서비스 선택";
|
String selectedApiServiceName = "API 서비스 선택";
|
||||||
boolean idExists = false;
|
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.entity.ApiSpecInfo;
|
||||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
import com.eactive.apim.portal.djb.testbed.service.DjbTestbedSpecServerRewriter;
|
||||||
import com.eactive.apim.portal.apps.apiservice.service.ApiSpecService;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
@@ -22,35 +22,46 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/apis")
|
@RequestMapping("/api/apis")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TestbedSpecController {
|
public class TestbedSpecController {
|
||||||
|
|
||||||
@Autowired
|
private final ApiSpecInfoService apiSpecInfoService;
|
||||||
private ApiSpecInfoService apiSpecInfoService;
|
private final DjbTestbedSpecServerRewriter serverRewriter;
|
||||||
|
|
||||||
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
|
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";
|
private static final String DEFAULT_SPEC_PATH = "swagger/default-token-api-spec.json";
|
||||||
|
|
||||||
@GetMapping(value = "/{id}/swagger.json", produces = MediaType.APPLICATION_JSON_VALUE)
|
@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)) {
|
if (DEFAULT_TOKEN_API_ID.equals(id)) {
|
||||||
try {
|
try {
|
||||||
|
// 기본 토큰 spec 은 서버 치환 대상이 아니다. path 가 포탈 mock 토큰 경로라
|
||||||
|
// GW 호스트를 붙이면 실재하지 않는 주소가 된다(servers 없음 → 문서 origin 사용).
|
||||||
Resource resource = new ClassPathResource(DEFAULT_SPEC_PATH);
|
Resource resource = new ClassPathResource(DEFAULT_SPEC_PATH);
|
||||||
String content = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
|
return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
|
||||||
return ResponseEntity.ok().body(content);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed to read default token api spec file", 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);
|
Optional<ApiSpecInfo> spec = apiSpecInfoService.findById(id);
|
||||||
|
if (!spec.isPresent() || !StringUtils.hasText(spec.get().getTestbedSpec())) {
|
||||||
if (!spec.isPresent()) {
|
return null;
|
||||||
StringUtils.hasText(spec.get().getTestbedSpec());
|
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok().body(spec.get().getTestbedSpec());
|
return serverRewriter.rewriteServerToGateway(spec.get().getTestbedSpec(), request);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||