Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59c379b22c | |||
| c732f1e1b2 | |||
| 9e32502ee3 | |||
| 2d0ff2bf84 | |||
| 21940a3e18 | |||
| 2e517a1e08 | |||
| 10c5e51162 | |||
| 69e5126de6 | |||
| d044ebd157 | |||
| af40f0ba61 | |||
| d6b3a44d61 | |||
| e57f2fe72f | |||
| cecf6260a6 | |||
| 52707998d4 | |||
| 28017fd83c | |||
| e55b7a6bf2 | |||
| 1b6fed7701 | |||
| 033faa6714 | |||
| 758b3ebd7d | |||
| f07123904a | |||
| 2bc2a3d6e3 | |||
| b58893d098 | |||
| 80b072983f | |||
| a456ed9bb0 | |||
| 33c64972f8 | |||
| cff0199d2d | |||
| 2e920eec96 | |||
| 65a262849e | |||
| 7056306718 | |||
| 05e0599b26 | |||
| c223fa3124 | |||
| 78b1edff5a | |||
| b41a81bda3 | |||
| 1ea2c89776 | |||
| f1d6181210 | |||
| bac9a54dee | |||
| 603cba65a7 | |||
| 889d77b990 | |||
| bf2dad6ba7 | |||
| 3a1b0cb9c1 | |||
| a13279a777 | |||
| 5299352235 | |||
| dfb37c18b9 | |||
| 5f5b23026f | |||
| 5c61fc3f24 | |||
| a7aa5bc98c | |||
| bf63701e68 | |||
| f26aae296a | |||
| 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
|
||||
build
|
||||
|
||||
# SonarScanner 작업 디렉터리
|
||||
.scannerwork/
|
||||
|
||||
|
||||
|
||||
build_knbank.sh
|
||||
@@ -108,3 +111,5 @@ diff
|
||||
|
||||
*rinjae*
|
||||
*gf63*
|
||||
*obsidian*
|
||||
design-backup
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
**기술 스택:**
|
||||
- Spring Boot 2.7.18 with Spring MVC and Thymeleaf
|
||||
- Java 8 (targetCompatibility: 1.8)
|
||||
- Java 8 (sourceCompatibility/targetCompatibility: 1.8)
|
||||
- Gradle 8.7 빌드 시스템
|
||||
- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스)
|
||||
- Spring Security (커스텀 인증)
|
||||
@@ -59,7 +59,7 @@ git push origin jenkins_with_weblogic
|
||||
|
||||
### 애플리케이션 실행
|
||||
- gradlew 사용 금지 - offline gradle 단독 실행
|
||||
- msi-gf63 컴퓨터에서는 /c/eactive/workspaces/shell-scripts/kjb-gradle.sh 로 gradle 실행 (jdk, gradle 등 환경 변수 세팅 후 gradle 실행하는 스크립트)
|
||||
- JDK/Gradle 환경 변수는 저장소 루트의 `.envrc`(direnv)로 자동 설정됨 (JAVA_HOME → Zulu JDK 8, GRADLE_USER_HOME → 격리 디렉터리)
|
||||
|
||||
```bash
|
||||
# dev 프로파일로 실행 (기본값)
|
||||
@@ -173,20 +173,21 @@ com.eactive.apim.portal/
|
||||
|
||||
1. **EMS Database** (Portal/Admin)
|
||||
- JNDI: `jdbc/dsOBP_EMS`
|
||||
- Schema: `EMSADM`
|
||||
- Schema: `EMSAPP`
|
||||
- Entities: `com.eactive.apim.portal.apps.*`
|
||||
- 목적: 포털 사용자, 조직, API 키, 승인
|
||||
|
||||
2. **Gateway Database** (API Specs)
|
||||
- JNDI: `jdbc/dsOBP_AGW`
|
||||
- Schema: `AGWADM`
|
||||
- Schema: `AGWAPP`
|
||||
- Entities: `com.eactive.eai.data.entity.onl.*`
|
||||
- 목적: API 명세, 서비스, 메시지
|
||||
|
||||
설정: `config/PortalDatasourceConfiguration.java`
|
||||
- 각 데이터베이스별 별도 EntityManager
|
||||
- Atomikos JTA를 통한 분산 트랜잭션
|
||||
- 트랜잭션 로그: `/Log/eapim/portal/`
|
||||
- **JTA/XA 미사용**. EntityManagerFactory 별 로컬 트랜잭션 (`config/PortalConfigTransaction.java`)
|
||||
- `transactionManager` (@Primary) → EMS
|
||||
- `gatewayTransactionManager` → Gateway
|
||||
|
||||
### 설정 프로파일
|
||||
|
||||
@@ -265,6 +266,41 @@ API 키는 승인 워크플로우와 함께 관리됩니다:
|
||||
8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가
|
||||
9. `application.yml`의 `pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑)
|
||||
|
||||
### 메뉴/네비게이션 항목 추가하기
|
||||
|
||||
**중요:** 메뉴 정의가 **두 곳으로 분리**되어 있다. yml 한 곳만 고치면 헤더 드롭다운에 안 보이고, 헤더만 고치면 브레드크럼이 비어 보인다. 메뉴 추가/이름 변경/순서 변경 시 **반드시 두 파일을 같이 수정**한다.
|
||||
|
||||
1. **`application.yml`의 `page:` 트리** (브레드크럼·페이지 메타용)
|
||||
- `PageService`가 URL → breadcrumb 매핑에 사용
|
||||
- 부모 메뉴(`service`, `apis`, `community` 등) 아래 `children:`에 새 키를 추가
|
||||
```yaml
|
||||
page:
|
||||
home:
|
||||
children:
|
||||
service:
|
||||
children:
|
||||
oauth2_guide:
|
||||
name: "OAuth2 인증가이드"
|
||||
path: "/service/oauth2-guide"
|
||||
```
|
||||
|
||||
2. **`templates/views/fragment/djbank/header_container.html`** (상단 글로벌 네비)
|
||||
- `<nav class="header-center"><ul class="nav-menu">` 블록에 정적 `<li>`로 하드코딩되어 있다 — yml 트리를 iterate 하지 **않음**
|
||||
- 부모 메뉴의 `<ul class="sub-menu">`에 `<li><a th:href="@{/...}">메뉴명</a></li>` 직접 추가
|
||||
|
||||
3. **라우트도 같이 등록** (`application.yml`의 `portal.pages` 매핑 섹션, 또는 `@GetMapping`)
|
||||
```yaml
|
||||
portal:
|
||||
pages:
|
||||
- path-pattern: /service/oauth2-guide
|
||||
method: GET
|
||||
view-name: apps/service/oauth2-guide
|
||||
```
|
||||
|
||||
4. **검증**: 변경 후 (a) 헤더 드롭다운에 항목이 보이는지, (b) 새 페이지 진입 시 브레드크럼에 메뉴명이 보이는지 둘 다 확인.
|
||||
|
||||
5. **재시작**: `application.yml` 변경은 `PortalPageHandlerMapper`가 `@PostConstruct`에서 한 번만 등록하므로 **자동 라이브 리로드 안 됨** — 수동 재시작 필요. HTML/CSS만 라이브 반영됨.
|
||||
|
||||
### MapStruct DTO 매핑
|
||||
|
||||
타입 안전한 DTO 변환을 위해 MapStruct 사용:
|
||||
@@ -324,7 +360,13 @@ return queryFactory.selectFrom(user)
|
||||
적절한 propagation과 함께 `@Transactional` 사용:
|
||||
- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성)
|
||||
- 읽기 전용 작업: 최적화를 위해 `@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
|
||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
||||
- Oracle 19c 접근 (개발: `192.168.240.177:1599`)
|
||||
- Node.js 18 LTS 이상 (SCSS 컴파일용)
|
||||
|
||||
### Git Bash에서 초기 세팅
|
||||
|
||||
@@ -90,10 +97,7 @@ export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
|
||||
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
|
||||
|
||||
# Gradle 명령어 설정
|
||||
# 일반 환경: gradle
|
||||
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
|
||||
export GRADLE_CMD="gradle"
|
||||
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
|
||||
# ====================================
|
||||
# 2. 작업 디렉토리 생성
|
||||
@@ -128,12 +132,6 @@ git clone ${GIT_REPO_BASE}/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# kjb-safedb 클론
|
||||
cd $WORKSPACE_DIR
|
||||
git clone ${GIT_REPO_BASE}/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# ====================================
|
||||
# 6. 최종 폴더 구조 확인
|
||||
# ====================================
|
||||
@@ -142,7 +140,6 @@ git checkout master # 또는 필요한 브랜치
|
||||
# ├── eapim-online/
|
||||
# │ └── elink-online-core-jpa/
|
||||
# ├── elink-portal-common/
|
||||
# └── kjb-safedb/
|
||||
|
||||
# ====================================
|
||||
# 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
@@ -169,10 +166,7 @@ set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
|
||||
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
|
||||
|
||||
REM Gradle 명령어 설정
|
||||
REM 일반 환경: gradle
|
||||
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
set GRADLE_CMD=gradle
|
||||
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
|
||||
REM ====================================
|
||||
REM 2. 작업 디렉토리 생성
|
||||
@@ -207,12 +201,6 @@ git clone %GIT_REPO_BASE%/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master
|
||||
|
||||
REM kjb-safedb 클론
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
git clone %GIT_REPO_BASE%/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master
|
||||
|
||||
REM ====================================
|
||||
REM 6. 최종 폴더 구조 확인
|
||||
REM ====================================
|
||||
@@ -221,7 +209,6 @@ REM ├── eapim-portal\
|
||||
REM ├── eapim-online\
|
||||
REM │ └── elink-online-core-jpa\
|
||||
REM ├── elink-portal-common\
|
||||
REM └── kjb-safedb\
|
||||
|
||||
REM ====================================
|
||||
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
|
||||
@@ -253,30 +240,19 @@ REM Gradle 디펜던시 다운로드
|
||||
|
||||
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
||||
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
||||
- **SafeDB**: 암호화 기능이 필요한 경우 kjb-safedb 모듈 설정 확인
|
||||
|
||||
## 빌드 및 실행
|
||||
|
||||
### Gradle 명령어 설정
|
||||
|
||||
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
|
||||
|
||||
**Git Bash:**
|
||||
```bash
|
||||
# 일반 환경
|
||||
export GRADLE_CMD="gradle"
|
||||
|
||||
# msi-gf63 환경
|
||||
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
```
|
||||
|
||||
**Windows CMD:**
|
||||
```cmd
|
||||
REM 일반 환경
|
||||
set GRADLE_CMD=gradle
|
||||
|
||||
REM msi-gf63 환경
|
||||
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
```
|
||||
|
||||
### 빌드 및 실행 명령어
|
||||
@@ -310,7 +286,6 @@ kjb-eapim/
|
||||
├── eapim-online/ # Online 프로젝트
|
||||
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
|
||||
├── elink-portal-common/ # 공통 유틸리티
|
||||
└── kjb-safedb/ # SafeDB 암호화 라이브러리
|
||||
```
|
||||
|
||||
### eapim-portal 내부 구조
|
||||
@@ -327,7 +302,6 @@ src/main/java/com/eactive/apim/portal/
|
||||
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
|
||||
- `eapim-online/elink-online-core-jpa`: Gateway 데이터 모델 (../eapim-online/elink-online-core-jpa)
|
||||
- `elink-portal-common`: 공통 유틸리티 (../elink-portal-common)
|
||||
- `kjb-safedb`: SafeDB 암호화 라이브러리 (../kjb-safedb)
|
||||
|
||||
## SASS / CSS 스타일 개발
|
||||
|
||||
@@ -339,8 +313,11 @@ src/main/java/com/eactive/apim/portal/
|
||||
|
||||
### 설치
|
||||
|
||||
Node.js 및 SASS 설치 절차는 [개발환경 준비 사항 § 6. Node.js & SASS 빌드 환경](djb-docs/개발환경-준비-사항.md#6-nodejs--sass-빌드-환경) 을 참고하세요.
|
||||
|
||||
Node.js가 이미 설치되어 있다면 프로젝트 의존성만 설치하면 됩니다:
|
||||
|
||||
```bash
|
||||
# Node.js가 설치된 상태에서
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -532,8 +509,6 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
||||
|
||||
- 사용자 관리 (내부/기업)
|
||||
- API 키 관리 및 승인 워크플로우
|
||||
- API 카탈로그 및 문서화
|
||||
- API 테스트 프록시
|
||||
- 감사 추적 (Hibernate Envers)
|
||||
|
||||
## 배포
|
||||
@@ -543,24 +518,10 @@ ls -lh src/main/resources/static/css/main.min.css # minified
|
||||
- WebLogic
|
||||
- Tomcat (내장)
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
./build_docker.sh
|
||||
docker run -p 8080:8080 eapim-portal:latest
|
||||
```
|
||||
|
||||
## 관련 프로젝트
|
||||
|
||||
프로젝트 구조 (kjb-eapim 디렉토리 기준):
|
||||
- `eapim-portal/` - Portal 웹 애플리케이션 (현재 프로젝트)
|
||||
- `eapim-online/` - Online 프로젝트 (elink-online-core-jpa 포함)
|
||||
- `elink-portal-common/` - 공통 유틸리티 라이브러리
|
||||
- `kjb-safedb/` - SafeDB 암호화 라이브러리
|
||||
- `eapim-admin/` - Admin 포털 (선택사항)
|
||||
- `kjb-eapim-sql/` - SQL 스크립트 (선택사항)
|
||||
|
||||
## 문서
|
||||
|
||||
- **개발환경 준비 사항**: [`djb-docs/개발환경-준비-사항.md`](djb-docs/개발환경-준비-사항.md) — JDK·Gradle·Node.js·SASS 설치 가이드
|
||||
- **메뉴 관리 개발 가이드**: [`readme-docs/메뉴-관리-개발-가이드.md`](readme-docs/메뉴-관리-개발-가이드.md) — menu.yml/roles.yml 스키마·시딩 규칙·캐시 리로드·admin 포탈메뉴관리 연동
|
||||
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
|
||||
- **사용자 가이드**: `개발자포탈.md` (한글)
|
||||
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
echo "========================"
|
||||
java -version
|
||||
javac -version
|
||||
echo "========================"
|
||||
gradle --version
|
||||
echo "========================"
|
||||
|
||||
gradle clean build -x test --no-daemon
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'war'
|
||||
id 'eclipse'
|
||||
id 'idea'
|
||||
id 'org.cyclonedx.bom' version '3.2.4'
|
||||
id 'org.springframework.boot' version '2.7.18'
|
||||
id 'io.spring.dependency-management' version '1.1.3'
|
||||
}
|
||||
@@ -23,29 +24,69 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 전용 라이브러리 — WebLogic 배포 산출물(war/bootWar)에서 제외한다.
|
||||
// 정보보호 점검에서 Actuator/모니터링 라이브러리가 배포본에 실려 있으면 불필요하게 탐지되므로
|
||||
// 로컬(bootRun/IDE)에서만 classpath 에 오르게 한다. 제외 로직은 아래 war/bootWar 블록.
|
||||
//
|
||||
// ※ Spring Boot 의 developmentOnly 를 쓰지 않는 이유
|
||||
// developmentOnly 는 bootJar/bootWar 에서만 제외된다. 표준 war task 는 runtimeClasspath 를
|
||||
// 그대로 쓰므로 산출물에 실린다 (실측: devtools 가 eapim-portal.war 에 포함되어 있었음).
|
||||
// ※ 별도 configuration(localOnly)을 쓰지 않는 이유
|
||||
// runtimeClasspath 에서 빠지면 IntelliJ 가 모듈 classpath 를 구성할 때도 빠져
|
||||
// IDE 로 기동할 때 Actuator/SBA 가 동작하지 않는다.
|
||||
def localOnlyLibPrefixes = [
|
||||
'spring-boot-devtools',
|
||||
'spring-boot-starter-actuator',
|
||||
'spring-boot-actuator', // spring-boot-actuator, -autoconfigure 모두 매칭
|
||||
'micrometer-', // actuator 전용(runtimeClasspath 상 다른 출처 없음 — 확인함)
|
||||
'spring-boot-admin-',
|
||||
]
|
||||
|
||||
dependencies {
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28"
|
||||
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
|
||||
implementation 'com.eactive.elink.common:elink-common-data:4.5.5'
|
||||
|
||||
// implementation 'io.swagger.core.v3:swagger-core:2.2.25'
|
||||
// implementation 'io.swagger.parser.v3:swagger-parser:2.1.23'
|
||||
implementation fileTree(dir: 'libs/swagger', include: ['*.jar'])
|
||||
// OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체.
|
||||
// swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는
|
||||
// 아직 swagger-core/parser 어느 버전도 미지원. 지원 추가 시 버전만 상향하면 됨. JDK8 호환.
|
||||
implementation 'io.swagger.core.v3:swagger-core:2.2.52' // io.swagger.v3.oas.models.*, io.swagger.v3.core.util.Json
|
||||
implementation 'io.swagger.parser.v3:swagger-parser:2.1.45' // io.swagger.parser.OpenAPIParser, io.swagger.v3.parser.*
|
||||
implementation 'io.swagger:swagger-annotations:1.6.14' // io.swagger.annotations.ApiModelProperty (CommissionSearch)
|
||||
|
||||
implementation project(':elink-online-core-jpa')
|
||||
implementation project(':elink-portal-common')
|
||||
// implementation project(':kjb-safedb')
|
||||
|
||||
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-validation')
|
||||
|
||||
// ↓ 로컬 전용. war/bootWar 산출물에서는 localOnlyLibPrefixes 로 제외된다.
|
||||
// runtimeOnly 인 이유: compileClasspath 에서 빠지므로 자바 코드가 이 API 를 참조하면
|
||||
// 컴파일 단계에서 막힌다. implementation 이면 참조가 컴파일에 통과해버리고,
|
||||
// 배포본(actuator/SBA 제외됨)에서 NoClassDefFoundError 로 터진다.
|
||||
// IntelliJ / bootRun 은 runtimeClasspath 기준이라 로컬 기동에는 정상 포함된다.
|
||||
// 설정(application-local*.yml)으로만 사용한다.
|
||||
runtimeOnly('org.springframework.boot:spring-boot-starter-actuator')
|
||||
// Spring Boot Admin client. 2.7.16 = Spring Boot 2.7.x 대응 마지막 계열(Java 8 호환).
|
||||
runtimeOnly('de.codecentric:spring-boot-admin-starter-client:2.7.16')
|
||||
// 위 runtimeOnly 원칙의 유일한 예외. InterceptorsEndpointConfig 가 actuator API
|
||||
// (@Endpoint / InfoContributor)를 참조해야 하므로 compileClasspath 에만 올린다.
|
||||
// compileOnly 는 runtimeClasspath 에 포함되지 않으므로 war/bootWar 산출물에는 영향이 없다.
|
||||
// 해당 클래스는 @ConditionalOnClass 로 actuator 부재 시 로드되지 않으니
|
||||
// 배포본(actuator 제외)에서 NoClassDefFoundError 가 나지 않는다.
|
||||
compileOnly('org.springframework.boot:spring-boot-actuator')
|
||||
|
||||
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
|
||||
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
|
||||
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
// Thymeleaf 3.1 코어가 #temporals 를 내장 제공 → java8time extras 를 함께 두면 표현식 객체가 중복 등록된다.
|
||||
implementation('org.springframework.boot:spring-boot-starter-thymeleaf') {
|
||||
exclude group: 'org.thymeleaf.extras', module: 'thymeleaf-extras-java8time'
|
||||
}
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation('org.springframework.boot:spring-boot-starter-cache')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
@@ -53,32 +94,48 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
implementation 'org.springframework:spring-expression:5.3.30'
|
||||
// spring-expression 개별 pin 제거: 아래 ext 의 spring-framework.version 이 전 모듈을 일괄 관리한다.
|
||||
// 개별 pin 이 남아 있으면 다른 spring-* 모듈보다 낮은 버전으로 고정되어 버전이 어긋난다.
|
||||
|
||||
implementation group: 'xalan', name: 'xalan', version: '2.7.3'
|
||||
|
||||
implementation 'org.hibernate:hibernate-envers:5.6.15.Final'
|
||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0'
|
||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0' // 3.0.0 은 thymeleaf 3.0 전용
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
implementation 'com.github.ua-parser:uap-java:1.5.3'
|
||||
// uap-java 제거: 소스 전체에 ua_parser 참조 0건이고, 1.5.3 은 snakeyaml 2.x 에서 삭제된
|
||||
// SafeConstructor() no-arg 를 호출해 NoSuchMethodError 를 낸다. 되살릴 경우 1.6.1 이상.
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1'
|
||||
implementation 'javax.servlet:javax.servlet-api:4.0.0'
|
||||
implementation 'org.jasypt:jasypt:1.9.3'
|
||||
implementation 'xerces:xercesImpl:2.12.2'
|
||||
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
// weblogic.xml prefer-application-packages 가 com.fasterxml.jackson.* 를 앱 우선으로 강제한다
|
||||
// (oracle_common jackson-databind 2.17.0 vs 앱 2.18.10 split-package 방지, WLog 실사용 확인).
|
||||
// Thymeleaf StandardJavaScriptSerializer 가 ObjectMapper.findAndRegisterModules() 로
|
||||
// jackson-module-jaxb-annotations 를 옵션 모듈로 자동탐색하는데, 이 모듈은 WebLogic 번들에만
|
||||
// 있고 앱엔 없어 ServiceConfigurationError(Provider not found) 발생 → 앱에도 명시 추가해
|
||||
// jackson 전체를 자체 완결시킨다(jackson-bom 2.18.10 로 버전 통일).
|
||||
implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations'
|
||||
|
||||
// Uncontrolled recursion in ClassUtils.getClass(...) on very long inputs. 3.18.0+ 에서 수정.
|
||||
implementation 'org.apache.commons:commons-lang3:3.20.0'
|
||||
implementation 'org.apache.commons:commons-collections4:4.4'
|
||||
|
||||
implementation 'commons-net:commons-net:3.9.0'
|
||||
implementation('commons-beanutils:commons-beanutils:1.9.4') {
|
||||
// CVE-2025-48734 (PropertyUtilsBean 이 enum 의 declaredClass 프로퍼티 노출 → ClassLoader 접근/RCE).
|
||||
// 1.11.0 부터 SuppressPropertiesBeanIntrospector 가 기본 활성이라 declaredClass 접근이 차단된다.
|
||||
// 이 앱의 호출부(PasswordMatchValidator / PasswordRuleValidator / AuthNumberValidator)는
|
||||
// 어노테이션에 박힌 고정 프로퍼티명만 넘기므로 외부 입력 경로는 없지만 버전은 올려 둔다.
|
||||
// 1.11.0 = Java 8 바이트코드(major 52), PropertyUtils.getProperty/getNestedProperty API 동일.
|
||||
implementation('commons-beanutils:commons-beanutils:1.11.0') {
|
||||
// exclude group: 'commons-collections', module: 'commons-collections'
|
||||
}
|
||||
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.3'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.3'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
|
||||
// jackson 개별 pin 제거: 아래 ext 의 jackson-bom.version 이 전 모듈을 일괄 관리한다.
|
||||
// 개별 pin 은 BOM 보다 우선하므로 남겨 두면 BOM 만 올렸을 때 core/annotations/databind 가
|
||||
// 옛 버전에 고정돼 버전이 어긋난다(실제로 그런 상태였다).
|
||||
|
||||
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
|
||||
|
||||
@@ -88,7 +145,10 @@ dependencies {
|
||||
implementation 'net.bytebuddy:byte-buddy:1.14.5'
|
||||
|
||||
// Commons FileUpload (WAS 독립적인 multipart 처리)
|
||||
implementation 'commons-fileupload:commons-fileupload:1.5'
|
||||
// CVE-2025-48976 (멀티파트 파트 헤더 크기 제한 부재 → DoS). 1.6.0 에서 partHeaderSizeMax 도입.
|
||||
// 주의: 1.6 부터 파트 헤더 총량 기본 상한이 10240 → 512 바이트로 줄었다(DEFAULT_PART_HEADER_SIZE_MAX).
|
||||
// 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 길어질 수 있어 실측으로 여유를 확인했다.
|
||||
implementation 'commons-fileupload:commons-fileupload:1.6.0'
|
||||
implementation 'commons-io:commons-io:2.15.1'
|
||||
|
||||
|
||||
@@ -106,6 +166,49 @@ ext {
|
||||
springMavenArtifactVersion = '5.3.30'
|
||||
encoding = 'UTF-8'
|
||||
profile = 'local'
|
||||
|
||||
// 내장 Tomcat 버전 상향 (Spring Boot 2.7.18 기본값 9.0.83 → 9.0.120).
|
||||
// Boot BOM 의 tomcat.version 프로퍼티를 덮어써서 tomcat-embed-core/-el/-websocket 이 함께 올라간다.
|
||||
// 9.0.x 계열 유지 = Servlet 4.0 / javax.* 네임스페이스 그대로, JDK8 호환.
|
||||
set('tomcat.version', '9.0.120')
|
||||
|
||||
// CVE-2022-1471 (snakeyaml Constructor 임의 타입 역직렬화 → RCE). Boot 2.7.18 BOM 기본값 1.30 → 2.6.
|
||||
// 앱/프레임워크 실경로는 이미 SafeConstructor 계열(OriginTrackedYamlLoader, spring-beans
|
||||
// FilteringConstructor, swagger-parser DeserializationUtils)이라 익스플로잇 경로는 없었으나
|
||||
// SCA 는 버전으로 판정하므로 2.x 로 올린다. snakeyaml 2.6 = Java 8 바이트코드(major 52).
|
||||
// 2.6 인 이유: swagger-core 2.2.52 / swagger-parser-v3 2.1.45 가 요구하는 버전이라 강등이 없다.
|
||||
// 부수 효과: swagger-parser 가 호출하는 LoaderOptions.setCodePointLimit(1.32+ API) 도 해소.
|
||||
set('snakeyaml.version', '2.6')
|
||||
|
||||
// jackson 전 모듈 버전 통일(Boot 2.7.18 BOM 기본 2.13.5). 2.18.x 는 JDK8 호환 라인이다.
|
||||
// 이유 3가지
|
||||
// 1) snakeyaml 2.x 는 ParserImpl(StreamReader) 를 제거했고 jackson-dataformat-yaml 은
|
||||
// 2.15+ 부터 ParserImpl(StreamReader, LoaderOptions) 를 쓴다 — 위 snakeyaml 상향의 전제.
|
||||
// 2) WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정.
|
||||
// 3) jackson-databind PolymorphicTypeValidator 우회(제네릭 타입 인자 미검증) — 2.18.8 에서 수정.
|
||||
// 이 앱은 다형성 역직렬화(activateDefaultTyping/@JsonTypeInfo)를 쓰지 않아 노출 경로는 없다.
|
||||
// 2.18.x 마지막 패치를 쓴다.
|
||||
set('jackson-bom.version', '2.18.10')
|
||||
|
||||
// Thymeleaf SSTI (≤3.1.3.RELEASE: 표현식 접근 객체 제한 우회 → 템플릿 인젝션). 3.0.x 는 EOL 이라
|
||||
// 백포트가 없어 3.1.4 로 올린다. JDK8/Spring5 유지: thymeleaf 3.1.4 / thymeleaf-spring5 3.1.4 /
|
||||
// extras-springsecurity5 3.1.5 / layout-dialect 3.4.0 모두 Java8 바이트코드(major 52), 패키지도
|
||||
// org.thymeleaf.spring5 + javax.servlet 그대로다.
|
||||
// Boot 2.7 ThymeleafAutoConfiguration 이 호출하는 setter 는 3.1.4 에 전부 존재함(확인함).
|
||||
// 주의: 3.1 은 #request/#session/#response/#servletContext 표현식 객체를 제거했다(IllegalArgumentException).
|
||||
set('thymeleaf.version', '3.1.4.RELEASE')
|
||||
set('thymeleaf-extras-springsecurity.version', '3.1.5.RELEASE')
|
||||
|
||||
// Spring Framework 5.3.x OSS 마지막 릴리스로 통일(Boot 2.7.18 BOM 기본 5.3.31, 일부 5.3.30 혼재였음).
|
||||
// 5.3.32~5.3.39 구간 CVE 정리용. 5.3.x 는 OSS EOL 이라 이 위로는 상용(Enterprise) 빌드뿐이다.
|
||||
// 남는 5.3.x 지적(CVE-2026-41855 JMS Jackson 역직렬화, CVE-2016-1000027 HttpInvoker)은
|
||||
// 5.3 계열에 수정본이 없고 앱이 JMS/HttpInvoker 를 쓰지 않으므로 억제 항목으로 따로 관리한다.
|
||||
set('spring-framework.version', '5.3.39')
|
||||
|
||||
// Spring Security 5.7.x OSS 마지막 릴리스(Boot 2.7.18 BOM 기본 5.7.11).
|
||||
// CVE-2026-22732(응답 커밋 후 보안 헤더 미기록)는 수정본이 5.7.22/5.8.24 = Enterprise 전용이라
|
||||
// OSS 로는 올릴 수 없다. 우회책(HeaderWriterFilter.shouldWriteHeadersEagerly=true)은 동작 변경이라 분리 검토.
|
||||
set('spring-security.version', '5.7.14')
|
||||
}
|
||||
|
||||
|
||||
@@ -124,21 +227,40 @@ bootRun {
|
||||
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
|
||||
|
||||
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
|
||||
args = ["--spring.profiles.active=dev"]
|
||||
|
||||
// 로컬 기동 기본 프로파일.
|
||||
// dev/stage/prod 는 리버스프록시(OHS) 뒤 WAR 배포용이라 server.forward-headers-strategy=framework 가
|
||||
// 걸려 있다. framework 는 신뢰 프록시 목록 없이 X-Forwarded-* 를 그대로 신뢰하므로 앞단이 없는
|
||||
// 로컬 기동에는 쓰지 않는다(로컬은 공통 기본값 native = Tomcat RemoteIpValve, 사설대역만 신뢰).
|
||||
// 다른 프로파일로 띄우려면: gradle bootRun -PbootProfile=dev
|
||||
// ('profile' 이 아니라 'bootProfile' 인 이유: 위 ext 블록이 profile='local' 을 이미 점유하고 있어
|
||||
// findProperty('profile') 은 -P 지정 여부와 무관하게 항상 'local' 을 돌려준다.)
|
||||
def bootRunProfile = (project.findProperty('bootProfile') ?: 'local_rinjaemac').toString()
|
||||
args = ["--spring.profiles.active=" + bootRunProfile]
|
||||
}
|
||||
|
||||
|
||||
// annotation processor 출력 디렉토리는 IntelliJ/Gradle 통합이 자동 등록함.
|
||||
// 명시적으로 srcDir 추가하면 IntelliJ idea 모드에서 MapStruct가 같은 파일을
|
||||
// 중복 생성하려다 javax.annotation.processing.FilerException 발생함.
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir 'src/main/java'
|
||||
srcDir 'build/generated/sources/annotationProcessor/java/main'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
annotationProcessor
|
||||
|
||||
// WebLogic 배포 시 Tyrus WebSocket 필터(weblogic.websocket.tyrus.TyrusServletFilter)와
|
||||
// 충돌 방지: WAR 에 번들된 Tomcat WsSci 가 javax.websocket.server.ServerContainer 속성을
|
||||
// WsServerContainer 로 등록 → WebLogic Tyrus 필터가 TyrusServerContainer 로 캐스팅하다 실패.
|
||||
// 앱은 WebSocket 미사용이므로 Tomcat WebSocket 모듈 제외.
|
||||
all {
|
||||
exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket'
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -148,20 +270,39 @@ compileJava {
|
||||
}
|
||||
|
||||
processResources {
|
||||
exclude { details ->
|
||||
details.file.name.startsWith('application-') &&
|
||||
details.file.name.endsWith('.yml') &&
|
||||
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
}
|
||||
// exclude { details ->
|
||||
// details.file.name.startsWith('application-') &&
|
||||
// details.file.name.endsWith('.yml') &&
|
||||
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
// }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
useJUnitPlatform {
|
||||
if (project.hasProperty('includeE2E') && project.includeE2E.toBoolean()) {
|
||||
includeTags 'e2e'
|
||||
} else {
|
||||
excludeTags 'e2e'
|
||||
}
|
||||
}
|
||||
enabled = true
|
||||
}
|
||||
|
||||
// 로컬 전용 설정 파일. 배포 산출물(WAR)에 실리면 Actuator/SBA 설정이 그대로 노출되어
|
||||
// 정보보호 점검에 불필요하게 걸린다. processResources 는 건드리지 않는다
|
||||
// (bootRun 이 build/resources/main 을 그대로 쓰므로 로컬 기동이 깨진다).
|
||||
def localOnlyResources = ['**/application-local*.yml']
|
||||
|
||||
// 배포 산출물에서 로컬 전용 라이브러리를 걸러낸다. localOnlyLibPrefixes 는 파일 상단 정의.
|
||||
def excludeLocalOnlyLibs = { org.gradle.api.file.FileCollection cp ->
|
||||
cp.filter { f -> !localOnlyLibPrefixes.any { p -> f.name.startsWith(p) } }
|
||||
}
|
||||
|
||||
bootWar {
|
||||
archiveFileName = "eapim-portal-boot.war"
|
||||
mainClass = 'com.eactive.apim.portal.PortalApplication'
|
||||
rootSpec.exclude(localOnlyResources)
|
||||
classpath = excludeLocalOnlyLibs(classpath)
|
||||
}
|
||||
|
||||
war {
|
||||
@@ -170,6 +311,8 @@ war {
|
||||
from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' }
|
||||
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
|
||||
|
||||
rootSpec.exclude(localOnlyResources)
|
||||
classpath = excludeLocalOnlyLibs(classpath)
|
||||
}
|
||||
|
||||
task printSourceSets {
|
||||
@@ -182,19 +325,5 @@ task printSourceSets {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CryptoCli 실행 task (bcrypt 지원)
|
||||
task cryptoCli(type: JavaExec) {
|
||||
mainClass = 'com.eactive.ext.kjb.safedb.CryptoCli'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
standardInput = System.in
|
||||
|
||||
// SafeDB 라이브러리 경로 추가 (리눅스)
|
||||
classpath += files("/safedb/JavaAPI/config")
|
||||
classpath += fileTree(dir: "/safedb/JavaAPI/lib", include: ["*.jar"])
|
||||
|
||||
// CLI 인자 전달: ./gradlew cryptoCli --args="bcrypt hash password123"
|
||||
if (project.hasProperty('args')) {
|
||||
args project.args.split('\\s+')
|
||||
}
|
||||
}
|
||||
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
|
||||
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
./gradlew bootWar
|
||||
docker build -t eactive-portal .
|
||||
|
||||
@@ -0,0 +1,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,121 @@
|
||||
<?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>
|
||||
-->
|
||||
|
||||
<!-- ================================================================================
|
||||
Spring 계열 Critical 5건 (2026-08-18 리포트 기준).
|
||||
|
||||
공통 배경: Spring Framework 5.3.x / Spring Boot 2.7.x / Spring Security 5.7.x 는 모두 OSS EOL 이라
|
||||
남은 수정본이 상용(Enterprise/Tanzu)뿐이다. JDK 8 유지 제약상 Boot 3 + Spring 6 이관 전까지는
|
||||
버전 상향으로 못 없앤다. 아래는 "코드에 트리거 경로가 없음"을 근거로 한 억제이며 만료일을 둔다.
|
||||
버전 범위를 5.3.x / 2.7.x / 5.7.x 로 고정해 두었으므로, 이관 후에는 억제가 자동으로 풀린다.
|
||||
재검토 시 확인할 것: (1) 각 근거 grep 이 여전히 0건인지 (2) OSS 수정본이 나왔는지.
|
||||
================================================================================ -->
|
||||
|
||||
<!-- 1. Spring Boot: Cassandra SSL 호스트명 미검증 -->
|
||||
<suppress until="2027-02-28Z">
|
||||
<notes><![CDATA[
|
||||
CVE-2026-40974 는 Spring Boot 의 Cassandra SSL 자동설정이 SSL 번들의 호스트명 검증 설정을
|
||||
드라이버에 전달하지 않는 문제다. 이 앱은 Cassandra 를 쓰지 않는다
|
||||
(runtimeClasspath 에 cassandra/datastax 계열 jar 0건, spring-boot-starter-data-cassandra 미선언).
|
||||
수정본 2.7.33 은 상용(Enterprise) 릴리스라 OSS 로는 올릴 수 없다.
|
||||
devtools/actuator/spring-boot-admin jar 는 스캔 대상(runtimeClasspath)에는 있으나
|
||||
배포 산출물에서는 build.gradle 의 localOnlyLibPrefixes 로 제외되어 WAR 에 실리지 않는다.
|
||||
확인: Rinjae / 2026-08-18 / grep -ri cassandra 0건, WAR 내 devtools/actuator jar 0건.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/.*@2\.7\..*$</packageUrl>
|
||||
<cve>CVE-2026-40974</cve>
|
||||
</suppress>
|
||||
|
||||
<!-- 2. Thymeleaf SSTI 2건: starter jar 이름 기준 오탐 -->
|
||||
<suppress>
|
||||
<notes><![CDATA[
|
||||
오탐. CVE-2026-40477 / CVE-2026-40478 은 thymeleaf 본체 3.1.3.RELEASE 이하의 표현식 샌드박스
|
||||
우회 문제이고 3.1.4.RELEASE 에서 수정됐다. 이 프로젝트는 build.gradle 의 ext 에서
|
||||
thymeleaf.version=3.1.4.RELEASE / thymeleaf-extras-springsecurity.version=3.1.5.RELEASE 로
|
||||
올려 두었으므로 실제 실리는 jar 는 thymeleaf-3.1.4.RELEASE.jar 다.
|
||||
Dependency-Check 가 spring-boot-starter-thymeleaf-2.7.18.jar(의존만 선언한 빈 starter)에
|
||||
thymeleaf CPE 를 잘못 매칭한 결과다. thymeleaf 본체 jar 에 대한 탐지는 억제하지 않는다.
|
||||
확인: Rinjae / 2026-08-18 / WAR 내 thymeleaf-3.1.4.RELEASE.jar, thymeleaf-spring5-3.1.4.RELEASE.jar.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/spring-boot-starter-thymeleaf@.*$</packageUrl>
|
||||
<cve>CVE-2026-40477</cve>
|
||||
<cve>CVE-2026-40478</cve>
|
||||
</suppress>
|
||||
|
||||
<!-- 3. Spring Framework: JMS Jackson 역직렬화 -->
|
||||
<suppress until="2027-02-28Z">
|
||||
<notes><![CDATA[
|
||||
CVE-2026-41855 는 org.springframework.jms.support.converter.MappingJackson2MessageConverter /
|
||||
JacksonJsonMessageConverter 가 임의 클래스 인스턴스화를 허용하는 문제다(신뢰할 수 없는 JMS 환경 전제).
|
||||
이 앱은 JMS 를 쓰지 않는다: runtimeClasspath 에 spring-jms 0건, 소스에 javax.jms /
|
||||
JmsTemplate / MappingJackson2MessageConverter 참조 0건.
|
||||
5.3.x 는 OSS 수정본이 없다(6.2.19 / 7.0.8 에서만 수정). 이미 5.3.x OSS 마지막인 5.3.39 로 올려 둔 상태다.
|
||||
확인: Rinjae / 2026-08-18 / grep -rn "javax.jms|JmsTemplate|MappingJackson2MessageConverter" src 0건.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-.*@5\.3\..*$</packageUrl>
|
||||
<cve>CVE-2026-41855</cve>
|
||||
</suppress>
|
||||
|
||||
<!-- 4. spring-web: HttpInvoker 역직렬화 -->
|
||||
<suppress until="2027-02-28Z">
|
||||
<notes><![CDATA[
|
||||
CVE-2016-1000027 은 HttpInvokerServiceExporter 를 노출했을 때만 성립한다. 해당 클래스는
|
||||
Spring 6.0 에서 제거됐고 5.3.x 에는 수정본이 없다(= 5.3.x 를 쓰는 한 계속 탐지된다).
|
||||
이 앱은 HttpInvoker 계열을 쓰지 않는다: 소스에 HttpInvoker 참조 0건이고 외부 호출은
|
||||
RestTemplate / HttpClient 기반이다.
|
||||
근본 해결은 Spring 6(Boot 3, JDK 17) 이관. 이관 전까지 억제한다.
|
||||
확인: Rinjae / 2026-08-18 / grep -rn HttpInvoker src 0건.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-web@5\.3\..*$</packageUrl>
|
||||
<cve>CVE-2016-1000027</cve>
|
||||
</suppress>
|
||||
|
||||
<!-- 5. Spring Security: 응답 커밋 시 보안 헤더 미기록 -->
|
||||
<suppress until="2027-02-28Z">
|
||||
<notes><![CDATA[
|
||||
CVE-2026-22732 는 응답이 커밋된 뒤 Spring Security 가 보안 헤더를 기록하지 못하는 문제다.
|
||||
트리거는 Content-Length 를 setHeader / setIntHeader / addIntHeader 로 지정하는 경로다
|
||||
(OnCommittedResponseWrapper 는 setContentLength / setContentLengthLong / addHeader 만 추적한다).
|
||||
(1) 이 코드베이스에는 트리거가 없다: portal / elink-portal-common / elink-online-core-jpa 전체에
|
||||
setIntHeader, addIntHeader, setHeader("Content-Length") 0건. 파일 다운로드는
|
||||
response.setContentLength(int) 를 쓰며 실측상 보안 헤더가 정상 기록된다.
|
||||
(2) 그럼에도 안전망으로 PortalConfigSecurity 에서
|
||||
HeaderWriterFilter.setShouldWriteHeadersEagerly(true) 를 적용해(spring.io 권고 우회책)
|
||||
요청 시작 시점에 헤더를 기록하도록 했다.
|
||||
수정본 5.7.22 / 5.8.24 는 Enterprise 전용이라 OSS 로는 올릴 수 없다. 이미 5.7.x OSS 마지막인
|
||||
5.7.14 로 올려 둔 상태다.
|
||||
확인: Rinjae / 2026-08-18 / 동일 스택(Boot 2.7.18 + Security 5.7.14 + Tomcat 9.0.120) 프로브 앱 실측 —
|
||||
우회책 미적용 시 setHeader/setIntHeader/addIntHeader 경로에서 헤더 누락 재현, 적용 후 정상 기록.
|
||||
]]></notes>
|
||||
<packageUrl regex="true">^pkg:maven/org\.springframework\.security/spring-security-.*@5\.7\..*$</packageUrl>
|
||||
<cve>CVE-2026-22732</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,17 @@
|
||||
<!--
|
||||
editor-content.css 대응 프래그먼트: <!doctype>/<html>/<head>/<style> 없이 순수 태그(h1/h2/h3/p/ul/li/strong)만 사용.
|
||||
admin Summernote 코드뷰에 그대로 붙여넣으면 됨. class/style 속성 없음 -> 붙여넣기(paste-cleaner)든
|
||||
코드뷰 직접 입력이든 태그 정리 과정에서 잘려나갈 것이 없다.
|
||||
-->
|
||||
<h1>개인정보수집동의서</h1>
|
||||
<p><strong>(주)제주은행 귀중</strong></p>
|
||||
<p>귀 행과의 Open API Market 회원가입 거래와 관련하여 귀 행이 본인의 개인정보를 수집·이용하고자 하는 경우에는 「개인정보보호법」 등 관계 법령에 따라 본인의 동의가 필요합니다.</p>
|
||||
<h2>개인정보</h2>
|
||||
<ul>
|
||||
<li><strong>일반 개인정보:</strong> 성명, 직장명, 부서명, 연락처(휴대폰 번호, 직장번호), 이메일주소, 아이디, 비밀번호</li>
|
||||
</ul>
|
||||
<p>□ 동의하지 않음 □ 동의함</p>
|
||||
<p>※ 본 동의 이후 동일한 목적 또는 이용범위 내에서 개인정보의 정확성, 최신성을 유지하기 위해 제공하는 경우에는 별도의 추가 동의가 필요하지 않습니다.</p>
|
||||
<p>※ 은행의 고의 또는 과실 등 귀책사유로 인한 개인정보 유출로 고객님에게 발생한 손해에 대해 관계 법령 등에 따라 보상받으실 수 있습니다.</p>
|
||||
<h3>위 개인정보 수집·이용에 동의하십니까?</h3>
|
||||
<p>제정일자: 2026. 99. 99. 개정</p>
|
||||
@@ -0,0 +1,19 @@
|
||||
개인정보수집동의서
|
||||
|
||||
(주)제주은행 귀중
|
||||
|
||||
귀 행과의 Open API Market 회원가입 거래와 관련하여 귀 행이 본인의 개인정보를 수집·이용하고자 하는 경우에는 「개인정보보호법」 등 관계 법령에 따라 본인의 동의가 필요합니다.
|
||||
|
||||
개인정보
|
||||
ㄴ 일반 개인정보
|
||||
성명, 직장명, 부서명, 연락처(휴대폰 번호, 직장번호), 이메일주소, 아이디, 비밀번호
|
||||
|
||||
□ 동의하지 않음 □ 동의함
|
||||
|
||||
※ 본 동의 이후 동일한 목적 또는 이용범위 내에서 개인정보의 정확성, 최신성을 유지하기 위해 제공하는 경우에는 별도의 추가 동의가 필요하지 않습니다.
|
||||
|
||||
※ 은행의 고의 또는 과실 등 귀책사유로 인한 개인정보 유출로 고객님에게 발생한 손해에 대해 관계 법령 등에 따라 보상받으실 수 있습니다.
|
||||
|
||||
위 개인정보 수집·이용에 동의하십니까?
|
||||
|
||||
제정일자: 2026. 99. 99. 개정
|
||||
@@ -0,0 +1,162 @@
|
||||
<!--
|
||||
editor-content.css 대응 프래그먼트: <!doctype>/<html>/<head>/<style> 없이 순수 태그(h2/h3/p)만 사용.
|
||||
admin Summernote 코드뷰에 그대로 붙여넣으면 됨(붙여넣기 시 badTags 정리 대상 없음).
|
||||
약관탭(.content-body.editor-content), 회원가입 동의창, admin .note-editable.editor-content 3곳 모두
|
||||
이 파일 안에서는 정의하지 않는 h2/h3/p 스타일은 공용 editor-content.css 가 담당한다.
|
||||
-->
|
||||
<h2>제1장 총칙</h2>
|
||||
<h3>제1조 (목적)</h3>
|
||||
<p>본 약관은 (주)제주은행(이하 “은행”)이 제공하는 Open API 서비스를 이용함에 있어 “은행”과 Open API 서비스를 이용하는 자(이하 “이용기관”)간의 권리, 의무 및 책임 사항, 이용조건 및 절차 등 기본적인 사항을 명확히 규정함을 목적으로 합니다.</p>
|
||||
<h3>제2조 (용어의 정의)</h3>
|
||||
<p> ① 본 약관에서 사용하는 용어의 정의는 다음 각 호와 같습니다.</p>
|
||||
<p> 1. “Open API”란 “은행”이 “이용기관”에게 제공하는 API(Application Programming Interface) 기능을 개방형 플랫폼 형태로 이용할 수 있도록 공개한 API</p>
|
||||
<p> 2. “Open API 서비스”(이하 “서비스”)란 “은행”의 서비스 및 데이터를 표준화된 API 형태로 Open API 시스템을 통하여 “이용기관”에 제공하는 것</p>
|
||||
<p> 3. “Open API 시스템”(이하 “시스템”)이란 “서비스”를 제공하기 위하여 “은행”에 설치한 중계시스템과 “이용기관”의 처리시스템을 통신회선으로 연결하여 “은행” 및 “이용기관” 상호간에 정보를 교환하는 시스템</p>
|
||||
<p> 4. “Open API 포탈”(이하 “포털”)이란 “서비스”를 소개하고 가입 절차 및 이용 절차를 제공하며 “이용기관”이 가입 및 “서비스” 이용을 하기 위하여 “은행”이 제공하는 웹사이트</p>
|
||||
<p> 5. “이용신청기관”이란 “은행”의 “서비스”를 이용하기 위하여 “서비스” 이용을 신청한 기관(법인, 개인사업자 및 임의단체 등)</p>
|
||||
<p> 6. “이용기관”이란 본 약관에 동의하고 “서비스” 이용신청에 대한 “은행”의 승낙을 받은 후, “은행”이 제공하는 Open API를 이용하여 별도의 서비스 또는 프로그램을 개발, 테스트 및 제공하고자 하는 자</p>
|
||||
<p> 7. “최종사용자”란 “은행”이 제공한 “서비스”를 결합하여 “이용기관”이 개발한 별도의 서비스 또는 프로그램을 이용하는 고객</p>
|
||||
<p> 8. “제휴사”란 “은행”이 “이용기관”에게 “서비스”를 제공하기 위하여 “은행”과 계약을 체결하여 협력하는 제3자</p>
|
||||
<h3>제3조 (서비스의 내용)</h3>
|
||||
<p> ① 조회성 API 서비스(계좌 거래내역 조회, 예금주 실명 조회 등)</p>
|
||||
<p> ② 거래성 API 서비스(환전, 이체 등)</p>
|
||||
<p> ③ 기타 “은행” 또는 “제휴사”가 제공하는 API 형태의 “서비스”</p>
|
||||
<h3>제4조 (약관의 효력 및 변경)</h3>
|
||||
<p> ① 본 약관 내용의 효력은 “은행” 인터넷 홈페이지 또는 “포털” 등에 게시되거나 전자우편 등의 방법으로 “이용기관”에 통지됨으로써 발생합니다.</p>
|
||||
<p> ② 본 약관의 변경은 전자금융거래 기본약관의 내용을 준용합니다.</p>
|
||||
<h2>제2장 서비스 이용계약</h2>
|
||||
<h3>제5조 (서비스 이용신청)</h3>
|
||||
<p> ① 서비스 이용신청은 “이용신청기관”이 “포털”에서 “은행”이 정한 가입양식에 회원정보 기입 및 본 약관에 동의함으로써 이루어집니다.</p>
|
||||
<p> ② 제1항을 완료한 “이용신청기관”은 이용 희망일 전일까지 “은행”이 요청하는 서류를 제출하여야 합니다.</p>
|
||||
<h3>제6조 (서비스 이용신청 승인 및 거절)</h3>
|
||||
<p> ① “은행”은 본 약관 제5조 제2항의 “이용신청기관”이 제출한 서류와 “서비스” 이용목적의 적정성을 검토한 후, “이용신청기관” 서비스 이용신청을 승낙 또는 거절할 수 있습니다.</p>
|
||||
<p> ② “은행”은 “이용신청기관”이 다음 각 호의 어느 하나에 해당할 경우 이용신청에 대해 거절할 수 있습니다.</p>
|
||||
<p> 1. 실명이 아니거나, 타인의 명의를 사용하여 신청한 경우</p>
|
||||
<p> 2. 이용신청 내용을 허위로 기재하거나 허위서류를 첨부한 경우</p>
|
||||
<p> 3. 이전에 “이용기관” 자격을 상실한 적이 있거나, 탈퇴, 이용중지 등이 있는 경우</p>
|
||||
<p> 4. “이용신청기관”의 업태가 사행행위에 관련이 있거나 금융질서문란 위험이 있는 경우</p>
|
||||
<p> 5. “이용신청기관”의 사업모델에 위법성이 있는 경우</p>
|
||||
<p> 6. “이용신청기관”의 재무안정성이 위험한 경우</p>
|
||||
<p> 7. “이용신청기관”의 전산망이나 정보처리시스템에 보안 취약점이 있거나, “이용신청기관”의 “최종사용자” 정보에 관한 기술적·물리적·관리적 보호조치가 미흡한 경우</p>
|
||||
<p> 8. “은행”의 평판이나 “최종사용자”의 권리에 위험을 끼칠 가능성이 있는 경우</p>
|
||||
<p> ③ “은행”은 이용신청에 대한 결과를 “이용신청자”에게 통보하여야 합니다.</p>
|
||||
<h2>제3장 서비스의 이용</h2>
|
||||
<h3>제7조 (서비스 이용개시)</h3>
|
||||
<p>“은행”이 “이용신청기관”의 이용신청을 심사 후 승낙하게 되면 “이용기관”이 요청한 이용 희망일로부터 “서비스”를 개시합니다. 단, 이용 희망일은 “은행”과 협의하여 결정합니다.</p>
|
||||
<h3>제8조 (서비스 이용시간)</h3>
|
||||
<p> ① “서비스”의 이용시간은 연중무휴(24시간)로 하며 “서비스”별 이용시간을 정하여 운영할 수 있습니다. 단, 다음 각 호의 어느 하나에 해당하여 은행이 정상적인 “서비스”를 제공하기 어려운 경우에는 서비스의 전부 또는 일부가 제한 될 수 있습니다.</p>
|
||||
<p> 1. 은행의 시스템을 점검하는 경우</p>
|
||||
<p> 2. 은행의 정보통신 설비의 보수점검, 증설, 교체, 이전 등 시스템 관리 작업을 진행하는 경우</p>
|
||||
<p> 3. 정전, 제반 설비의 장애 또는 이용량의 폭주 등이 발생하는 경우</p>
|
||||
<p> 4. 천재지변, 전쟁, 폭동, 테러, 해킹, Ddos, 국가비상사태 등 불가항력적 사유가 발생하는 경우</p>
|
||||
<p> 5. 시스템 오류 등 서비스 제공에 관한 기술상 문제가 발생하는 경우</p>
|
||||
<p> ② 제1항의 사유로 서비스가 일시 중지되는 경우, “은행”은 사전에 이를 공지하여야 합니다. 단, 부득이한 경우 사후에 통보할 수 있습니다.</p>
|
||||
<h3>제9조 (서비스 변경 및 중단)</h3>
|
||||
<p> ① “은행”은 변경될 “서비스”의 내용 및 변경일자를 “이용기관”에게 통지하고 “서비스”를 변경하여 제공할 수 있습니다.</p>
|
||||
<p> ② “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “서비스”의 전부 또는 일부를 중단할 수 있습니다.</p>
|
||||
<p> 1. “서비스”용 정보통신설비의 보수·교체·고장, 통신두절 등으로 인해 부득이한 경우</p>
|
||||
<p> 2. 정전, 모든 설비의 장애 또는 이용량의 폭주 등으로 정상적인 서비스 이용에 지장이 있는 경우</p>
|
||||
<p> 3. “서비스” 종료 등과 같은 “은행”의 사정으로 “서비스”를 유지할 수 없는 경우</p>
|
||||
<p> 4. 기타 천재지변, 국가비상사태 등 불가항력적 사유가 있는 경우</p>
|
||||
<p> ③ “서비스”를 변경하거나 중단할 경우, “은행”은 그 사실을 사전에 “이용기관”에 통지하여야 합니다. 단, 사전에 통지할 수 없는 부득이한 사유가 있는 경우에는 사후에 그 사유와 함께 통지하여야 합니다. 또한 “포털”에 등록된 연락처가 부정확하거나 “이용기관” 측의 사유로 인하여 “은행”의 통보를 받지 못해 발생하는 손실에 대해서는 “은행”의 고의 또는 과실이 없는 경우 그 책임의 전부 또는 일부를 지지 않습니다.</p>
|
||||
<p> ④ “은행”은 “서비스”의 변경 혹은 중단으로 발생하는 문제에 대해서는 “은행”의 고의 또는 과실이 없는 경우 그 책임의 전부 또는 일부를 지지 않습니다.</p>
|
||||
<h3>제10조 (서비스 이용 제한)</h3>
|
||||
<p> ① “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “이용기관”에 대한 “서비스” 이용을 개별 고지 후 제한할 수 있습니다.</p>
|
||||
<p> 1. 감독당국으로부터 중지 권고가 있는 경우</p>
|
||||
<p> 2. “이용기관”이 휴업 또는 폐업을 하는 경우</p>
|
||||
<p> 3. 접근권한을 넘어서는 접속을 시도하거나 반복적인 접속을 시도하여 과부하 발생 등 시스템 운영의 안전성을 저해한다고 판단되는 행위가 확인된 경우</p>
|
||||
<p> 4. “이용기관” 수수료 납부계좌에서 부족분이 3회 이상 발생할 경우</p>
|
||||
<p> 5. “이용기관”에 본 약관 제6조 제2항 각 호의 사유가 발생한 경우 또는 “서비스” 신청 당시 “이용기관”에 본 약관 제6조 제2항 각 호의 사유가 있었음이 밝혀진 경우</p>
|
||||
<p> 6. “시스템” 및 “포털”의 안정적인 운영을 위하여 필요하다고 인정되는 경우</p>
|
||||
<p> ② “이용기관”이 제1항에 해당할 경우, “은행”은 “서비스” 이용을 제한할 수 있으며 이와 별도로 해당 “이용기관”에 소명자료 제출을 요구할 수 있습니다. “이용기관”은 통지받은 날로부터 15일 이내에 소명자료를 제출하여야 합니다. 단, “은행”은 “이용기관”의 소명자료가 정당하다고 판단하기 전까지 “이용기관”의 “서비스” 이용을 제한할 수 있습니다.</p>
|
||||
<h3>제11조 (서비스 이용 해지)</h3>
|
||||
<p> ① “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “이용기관”에 대한 “서비스” 이용을 개별 고지 후 해지할 수 있습니다.</p>
|
||||
<p> 1. 감독당국으로부터 해지권고가 있는 경우</p>
|
||||
<p> 2. 최근 12개월간 “시스템” 이용 실적이 없는 경우</p>
|
||||
<p> 3. “이용기관” 수수료 납부계좌에서 부족분이 6회 이상 발생할 경우</p>
|
||||
<p> 4. “이용기관”의 정상적인 운영이 불가능하다고 “은행”이 판단할 경우</p>
|
||||
<p> ② “이용기관”이 제1항에 해당할 경우, “이용기관”에 소명자료 제출을 요구할 수 있습니다. “이용기관”은 통지받은 날로부터 15일 이내에 소명자료를 제출하여야 합니다.</p>
|
||||
<p> ③ 해지일은 “은행”이 “시스템” 또는 “포털” 원장에 해지 등록한 날로 합니다.</p>
|
||||
<h2>제4장 계약당사자의 의무 및 손해배상 등</h2>
|
||||
<h3>제12조 (은행의 의무)</h3>
|
||||
<p> ① “은행”은 본 약관에서 특별히 규정한 사유 외에는 “서비스”를 지속적·안정적으로 제공하여야 합니다.</p>
|
||||
<p> ② “은행”은 “서비스” 제공과 관련하여 알게 된 “이용기관”의 정보를 업무 이외의 목적으로 사용할 수 없습니다. 단, 관계법령에 따른 수사상의 관계기관으로부터 요구 받은 경우나 정보통신 윤리위원회의 요청이 있는 경우 등 법률의 규정에 따른 적법한 절차에 의한 경우에는 그러하지 않습니다.</p>
|
||||
<p> ③ “은행”이 제공하는 “서비스”로 인하여 “이용기관”에 손해가 발생한 경우 “서비스”를 제공하는 “은행”과 이용하는 “이용기관”이 상호협의에 의하여 해결합니다. 단, 그러한 손해가 “은행”의 고의나 과실에 기해 발생한 경우에 한하여 “은행”은 그 책임의 전부 또는 일부를 부담합니다.</p>
|
||||
<p> ④ “은행”은 개인정보보호법, 신용정보의 이용 및 보호에 관한 법률, 금융실명거래 및 비밀보장에 관한 법률, 정보통신망 이용촉진 및 정보보호에 관한 법률, 통신비밀보호법 등 “서비스” 운영, 유지와 관련 있는 법규를 준수하여야 합니다.</p>
|
||||
<h3>제13조 (약관 외 준칙)</h3>
|
||||
<p> ① “이용기관”은 “포털”에서 “은행”이 정한 가입양식에 회원정보를 기입할 때, 사실에 근거하여 정확하게 기입하여야 합니다.</p>
|
||||
<p> ② “이용기관”은 “은행”에 제공한 사항이 변경될 때, 즉시 “은행”에 변경 신청하여 변경사항을 최신의 정보로 수정해야 합니다. 수정하지 않은 정보로 인하여 발생하는 문제에 대한 책임은 “이용기관”에 있으며 “은행”은 이에 대해 아무런 책임을 지지 않습니다.</p>
|
||||
<p> ③ “이용기관”은 “시스템”과 관련한 “서비스” 이용의 대가로 수수료를 납부하여야 합니다.</p>
|
||||
<p> ④ “이용기관”은 “서비스”의 이용 권한, 기타 이용 계약상의 지위를 타인에게 양도, 증여할 수 없으며 게시물에 대한 저작권을 포함한 모든 권리 및 책임은 이를 게시한 “이용기관”에 있습니다.</p>
|
||||
<p> ⑤ “이용기관”은 “서비스”의 안정적인 이용을 위해 전산시스템의 구축에 필요한 회선, 단말기 및 부대기기를 “이용기관”의 부담으로 설치하여야 하고, 보안상 안전하게 처리할 수 있는 전산시스템을 갖추어야 합니다.</p>
|
||||
<p> ⑥ “이용기관”은 “은행”이나 “최종사용자”가 업무처리에 대하여 이의를 제기하거나 확인을 요청하면 이를 신속히 처리하여야 합니다.</p>
|
||||
<p> ⑦ “이용기관”은 본 약관에서 정한 사항을 준수하여야 하며, “은행”이 “이용기관”의 약관 이행여부를 점검하고자 할 경우 적극 협조하여야 합니다.</p>
|
||||
<p> ⑧ “이용기관”은 “포털”에서 제공하는 “서비스” 이용과 관련하여 다음 각 호의 행위를 하여서는 안 됩니다.</p>
|
||||
<p> 1. 다른 “이용기관”의 아이디를 부정 사용하는 행위</p>
|
||||
<p> 2. 범죄행위를 목적으로 하거나 기타 범죄행위와 관련된 행위</p>
|
||||
<p> 3. 선량한 풍속, 기타 사회질서를 해하는 행위</p>
|
||||
<p> 4. 타인의 명예를 훼손하거나 모욕하는 행위</p>
|
||||
<p> 5. 타인의 지식재산권 등의 권리를 침해하는 행위</p>
|
||||
<p> 6. 해킹행위 또는 컴퓨터 바이러스의 유포행위</p>
|
||||
<p> 7. 타인의 의사에 반하여 광고성 정보 등 일정한 내용을 지속적으로 전송하는 행위</p>
|
||||
<p> 8. 서비스의 안정적인 운영에 지장을 주거나 줄 우려가 있는 일체의 행위</p>
|
||||
<p> ⑨ “이용기관”은 개인정보보호법, 신용정보의 이용 및 보호에 관한 법률, 금융실명거래 및 비밀보장에 관한 법률, 정보통신망 이용촉진 및 정보보호에 관한 법률, 통신비밀보호법 등 “서비스” 운영, 유지와 관련 있는 법규를 준수하여야 합니다.</p>
|
||||
<h2>제5장 수수료</h2>
|
||||
<h3>제14조 (수수료)</h3>
|
||||
<p> ① “은행”은 “서비스” 제공의 대가로 “이용기관”에게 수수료를 부과합니다.</p>
|
||||
<p> ② “은행”은 매월 1일~말일까지 발생하는 수수료를 익월 10일(공휴일인 경우에는 익영업일)에 “이용기관”에게 부과하며, “이용기관”이 등록한 수수료 납부계좌에서 예금청구서 및 통장에 의하지 아니하고 대체 출금하기로 합니다.</p>
|
||||
<p> ③ “은행”은 “이용기관”이 해지 등의 사유로 “서비스” 이용이 불가한 경우, 이용불가 시점까지 발생한 미결제 수수료를 본 “서비스” 해지일에 예금청구서 및 통장에 의하지 아니하고 “이용기관”의 수수료 납부계좌에서 출금하고, 부족액이 발생할 시에 별도 청구합니다.</p>
|
||||
<p> ④ “은행”은 제2항의 규정에도 불구하고 전산상의 이유 등으로 “이용기관”이 등록한 수수료 납부계좌에서 예금청구서 및 통장에 의하지 아니하고 대체 출금할 수 없는 경우, “이용기관”에 “은행”이 정하는 다른 방법으로 수수료 납부를 요청할 수 있습니다.</p>
|
||||
<p> ⑤ “이용기관”에게 부과되는 수수료는 “은행”이 정하는 바에 따릅니다. 다만 “은행”이 사업적으로 필요하다고 판단되는 경우 서비스 이용에 따른 수수료를 감면할 수 있습니다.</p>
|
||||
<p> ⑥ 수수료는 은행의 정책에 따라 변경될 수 있으며, 은행이 수수료를 변경할 경우 전자금융거래 기본약관의 내용을 준용합니다.</p>
|
||||
<h2>제6장 보칙</h2>
|
||||
<h3>제15조 (최종사용자에 대한 고지 의무)</h3>
|
||||
<p> ① “이용기관”은 중요사항에 대해 “최종사용자”에게 통보하여야 합니다.</p>
|
||||
<p> ② “이용기관”은 “서비스” 이용 관련사항을 변경한 경우에 “최종사용자”에게 사전에 통지하여야 합니다.</p>
|
||||
<p> ③ “은행”은 “이용기관”이 “최종사용자”에게 고지할 필요가 있다고 판단되는 사항을 고지하도록 “이용기관”에게 요구할 수 있습니다.</p>
|
||||
<p> ④ “이용기관”은 “시스템”과 관련하여 알게 된 “최종사용자” 정보가 제3자에게 유출되었을 경우 즉시 해당 “최종사용자” 및 “은행”에 통지하여 추가적인 피해가 없도록 조치하여야 합니다.</p>
|
||||
<h3>제16조 (부정 접속 방지)</h3>
|
||||
<p> ① “은행”은 부정접속을 방지하기 위하여 “이용기관”의 “포털” 접속용 패스워드 입력오류가 연속 5회를 초과할 경우 업무 이용을 중지시킵니다.</p>
|
||||
<p> ② 제1항에 따라 업무 이용이 중지된 “이용기관”은 업무 이용을 위하여 “은행”에 해제 요청을 하여야 합니다.</p>
|
||||
<h3>제17조 (전산처리 절차)</h3>
|
||||
<p>전산처리명세의 송·수신 절차, 방법, 시간, 이체일 등 전산처리와 관련된 사항은 “포털”을 통해 별도 공시합니다.</p>
|
||||
<h3>제18조 (보안관리)</h3>
|
||||
<p> ① “이용기관”은 “은행”에서 제시하는 보안 기준을 준수하여야 하고, “은행”은 “이용기관”의 전산시스템에 대하여 각 호의 사항을 점검 요청할 수 있습니다.</p>
|
||||
<p> 1. 개인정보 및 금융정보 암호화 운영현황</p>
|
||||
<p> 2. 해킹 등에 대비한 정책사항</p>
|
||||
<p> 3. 시스템 운영에 대한 정보보호 체계 등</p>
|
||||
<p> ② “이용기관”은 “서비스” 이용과 관련하여 감독 당국에서 관련 자료 또는 감독을 요청하면 이에 대하여 적극적으로 협조하여야 합니다.</p>
|
||||
<p> ③ “이용기관”의 “서비스” 이용에 대한 고객 정보 관리책임은 “이용기관”에게 있으며, “이용기관”의 전산시스템에 대한 해킹 또는 외부의 네트워크 공격 등 위험이 발생한 경우에는 즉시 “은행”에 이를 통지하고, 본 “서비스”의 이용을 중단하여야 합니다.</p>
|
||||
<h3>제19조 (기밀유지)</h3>
|
||||
<p> ① “은행”과 “이용기관”은 “서비스”와 관련되거나 상호정보교환 등 업무처리 과정에서 인지 또는 획득한 정보를 이 업무처리 이외의 목적으로 사용하거나 외부에 유출하여서는 안 됩니다.</p>
|
||||
<p> ② 제1항에서 정한 의무를 위반하여 상대방 또는 제3자에게 손해가 발생한 경우, 귀책사유 있는 당사자는 상대방 또는 제3자에게 이로 인하여 발생한 손해의 일부 또는 전부를 배상하여야 합니다.</p>
|
||||
<h3>제20조 (손해배상 및 면책)</h3>
|
||||
<p> ① “은행”은 “이용기관”으로부터 접근매체의 분실이나 도난의 통지를 받은 후에 제3자가 그 접근매체를 사용하여 “이용기관”에게 손해가 발생한 경우 그 손해를 배상합니다.</p>
|
||||
<p> ② “은행”은 다음 각 1호의 사고로 인하여 “이용기관”에게 손해가 발생한 경우 그 손해를 배상합니다.</p>
|
||||
<p> 1. 접근매체의 위조나 변조로 발생한 사고</p>
|
||||
<p> 2. 계약체결 또는 거래지시의 전자적 전송이나 처리과정에서 발생한 사고</p>
|
||||
<p> 3. 전자금융거래를 위한 전자적 장치 또는 「정보통신망 이용촉진 및 정보보호 등에 관한 법률」 제2조 제1항 제1호에 따른 “은행”의 정보통신망에 침입하여 거짓이나 그 밖의 부정한 방법으로 획득한 접근매체의 이용으로 발생한 사고</p>
|
||||
<p> ③ 제1항 및 제2항에 의하여 금전적 손해가 발생한 경우 해당 금액 및 이에 대한 1년 만기 정기예금 이율로 계산한 경과이자를 배상합니다. 다만, 손해액이 해당 금액과 1년 만기 정기예금 이율로 계산한 금액을 초과하는 경우에는 실손해액을 배상합니다.</p>
|
||||
<p> ④ 제2항의 규정에도 불구하고 “은행”은 “이용기관”이 고의 또는 과실로 다음 각 호의 행위를 하여 “이용기관”에게 손해가 생기는 경우 “은행”의 고의 또는 과실이 없다면 “은행”은 그 책임의 전부 또는 일부를 지지 않습니다.</p>
|
||||
<p> 1. “이용기관”이 접근매체를 제3자에게 대여하거나 사용을 위임한 경우 또는 양도나 담보의 목적으로 제공한 경우(「전자금융거래법」제18조에 따라 선불전자지급수단이나 전자화폐를 양도하거나 담보로 제공한 경우를 제외합니다.)</p>
|
||||
<p> 2. 제3자가 권한 없이 “이용기관”의 접근매체를 이용하여 전자금융거래를 할 수 있음을 알았거나 쉽게 알 수 있었음에도 불구하고 “이용기관”이 자신의 접근매체를 누설 또는 노출하거나 방치한 경우</p>
|
||||
<p> 3. “은행”이 접근매체를 통하여 “이용기관”의 신원, 권한 및 거래지시의 내용 등을 확인하는 것 외에 보안강화를 위하여 전자금융거래 시 사전에 요구하는 추가적인 보안조치를 “이용기관”이 정당한 사유 없이 거부하여 사고가 발생한 경우</p>
|
||||
<p> 4. “이용기관”이 제3호에 따른 추가적인 보안조치에 사용되는 매체·수단 또는 정보에 대하여 다음 각 목의 어느 하나에 해당하는 행위를 하여 사고가 발생한 경우</p>
|
||||
<p> 가. 누설·노출 또는 방치한 행위</p>
|
||||
<p> 나. 제3자에게 대여하거나 그 사용을 위임한 행위 또는 양도나 담보의 목적으로 제공한 행위</p>
|
||||
<p> 5. 법인(「중소기업기본법」제2조 제2항에 의한 소기업을 제외합니다.)인 “이용기관”에게 손해가 발생한 경우로 “은행”이 사고를 방지하기 위하여 보안절차를 수립하고 이를 철저히 준수하는 등 합리적으로 요구되는 충분한 주의의무를 다한 경우</p>
|
||||
<h3>제21조 (이의제기 및 협조)</h3>
|
||||
<p> ① “이용기관”은 전자금융거래의 처리에 관하여 이의가 있을 때에는 “은행”의 분쟁처리기구에 그 해결을 요구하거나 금융감독원 금융분쟁조정위원회, 한국소비자보호원 소비자분쟁조정위원회 등을 통하여 분쟁조정을 신청할 수 있습니다.</p>
|
||||
<p> ② “이용기관”이 “은행”의 본점이나 영업점 또는 “은행”의 분쟁처리기구에 이의를 제기한 경우 “은행”은 15일 이내에 이에 대한 조사 또는 처리결과를 “이용기관”에게 알려야 합니다.</p>
|
||||
<p> ③ “은행”은 손해배상 등 분쟁처리를 위한 분쟁처리책임자 및 담당자를 지정하고, 그 연락처를 관련 인터넷 홈페이지에 게시하여야 합니다.</p>
|
||||
<p> ④ “이용기관”은 제20조 제2항에 정한 사고발생으로 인한 손해배상 등 분쟁처리와 관련한 “은행”의 사고조사 및 관계당국의 수사 또는 조사 절차에 협조하여야 합니다.</p>
|
||||
<h3>제22조 (거래기록·자료의 제공)</h3>
|
||||
<p> ① “은행”은 “이용기관”의 요청이 있을 경우 「금융실명거래 및 비밀보장에 관한 법률」 등 다른 법률에 저촉되지 않는 범위 내에서 “은행”이 보존·관리하고 있는 전자금융거래 관련 기록·자료를 “이용기관”에게 제공하여야 합니다.</p>
|
||||
<p> ② “이용기관”이 제공 요청을 할 수 있는 거래기록·자료의 범위와 대상기간은 「전자금융거래법」 제22조 제1항 및 제2항에서 정한 기록 및 기간으로 합니다.</p>
|
||||
<p> ③ “이용기관”이 서면(전자문서를 제외합니다. 이하 같습니다.) 형태의 거래기록·자료(이하 “거래명세서”라 합니다.)를 제공할 것을 “은행”에 요청하는 경우에는 본인이 서면으로 영업점에 신청하여야 하며, “은행”은 신청 가능 영업점의 주소 및 전화번호 등을 인터넷 등을 통하여 “이용기관”이 알 수 있도록 게시하여야 합니다.</p>
|
||||
<p> ④ “은행”은 “이용기관”으로부터 제3항에 따른 거래명세서의 제공요청을 받은 경우 2주일 이내에 “이용기관”에게 거래명세서를 교부하여야 합니다.</p>
|
||||
<p> ⑤ “은행”은 불가피한 사유로 인하여 거래기록·자료(거래명세서 포함)의 제공이 불가능한 경우에는 그 사유를 “이용기관”에게 즉시 통보하여야 하며, 해당 사유가 소멸한 경우 다음 각 호에 정한 기한 내에 “이용기관”에게 확인 및 제공하여야 합니다.</p>
|
||||
<p> 1. 전자적 장치를 통한 제공의 경우에는 즉시</p>
|
||||
<p> 2. 제3항에 따른 서면 제공 방식의 경우에는 2주일 이내에 거래명세서를 교부</p>
|
||||
<h2>부칙</h2>
|
||||
<p>본 약관은 2026. 99. 99.부터 시행합니다.</p>
|
||||
@@ -0,0 +1,184 @@
|
||||
제1장 총칙
|
||||
|
||||
제1조 (목적)
|
||||
본 약관은 (주)제주은행(이하 “은행”)이 제공하는 Open API 서비스를 이용함에 있어 “은행”과 Open API 서비스를 이용하는 자(이하 “이용기관”)간의 권리, 의무 및 책임 사항, 이용조건 및 절차 등 기본적인 사항을 명확히 규정함을 목적으로 합니다.
|
||||
|
||||
제2조 (용어의 정의)
|
||||
① 본 약관에서 사용하는 용어의 정의는 다음 각 호와 같습니다.
|
||||
1. “Open API”란 “은행”이 “이용기관”에게 제공하는 API(Application Programming Interface) 기능을 개방형 플랫폼 형태로 이용할 수 있도록 공개한 API
|
||||
2. “Open API 서비스”(이하 “서비스”)란 “은행”의 서비스 및 데이터를 표준화된 API 형태로 Open API 시스템을 통하여 “이용기관”에 제공하는 것
|
||||
3. “Open API 시스템”(이하 “시스템”)이란 “서비스”를 제공하기 위하여 “은행”에 설치한 중계시스템과 “이용기관”의 처리시스템을 통신회선으로 연결하여 “은행” 및 “이용기관” 상호간에 정보를 교환하는 시스템
|
||||
4. “Open API 포탈”(이하 “포털”)이란 “서비스”를 소개하고 가입 절차 및 이용 절차를 제공하며 “이용기관”이 가입 및 “서비스” 이용을 하기 위하여 “은행”이 제공하는 웹사이트
|
||||
5. “이용신청기관”이란 “은행”의 “서비스”를 이용하기 위하여 “서비스” 이용을 신청한 기관(법인, 개인사업자 및 임의단체 등)
|
||||
6. “이용기관”이란 본 약관에 동의하고 “서비스” 이용신청에 대한 “은행”의 승낙을 받은 후, “은행”이 제공하는 Open API를 이용하여 별도의 서비스 또는 프로그램을 개발, 테스트 및 제공하고자 하는 자
|
||||
7. “최종사용자”란 “은행”이 제공한 “서비스”를 결합하여 “이용기관”이 개발한 별도의 서비스 또는 프로그램을 이용하는 고객
|
||||
8. “제휴사”란 “은행”이 “이용기관”에게 “서비스”를 제공하기 위하여 “은행”과 계약을 체결하여 협력하는 제3자
|
||||
|
||||
제3조 (서비스의 내용)
|
||||
① 조회성 API 서비스(계좌 거래내역 조회, 예금주 실명 조회 등)
|
||||
② 거래성 API 서비스(환전, 이체 등)
|
||||
③ 기타 “은행” 또는 “제휴사”가 제공하는 API 형태의 “서비스”
|
||||
|
||||
제4조 (약관의 효력 및 변경)
|
||||
① 본 약관 내용의 효력은 “은행” 인터넷 홈페이지 또는 “포털” 등에 게시되거나 전자우편 등의 방법으로 “이용기관”에 통지됨으로써 발생합니다.
|
||||
② 본 약관의 변경은 전자금융거래 기본약관의 내용을 준용합니다.
|
||||
|
||||
제2장 서비스 이용계약
|
||||
|
||||
제5조 (서비스 이용신청)
|
||||
① 서비스 이용신청은 “이용신청기관”이 “포털”에서 “은행”이 정한 가입양식에 회원정보 기입 및 본 약관에 동의함으로써 이루어집니다.
|
||||
② 제1항을 완료한 “이용신청기관”은 이용 희망일 전일까지 “은행”이 요청하는 서류를 제출하여야 합니다.
|
||||
|
||||
제6조 (서비스 이용신청 승인 및 거절)
|
||||
① “은행”은 본 약관 제5조 제2항의 “이용신청기관”이 제출한 서류와 “서비스” 이용목적의 적정성을 검토한 후, “이용신청기관” 서비스 이용신청을 승낙 또는 거절할 수 있습니다.
|
||||
② “은행”은 “이용신청기관”이 다음 각 호의 어느 하나에 해당할 경우 이용신청에 대해 거절할 수 있습니다.
|
||||
1. 실명이 아니거나, 타인의 명의를 사용하여 신청한 경우
|
||||
2. 이용신청 내용을 허위로 기재하거나 허위서류를 첨부한 경우
|
||||
3. 이전에 “이용기관” 자격을 상실한 적이 있거나, 탈퇴, 이용중지 등이 있는 경우
|
||||
4. “이용신청기관”의 업태가 사행행위에 관련이 있거나 금융질서문란 위험이 있는 경우
|
||||
5. “이용신청기관”의 사업모델에 위법성이 있는 경우
|
||||
6. “이용신청기관”의 재무안정성이 위험한 경우
|
||||
7. “이용신청기관”의 전산망이나 정보처리시스템에 보안 취약점이 있거나, “이용신청기관”의 “최종사용자” 정보에 관한 기술적·물리적·관리적 보호조치가 미흡한 경우
|
||||
8. “은행”의 평판이나 “최종사용자”의 권리에 위험을 끼칠 가능성이 있는 경우
|
||||
③ “은행”은 이용신청에 대한 결과를 “이용신청자”에게 통보하여야 합니다.
|
||||
|
||||
제3장 서비스의 이용
|
||||
|
||||
제7조 (서비스 이용개시)
|
||||
“은행”이 “이용신청기관”의 이용신청을 심사 후 승낙하게 되면 “이용기관”이 요청한 이용 희망일로부터 “서비스”를 개시합니다. 단, 이용 희망일은 “은행”과 협의하여 결정합니다.
|
||||
|
||||
제8조 (서비스 이용시간)
|
||||
① “서비스”의 이용시간은 연중무휴(24시간)로 하며 “서비스”별 이용시간을 정하여 운영할 수 있습니다. 단, 다음 각 호의 어느 하나에 해당하여 은행이 정상적인 “서비스”를 제공하기 어려운 경우에는 서비스의 전부 또는 일부가 제한 될 수 있습니다.
|
||||
1. 은행의 시스템을 점검하는 경우
|
||||
2. 은행의 정보통신 설비의 보수점검, 증설, 교체, 이전 등 시스템 관리 작업을 진행하는 경우
|
||||
3. 정전, 제반 설비의 장애 또는 이용량의 폭주 등이 발생하는 경우
|
||||
4. 천재지변, 전쟁, 폭동, 테러, 해킹, Ddos, 국가비상사태 등 불가항력적 사유가 발생하는 경우
|
||||
5. 시스템 오류 등 서비스 제공에 관한 기술상 문제가 발생하는 경우
|
||||
② 제1항의 사유로 서비스가 일시 중지되는 경우, “은행”은 사전에 이를 공지하여야 합니다. 단, 부득이한 경우 사후에 통보할 수 있습니다.
|
||||
|
||||
제9조 (서비스 변경 및 중단)
|
||||
① “은행”은 변경될 “서비스”의 내용 및 변경일자를 “이용기관”에게 통지하고 “서비스”를 변경하여 제공할 수 있습니다.
|
||||
② “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “서비스”의 전부 또는 일부를 중단할 수 있습니다.
|
||||
1. “서비스”용 정보통신설비의 보수·교체·고장, 통신두절 등으로 인해 부득이한 경우
|
||||
2. 정전, 모든 설비의 장애 또는 이용량의 폭주 등으로 정상적인 서비스 이용에 지장이 있는 경우
|
||||
3. “서비스” 종료 등과 같은 “은행”의 사정으로 “서비스”를 유지할 수 없는 경우
|
||||
4. 기타 천재지변, 국가비상사태 등 불가항력적 사유가 있는 경우
|
||||
③ “서비스”를 변경하거나 중단할 경우, “은행”은 그 사실을 사전에 “이용기관”에 통지하여야 합니다. 단, 사전에 통지할 수 없는 부득이한 사유가 있는 경우에는 사후에 그 사유와 함께 통지하여야 합니다. 또한 “포털”에 등록된 연락처가 부정확하거나 “이용기관” 측의 사유로 인하여 “은행”의 통보를 받지 못해 발생하는 손실에 대해서는 “은행”의 고의 또는 과실이 없는 경우 그 책임의 전부 또는 일부를 지지 않습니다.
|
||||
④ “은행”은 “서비스”의 변경 혹은 중단으로 발생하는 문제에 대해서는 “은행”의 고의 또는 과실이 없는 경우 그 책임의 전부 또는 일부를 지지 않습니다.
|
||||
|
||||
제10조 (서비스 이용 제한)
|
||||
① “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “이용기관”에 대한 “서비스” 이용을 개별 고지 후 제한할 수 있습니다.
|
||||
1. 감독당국으로부터 중지 권고가 있는 경우
|
||||
2. “이용기관”이 휴업 또는 폐업을 하는 경우
|
||||
3. 접근권한을 넘어서는 접속을 시도하거나 반복적인 접속을 시도하여 과부하 발생 등 시스템 운영의 안전성을 저해한다고 판단되는 행위가 확인된 경우
|
||||
4. “이용기관” 수수료 납부계좌에서 부족분이 3회 이상 발생할 경우
|
||||
5. “이용기관”에 본 약관 제6조 제2항 각 호의 사유가 발생한 경우 또는 “서비스” 신청 당시 “이용기관”에 본 약관 제6조 제2항 각 호의 사유가 있었음이 밝혀진 경우
|
||||
6. “시스템” 및 “포털”의 안정적인 운영을 위하여 필요하다고 인정되는 경우
|
||||
② “이용기관”이 제1항에 해당할 경우, “은행”은 “서비스” 이용을 제한할 수 있으며 이와 별도로 해당 “이용기관”에 소명자료 제출을 요구할 수 있습니다. “이용기관”은 통지받은 날로부터 15일 이내에 소명자료를 제출하여야 합니다. 단, “은행”은 “이용기관”의 소명자료가 정당하다고 판단하기 전까지 “이용기관”의 “서비스” 이용을 제한할 수 있습니다.
|
||||
|
||||
제11조 (서비스 이용 해지)
|
||||
① “은행”은 “이용기관”이 다음 각 호의 어느 하나에 해당할 경우 “이용기관”에 대한 “서비스” 이용을 개별 고지 후 해지할 수 있습니다.
|
||||
1. 감독당국으로부터 해지권고가 있는 경우
|
||||
2. 최근 12개월간 “시스템” 이용 실적이 없는 경우
|
||||
3. “이용기관” 수수료 납부계좌에서 부족분이 6회 이상 발생할 경우
|
||||
4. “이용기관”의 정상적인 운영이 불가능하다고 “은행”이 판단할 경우
|
||||
② “이용기관”이 제1항에 해당할 경우, “이용기관”에 소명자료 제출을 요구할 수 있습니다. “이용기관”은 통지받은 날로부터 15일 이내에 소명자료를 제출하여야 합니다.
|
||||
③ 해지일은 “은행”이 “시스템” 또는 “포털” 원장에 해지 등록한 날로 합니다.
|
||||
|
||||
제4장 계약당사자의 의무 및 손해배상 등
|
||||
|
||||
제12조 (은행의 의무)
|
||||
① “은행”은 본 약관에서 특별히 규정한 사유 외에는 “서비스”를 지속적·안정적으로 제공하여야 합니다.
|
||||
② “은행”은 “서비스” 제공과 관련하여 알게 된 “이용기관”의 정보를 업무 이외의 목적으로 사용할 수 없습니다. 단, 관계법령에 따른 수사상의 관계기관으로부터 요구 받은 경우나 정보통신 윤리위원회의 요청이 있는 경우 등 법률의 규정에 따른 적법한 절차에 의한 경우에는 그러하지 않습니다.
|
||||
③ “은행”이 제공하는 “서비스”로 인하여 “이용기관”에 손해가 발생한 경우 “서비스”를 제공하는 “은행”과 이용하는 “이용기관”이 상호협의에 의하여 해결합니다. 단, 그러한 손해가 “은행”의 고의나 과실에 기해 발생한 경우에 한하여 “은행”은 그 책임의 전부 또는 일부를 부담합니다.
|
||||
④ “은행”은 개인정보보호법, 신용정보의 이용 및 보호에 관한 법률, 금융실명거래 및 비밀보장에 관한 법률, 정보통신망 이용촉진 및 정보보호에 관한 법률, 통신비밀보호법 등 “서비스” 운영, 유지와 관련 있는 법규를 준수하여야 합니다.
|
||||
|
||||
제13조 (약관 외 준칙)
|
||||
① “이용기관”은 “포털”에서 “은행”이 정한 가입양식에 회원정보를 기입할 때, 사실에 근거하여 정확하게 기입하여야 합니다.
|
||||
② “이용기관”은 “은행”에 제공한 사항이 변경될 때, 즉시 “은행”에 변경 신청하여 변경사항을 최신의 정보로 수정해야 합니다. 수정하지 않은 정보로 인하여 발생하는 문제에 대한 책임은 “이용기관”에 있으며 “은행”은 이에 대해 아무런 책임을 지지 않습니다.
|
||||
③ “이용기관”은 “시스템”과 관련한 “서비스” 이용의 대가로 수수료를 납부하여야 합니다.
|
||||
④ “이용기관”은 “서비스”의 이용 권한, 기타 이용 계약상의 지위를 타인에게 양도, 증여할 수 없으며 게시물에 대한 저작권을 포함한 모든 권리 및 책임은 이를 게시한 “이용기관”에 있습니다.
|
||||
⑤ “이용기관”은 “서비스”의 안정적인 이용을 위해 전산시스템의 구축에 필요한 회선, 단말기 및 부대기기를 “이용기관”의 부담으로 설치하여야 하고, 보안상 안전하게 처리할 수 있는 전산시스템을 갖추어야 합니다.
|
||||
⑥ “이용기관”은 “은행”이나 “최종사용자”가 업무처리에 대하여 이의를 제기하거나 확인을 요청하면 이를 신속히 처리하여야 합니다.
|
||||
⑦ “이용기관”은 본 약관에서 정한 사항을 준수하여야 하며, “은행”이 “이용기관”의 약관 이행여부를 점검하고자 할 경우 적극 협조하여야 합니다.
|
||||
⑧ “이용기관”은 “포털”에서 제공하는 “서비스” 이용과 관련하여 다음 각 호의 행위를 하여서는 안 됩니다.
|
||||
1. 다른 “이용기관”의 아이디를 부정 사용하는 행위
|
||||
2. 범죄행위를 목적으로 하거나 기타 범죄행위와 관련된 행위
|
||||
3. 선량한 풍속, 기타 사회질서를 해하는 행위
|
||||
4. 타인의 명예를 훼손하거나 모욕하는 행위
|
||||
5. 타인의 지식재산권 등의 권리를 침해하는 행위
|
||||
6. 해킹행위 또는 컴퓨터 바이러스의 유포행위
|
||||
7. 타인의 의사에 반하여 광고성 정보 등 일정한 내용을 지속적으로 전송하는 행위
|
||||
8. 서비스의 안정적인 운영에 지장을 주거나 줄 우려가 있는 일체의 행위
|
||||
⑨ “이용기관”은 개인정보보호법, 신용정보의 이용 및 보호에 관한 법률, 금융실명거래 및 비밀보장에 관한 법률, 정보통신망 이용촉진 및 정보보호에 관한 법률, 통신비밀보호법 등 “서비스” 운영, 유지와 관련 있는 법규를 준수하여야 합니다.
|
||||
|
||||
제5장 수수료
|
||||
|
||||
제14조 (수수료)
|
||||
① “은행”은 “서비스” 제공의 대가로 “이용기관”에게 수수료를 부과합니다.
|
||||
② “은행”은 매월 1일~말일까지 발생하는 수수료를 익월 10일(공휴일인 경우에는 익영업일)에 “이용기관”에게 부과하며, “이용기관”이 등록한 수수료 납부계좌에서 예금청구서 및 통장에 의하지 아니하고 대체 출금하기로 합니다.
|
||||
③ “은행”은 “이용기관”이 해지 등의 사유로 “서비스” 이용이 불가한 경우, 이용불가 시점까지 발생한 미결제 수수료를 본 “서비스” 해지일에 예금청구서 및 통장에 의하지 아니하고 “이용기관”의 수수료 납부계좌에서 출금하고, 부족액이 발생할 시에 별도 청구합니다.
|
||||
④ “은행”은 제2항의 규정에도 불구하고 전산상의 이유 등으로 “이용기관”이 등록한 수수료 납부계좌에서 예금청구서 및 통장에 의하지 아니하고 대체 출금할 수 없는 경우, “이용기관”에 “은행”이 정하는 다른 방법으로 수수료 납부를 요청할 수 있습니다.
|
||||
⑤ “이용기관”에게 부과되는 수수료는 “은행”이 정하는 바에 따릅니다. 다만 “은행”이 사업적으로 필요하다고 판단되는 경우 서비스 이용에 따른 수수료를 감면할 수 있습니다.
|
||||
⑥ 수수료는 은행의 정책에 따라 변경될 수 있으며, 은행이 수수료를 변경할 경우 전자금융거래 기본약관의 내용을 준용합니다.
|
||||
|
||||
제6장 보칙
|
||||
|
||||
제15조 (최종사용자에 대한 고지 의무)
|
||||
① “이용기관”은 중요사항에 대해 “최종사용자”에게 통보하여야 합니다.
|
||||
② “이용기관”은 “서비스” 이용 관련사항을 변경한 경우에 “최종사용자”에게 사전에 통지하여야 합니다.
|
||||
③ “은행”은 “이용기관”이 “최종사용자”에게 고지할 필요가 있다고 판단되는 사항을 고지하도록 “이용기관”에게 요구할 수 있습니다.
|
||||
④ “이용기관”은 “시스템”과 관련하여 알게 된 “최종사용자” 정보가 제3자에게 유출되었을 경우 즉시 해당 “최종사용자” 및 “은행”에 통지하여 추가적인 피해가 없도록 조치하여야 합니다.
|
||||
|
||||
제16조 (부정 접속 방지)
|
||||
① “은행”은 부정접속을 방지하기 위하여 “이용기관”의 “포털” 접속용 패스워드 입력오류가 연속 5회를 초과할 경우 업무 이용을 중지시킵니다.
|
||||
② 제1항에 따라 업무 이용이 중지된 “이용기관”은 업무 이용을 위하여 “은행”에 해제 요청을 하여야 합니다.
|
||||
|
||||
제17조 (전산처리 절차)
|
||||
전산처리명세의 송·수신 절차, 방법, 시간, 이체일 등 전산처리와 관련된 사항은 “포털”을 통해 별도 공시합니다.
|
||||
|
||||
제18조 (보안관리)
|
||||
① “이용기관”은 “은행”에서 제시하는 보안 기준을 준수하여야 하고, “은행”은 “이용기관”의 전산시스템에 대하여 각 호의 사항을 점검 요청할 수 있습니다.
|
||||
1. 개인정보 및 금융정보 암호화 운영현황
|
||||
2. 해킹 등에 대비한 정책사항
|
||||
3. 시스템 운영에 대한 정보보호 체계 등
|
||||
② “이용기관”은 “서비스” 이용과 관련하여 감독 당국에서 관련 자료 또는 감독을 요청하면 이에 대하여 적극적으로 협조하여야 합니다.
|
||||
③ “이용기관”의 “서비스” 이용에 대한 고객 정보 관리책임은 “이용기관”에게 있으며, “이용기관”의 전산시스템에 대한 해킹 또는 외부의 네트워크 공격 등 위험이 발생한 경우에는 즉시 “은행”에 이를 통지하고, 본 “서비스”의 이용을 중단하여야 합니다.
|
||||
|
||||
제19조 (기밀유지)
|
||||
① “은행”과 “이용기관”은 “서비스”와 관련되거나 상호정보교환 등 업무처리 과정에서 인지 또는 획득한 정보를 이 업무처리 이외의 목적으로 사용하거나 외부에 유출하여서는 안 됩니다.
|
||||
② 제1항에서 정한 의무를 위반하여 상대방 또는 제3자에게 손해가 발생한 경우, 귀책사유 있는 당사자는 상대방 또는 제3자에게 이로 인하여 발생한 손해의 일부 또는 전부를 배상하여야 합니다.
|
||||
|
||||
제20조 (손해배상 및 면책)
|
||||
① “은행”은 “이용기관”으로부터 접근매체의 분실이나 도난의 통지를 받은 후에 제3자가 그 접근매체를 사용하여 “이용기관”에게 손해가 발생한 경우 그 손해를 배상합니다.
|
||||
② “은행”은 다음 각 1호의 사고로 인하여 “이용기관”에게 손해가 발생한 경우 그 손해를 배상합니다.
|
||||
1. 접근매체의 위조나 변조로 발생한 사고
|
||||
2. 계약체결 또는 거래지시의 전자적 전송이나 처리과정에서 발생한 사고
|
||||
3. 전자금융거래를 위한 전자적 장치 또는 「정보통신망 이용촉진 및 정보보호 등에 관한 법률」 제2조 제1항 제1호에 따른 “은행”의 정보통신망에 침입하여 거짓이나 그 밖의 부정한 방법으로 획득한 접근매체의 이용으로 발생한 사고
|
||||
③ 제1항 및 제2항에 의하여 금전적 손해가 발생한 경우 해당 금액 및 이에 대한 1년 만기 정기예금 이율로 계산한 경과이자를 배상합니다. 다만, 손해액이 해당 금액과 1년 만기 정기예금 이율로 계산한 금액을 초과하는 경우에는 실손해액을 배상합니다.
|
||||
④ 제2항의 규정에도 불구하고 “은행”은 “이용기관”이 고의 또는 과실로 다음 각 호의 행위를 하여 “이용기관”에게 손해가 생기는 경우 “은행”의 고의 또는 과실이 없다면 “은행”은 그 책임의 전부 또는 일부를 지지 않습니다.
|
||||
1. “이용기관”이 접근매체를 제3자에게 대여하거나 사용을 위임한 경우 또는 양도나 담보의 목적으로 제공한 경우(「전자금융거래법」제18조에 따라 선불전자지급수단이나 전자화폐를 양도하거나 담보로 제공한 경우를 제외합니다.)
|
||||
2. 제3자가 권한 없이 “이용기관”의 접근매체를 이용하여 전자금융거래를 할 수 있음을 알았거나 쉽게 알 수 있었음에도 불구하고 “이용기관”이 자신의 접근매체를 누설 또는 노출하거나 방치한 경우
|
||||
3. “은행”이 접근매체를 통하여 “이용기관”의 신원, 권한 및 거래지시의 내용 등을 확인하는 것 외에 보안강화를 위하여 전자금융거래 시 사전에 요구하는 추가적인 보안조치를 “이용기관”이 정당한 사유 없이 거부하여 사고가 발생한 경우
|
||||
4. “이용기관”이 제3호에 따른 추가적인 보안조치에 사용되는 매체·수단 또는 정보에 대하여 다음 각 목의 어느 하나에 해당하는 행위를 하여 사고가 발생한 경우
|
||||
가. 누설·노출 또는 방치한 행위
|
||||
나. 제3자에게 대여하거나 그 사용을 위임한 행위 또는 양도나 담보의 목적으로 제공한 행위
|
||||
5. 법인(「중소기업기본법」제2조 제2항에 의한 소기업을 제외합니다.)인 “이용기관”에게 손해가 발생한 경우로 “은행”이 사고를 방지하기 위하여 보안절차를 수립하고 이를 철저히 준수하는 등 합리적으로 요구되는 충분한 주의의무를 다한 경우
|
||||
|
||||
제21조 (이의제기 및 협조)
|
||||
① “이용기관”은 전자금융거래의 처리에 관하여 이의가 있을 때에는 “은행”의 분쟁처리기구에 그 해결을 요구하거나 금융감독원 금융분쟁조정위원회, 한국소비자보호원 소비자분쟁조정위원회 등을 통하여 분쟁조정을 신청할 수 있습니다.
|
||||
② “이용기관”이 “은행”의 본점이나 영업점 또는 “은행”의 분쟁처리기구에 이의를 제기한 경우 “은행”은 15일 이내에 이에 대한 조사 또는 처리결과를 “이용기관”에게 알려야 합니다.
|
||||
③ “은행”은 손해배상 등 분쟁처리를 위한 분쟁처리책임자 및 담당자를 지정하고, 그 연락처를 관련 인터넷 홈페이지에 게시하여야 합니다.
|
||||
④ “이용기관”은 제20조 제2항에 정한 사고발생으로 인한 손해배상 등 분쟁처리와 관련한 “은행”의 사고조사 및 관계당국의 수사 또는 조사 절차에 협조하여야 합니다.
|
||||
|
||||
제22조 (거래기록·자료의 제공)
|
||||
① “은행”은 “이용기관”의 요청이 있을 경우 「금융실명거래 및 비밀보장에 관한 법률」 등 다른 법률에 저촉되지 않는 범위 내에서 “은행”이 보존·관리하고 있는 전자금융거래 관련 기록·자료를 “이용기관”에게 제공하여야 합니다.
|
||||
② “이용기관”이 제공 요청을 할 수 있는 거래기록·자료의 범위와 대상기간은 「전자금융거래법」 제22조 제1항 및 제2항에서 정한 기록 및 기간으로 합니다.
|
||||
③ “이용기관”이 서면(전자문서를 제외합니다. 이하 같습니다.) 형태의 거래기록·자료(이하 “거래명세서”라 합니다.)를 제공할 것을 “은행”에 요청하는 경우에는 본인이 서면으로 영업점에 신청하여야 하며, “은행”은 신청 가능 영업점의 주소 및 전화번호 등을 인터넷 등을 통하여 “이용기관”이 알 수 있도록 게시하여야 합니다.
|
||||
④ “은행”은 “이용기관”으로부터 제3항에 따른 거래명세서의 제공요청을 받은 경우 2주일 이내에 “이용기관”에게 거래명세서를 교부하여야 합니다.
|
||||
⑤ “은행”은 불가피한 사유로 인하여 거래기록·자료(거래명세서 포함)의 제공이 불가능한 경우에는 그 사유를 “이용기관”에게 즉시 통보하여야 하며, 해당 사유가 소멸한 경우 다음 각 호에 정한 기한 내에 “이용기관”에게 확인 및 제공하여야 합니다.
|
||||
1. 전자적 장치를 통한 제공의 경우에는 즉시
|
||||
2. 제3항에 따른 서면 제공 방식의 경우에는 2주일 이내에 거래명세서를 교부
|
||||
|
||||
부칙
|
||||
본 약관은 2026. 99. 99.부터 시행합니다.
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
editor-content.css 대응 프래그먼트: <!doctype>/<html>/<head>/<style> 없이 순수 태그(h1/h2/h3/p/ul/li/strong)만 사용.
|
||||
admin Summernote 코드뷰에 그대로 붙여넣으면 됨. class/style 속성 없음 -> 붙여넣기(paste-cleaner)든
|
||||
코드뷰 직접 입력이든 태그 정리 과정에서 잘려나갈 것이 없다.
|
||||
-->
|
||||
<h1>알림수신동의서</h1>
|
||||
<p><strong>(주)제주은행 귀중</strong></p>
|
||||
<p>귀 행의 Open API 포털에서 이용기관(귀 행과 계약을 체결한 법인) 관리자가 개발자 초대를 위하여 입력한 휴대폰 번호로 초대 안내 문자메시지(알림)를 발송하고자 하는 경우에는 「개인정보보호법」 등 관계 법령에 따라 수신자 본인의 동의가 필요합니다.</p>
|
||||
<h2>발송정보</h2>
|
||||
<ul>
|
||||
<li><strong>수집·이용 항목:</strong> 휴대폰 번호</li>
|
||||
<li><strong>발송 목적:</strong> Open API 포털 개발자 초대 안내(초대 이용기관명, 초대자명, 인증번호, 가입 진행 링크 포함) 문자메시지 발송</li>
|
||||
<li><strong>보유·이용 기간:</strong> 초대 유효기간(발송일로부터 7일) 경과 또는 초대 처리(가입 완료·취소) 시까지 보유 후 파기</li>
|
||||
</ul>
|
||||
<p>□ 동의하지 않음 □ 동의함</p>
|
||||
<p>※ 이용기관 관리자는 본인이 입력한 휴대폰 번호의 실제 사용자로부터 위 알림 수신에 대한 동의를 사전에 받은 경우에 한하여 초대를 진행하여야 합니다.</p>
|
||||
<p>※ 동의하지 않을 경우 초대 신청(등록)은 유지되나 초대 안내 문자메시지는 발송되지 않으며, 이 경우 이용기관 관리자로부터 별도의 방법으로 초대 안내를 받아야 합니다.</p>
|
||||
<p>※ 수집된 휴대폰 번호는 문자메시지 발송 업무를 위탁받은 발송대행사에 위 목적 범위 내에서만 제공되며, 목적 외 용도로 이용되지 않습니다.</p>
|
||||
<h3>위 알림 수신에 동의하십니까?</h3>
|
||||
<p>제정일자: 2026. 99. 99. 제정</p>
|
||||
@@ -0,0 +1,22 @@
|
||||
알림수신동의서
|
||||
|
||||
(주)제주은행 귀중
|
||||
|
||||
귀 행의 Open API 포털에서 이용기관(귀 행과 계약을 체결한 법인) 관리자가 개발자 초대를 위하여 입력한 휴대폰 번호로 초대 안내 문자메시지(알림)를 발송하고자 하는 경우에는 「개인정보보호법」 등 관계 법령에 따라 수신자 본인의 동의가 필요합니다.
|
||||
|
||||
발송정보
|
||||
ㄴ 수집·이용 항목: 휴대폰 번호
|
||||
ㄴ 발송 목적: Open API 포털 개발자 초대 안내(초대 이용기관명, 초대자명, 인증번호, 가입 진행 링크 포함) 문자메시지 발송
|
||||
ㄴ 보유·이용 기간: 초대 유효기간(발송일로부터 7일) 경과 또는 초대 처리(가입 완료·취소) 시까지 보유 후 파기
|
||||
|
||||
□ 동의하지 않음 □ 동의함
|
||||
|
||||
※ 이용기관 관리자는 본인이 입력한 휴대폰 번호의 실제 사용자로부터 위 알림 수신에 대한 동의를 사전에 받은 경우에 한하여 초대를 진행하여야 합니다.
|
||||
|
||||
※ 동의하지 않을 경우 초대 신청(등록)은 유지되나 초대 안내 문자메시지는 발송되지 않으며, 이 경우 이용기관 관리자로부터 별도의 방법으로 초대 안내를 받아야 합니다.
|
||||
|
||||
※ 수집된 휴대폰 번호는 문자메시지 발송 업무를 위탁받은 발송대행사에 위 목적 범위 내에서만 제공되며, 목적 외 용도로 이용되지 않습니다.
|
||||
|
||||
위 알림 수신에 동의하십니까?
|
||||
|
||||
제정일자: 2026. 99. 99. 제정
|
||||
@@ -0,0 +1,273 @@
|
||||
# 개발환경 준비 사항
|
||||
|
||||
EAPIM Portal 개발에 필요한 도구와 런타임을 설치하고 검증하는 절차입니다.
|
||||
처음 환경을 세팅하는 신규 개발자는 이 문서를 끝까지 따라 진행한 뒤 [README.md](../README.md)의 "초기 세팅 가이드" 단계를 이어서 수행하세요.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한눈에 보는 요구사항
|
||||
|
||||
| 구분 | 도구 | 권장 버전 | 비고 |
|
||||
|------|------|-----------|----------------------------------|
|
||||
| 백엔드 | JDK | 1.8 (Zulu 8 권장) | `sourceCompatibility=1.8` |
|
||||
| 빌드 | Gradle | 8.7 | gradlew 사용 금지 — 시스템 gradle 직접 사용 |
|
||||
| 형상관리 | Git | 2.30 이상 | SSH 키 등록 권장 |
|
||||
| DB | Oracle Client/접근 | 19c | 개발은 `192.168.240.177:1598` |
|
||||
| 프론트엔드 빌드 | Node.js | 18 LTS 이상 | SCSS 컴파일 전용 |
|
||||
| 프론트엔드 빌드 | npm | Node.js 함께 설치 | `sass` 패키지 사용 |
|
||||
| (선택) 환경 변수 | direnv | 최신 | `.envrc` 자동 로드 |
|
||||
| (선택) IDE | IntelliJ IDEA | 2023.x 이상 | Lombok / MapStruct 플러그인 |
|
||||
|
||||
---
|
||||
|
||||
## 1. JDK 8 설치
|
||||
|
||||
Java 8 (1.8) 호환이 필요합니다. Azul Zulu JDK 8 사용을 권장합니다.
|
||||
|
||||
### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install --cask zulu@8
|
||||
# 설치 후 경로 확인
|
||||
/usr/libexec/java_home -v 1.8
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. [Azul Zulu JDK 8 다운로드](https://www.azul.com/downloads/?version=java-8-lts&package=jdk)
|
||||
2. MSI 설치 후 `JAVA_HOME` 환경변수 등록
|
||||
3. `PATH`에 `%JAVA_HOME%\bin` 추가
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
java -version # 1.8.x
|
||||
javac -version # 1.8.x
|
||||
echo $JAVA_HOME # macOS/Linux
|
||||
echo %JAVA_HOME% # Windows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Gradle 8.7 설치
|
||||
|
||||
이 프로젝트는 **gradlew 사용을 금지**합니다. 시스템에 설치된 gradle 8.7을 직접 사용합니다.
|
||||
|
||||
### macOS (Homebrew + SDKMAN 권장)
|
||||
|
||||
```bash
|
||||
# SDKMAN 설치 (이미 있으면 생략)
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
|
||||
# Gradle 8.7 설치
|
||||
sdk install gradle 8.7
|
||||
sdk default gradle 8.7
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. [Gradle 8.7 Binary-only 다운로드](https://gradle.org/releases/)
|
||||
2. `C:\gradle\gradle-8.7` 경로에 압축 해제
|
||||
3. `GRADLE_HOME=C:\gradle\gradle-8.7` 환경변수 등록
|
||||
4. `PATH`에 `%GRADLE_HOME%\bin` 추가
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
gradle -v
|
||||
# Gradle 8.7
|
||||
# JVM: 1.8.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. (권장) direnv 사용
|
||||
|
||||
저장소 루트의 `.envrc` 가 `JAVA_HOME`, `GRADLE_USER_HOME` 등을 자동 주입합니다.
|
||||
디렉터리 진입만으로 환경이 격리되므로 다른 프로젝트와 JDK가 섞이지 않습니다.
|
||||
|
||||
### 설치
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install direnv
|
||||
|
||||
# Windows (scoop)
|
||||
scoop install direnv
|
||||
```
|
||||
|
||||
### 셸 훅 등록 (한 번만)
|
||||
|
||||
```bash
|
||||
# zsh
|
||||
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
|
||||
|
||||
# bash
|
||||
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
### 프로젝트에서 허용
|
||||
|
||||
```bash
|
||||
cd /path/to/eapim-portal
|
||||
direnv allow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Git 설치
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install git
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Git for Windows](https://git-scm.com/download/win) 설치. Git Bash가 함께 설치되어 README의 Bash 스니펫을 그대로 사용할 수 있습니다.
|
||||
|
||||
### SSH 키 등록 (사내 Git 서버용)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "your.email@djbank.co.kr"
|
||||
cat ~/.ssh/id_ed25519.pub
|
||||
# 출력된 공개키를 사내 Git 서버(192.168.240.178:18081)에 등록
|
||||
```
|
||||
|
||||
### 검증
|
||||
|
||||
```bash
|
||||
git --version
|
||||
ssh -T git@192.168.240.178 -p 18081
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Oracle 19c 접근 정보
|
||||
|
||||
개발 환경에서는 별도 Oracle 설치 없이 사내 개발 DB에 접속합니다.
|
||||
|
||||
- **Host**: `192.168.240.177`
|
||||
- **Port**: `1599`
|
||||
- **Schemas**: `EMSADM` (Portal), `AGWADM` (Gateway)
|
||||
- 상세 계정/패스워드는 사내 비밀번호 관리자에게 문의
|
||||
|
||||
스테이징/운영은 JNDI를 통해 접근합니다 — `jdbc/dsOBP_EMS`, `jdbc/dsOBP_AGW`.
|
||||
|
||||
DB 클라이언트 도구는 자유 선택 (DBeaver, DataGrip, SQL Developer 등).
|
||||
|
||||
---
|
||||
|
||||
## 6. Node.js & SASS 빌드 환경
|
||||
|
||||
SCSS 파일을 CSS로 컴파일하기 위해 Node.js 환경이 필요합니다.
|
||||
`src/main/resources/static/css/main.css` 는 SASS 컴파일 결과물이므로 **반드시 SCSS 파일을 수정한 뒤 컴파일**해야 합니다.
|
||||
|
||||
### 6.1 Node.js 설치 (LTS 18 이상)
|
||||
|
||||
#### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install node
|
||||
```
|
||||
|
||||
#### macOS / Linux (nvm 권장 — 버전 관리 용이)
|
||||
|
||||
```bash
|
||||
# nvm 설치
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
exec $SHELL -l
|
||||
|
||||
# Node.js LTS 설치
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
1. [Node.js LTS 다운로드](https://nodejs.org/en/download) 후 MSI 설치
|
||||
2. 또는 [nvm-windows](https://github.com/coreybutler/nvm-windows/releases) 사용
|
||||
|
||||
#### 검증
|
||||
|
||||
```bash
|
||||
node -v # v18.x 이상
|
||||
npm -v # 9.x 이상
|
||||
```
|
||||
|
||||
### 6.2 프로젝트 SASS 의존성 설치
|
||||
|
||||
```bash
|
||||
cd /path/to/eapim-portal
|
||||
npm install
|
||||
```
|
||||
|
||||
`package.json` 의 `devDependencies.sass` (Dart Sass) 가 `node_modules/` 에 설치됩니다.
|
||||
|
||||
### 6.3 SASS 빌드 명령어
|
||||
|
||||
```bash
|
||||
# 1회 컴파일 (개발용, 들여쓰기 유지)
|
||||
npm run sass:build
|
||||
|
||||
# 1회 컴파일 (배포용, minified)
|
||||
npm run sass:build:minified
|
||||
|
||||
# 파일 변경 감지 후 자동 컴파일 (개발 중 권장)
|
||||
npm run sass:watch
|
||||
|
||||
# 빌드 + minified 동시 생성
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 6.4 SASS 컴파일 검증
|
||||
|
||||
```bash
|
||||
ls -lh src/main/resources/static/css/main.css
|
||||
ls -lh src/main/resources/static/css/main.min.css
|
||||
```
|
||||
|
||||
파일이 갱신되었고 브라우저에서 스타일이 정상 적용되면 성공입니다.
|
||||
|
||||
### 6.5 SASS 작업 시 주의사항
|
||||
|
||||
- `src/main/resources/static/css/main.css` **직접 수정 금지** — 재컴파일 시 덮어씌워짐
|
||||
- SCSS 파일은 `src/main/resources/static/sass/` 하위에서만 작성
|
||||
- 새 파일 추가 시 파일명 앞에 언더스코어(`_`) 필수 (`_filename.scss`)
|
||||
- `@import` 대신 `@use` 사용 (Dart Sass 3.0 호환)
|
||||
|
||||
---
|
||||
|
||||
## 7. (선택) IDE 설정
|
||||
|
||||
### IntelliJ IDEA
|
||||
|
||||
1. **Lombok 플러그인** 설치 → `Settings → Plugins → Lombok`
|
||||
2. **Annotation Processing 활성화** → `Settings → Build → Compiler → Annotation Processors → Enable annotation processing`
|
||||
3. **Gradle JVM** 설정 → `Settings → Build → Build Tools → Gradle → Gradle JVM` 을 JDK 1.8 (Zulu) 로 지정
|
||||
4. **MapStruct** 는 별도 플러그인 불필요 (annotation processor 만 활성화되어 있으면 됨)
|
||||
|
||||
### VS Code
|
||||
|
||||
- `Extension Pack for Java`
|
||||
- `Lombok Annotations Support`
|
||||
- `Sass` (Syler) — SCSS 문법 하이라이팅
|
||||
|
||||
---
|
||||
|
||||
## 8. 최종 체크리스트
|
||||
|
||||
아래 명령이 모두 정상 동작하면 개발환경 준비가 완료된 것입니다.
|
||||
|
||||
```bash
|
||||
java -version # 1.8.x
|
||||
gradle -v # 8.7 / JVM 1.8
|
||||
git --version
|
||||
node -v # v18 이상
|
||||
npm -v
|
||||
direnv version # (선택)
|
||||
```
|
||||
|
||||
이후 [README.md](../README.md) 의 "초기 세팅 가이드" 로 이동해 프로젝트 클론과 의존 모듈 설치를 진행하세요.
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>DJBank API Portal 이메일 인증</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
table, td { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin:0;padding:0;width:100%;background-color:#eef0f3;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||||
<!-- 미리보기 텍스트 (받은편지함 목록에 노출, 본문에서는 숨김) -->
|
||||
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#eef0f3;">DJBank 개발자포탈 이메일 인증번호 안내입니다.</div>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#eef0f3;">
|
||||
<tr>
|
||||
<td align="center" style="padding:30px 12px;">
|
||||
|
||||
<!-- ============ 카드 (흰 배경 · 테두리 #dfdfdf · 라운드 20) ============ -->
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;background-color:#ffffff;border:1px solid #dfdfdf;border-radius:20px;">
|
||||
|
||||
<!-- ===== Header : DJ Bank 로고 + API Portal ===== -->
|
||||
<tr>
|
||||
<td style="padding:26px 27px 0 27px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td width="20" style="width:20px;font-size:0;line-height:0;"> </td>
|
||||
<td valign="middle" style="line-height:0;">
|
||||
<img src="images/logo-djbank.png" width="144" height="40" alt="DJ Bank" style="display:block;width:144px;height:40px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
<td width="16" style="width:16px;font-size:0;line-height:0;"> </td>
|
||||
<td valign="middle" style="line-height:0;">
|
||||
<img src="images/logo-apiportal.png" width="124" height="21" alt="API Portal" style="display:block;width:124px;height:21px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- ===== 본문 : 상단 파란선(#2A69DE) + 그림자 ===== -->
|
||||
<tr>
|
||||
<td style="padding:7px 27px 0 27px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top:3px solid #2a69de;box-shadow:0px 4px 12px 0px rgba(85,95,108,0.25);">
|
||||
<tr>
|
||||
<td style="padding:26px 29px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
|
||||
<!-- 제목 (subject) : ##변수## 치환 토큰 -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:15px;line-height:25px;color:#000000;font-weight:600;padding-bottom:20px;">##subject:이메일인증##</td>
|
||||
</tr>
|
||||
|
||||
<!-- 인증번호 박스 (#ECF1FF · 라운드 10) -->
|
||||
<tr>
|
||||
<td style="padding-bottom:20px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#ecf1ff;border-radius:10px;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ecf1ff" style="background-color:#ecf1ff;border-radius:10px;padding:16px 20px;font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:14px;line-height:24px;color:#000000;font-weight:700;text-align:center;letter-spacing:2px;"><!-- 동적값: 인증번호 -->123456</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 안내 문구 -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#000000;padding-bottom:20px;">
|
||||
안녕하세요, <!-- 동적값: 회원명 -->김철수 님.<br>
|
||||
DJBank 개발자포탈 회원가입 시 입력하신 이메일을 인증해 주세요.<br>
|
||||
아래 인증번호를 회원가입 화면에 입력하시면 인증이 완료됩니다.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 경고 문구 (#FF2727) -->
|
||||
<tr>
|
||||
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#ff2727;font-weight:600;">※ 본 메일을 요청하지 않으셨다면 이 메일을 무시해 주시기 바랍니다.</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- ===== Footer : 제주은행 로고 + 안내 (#F1F1ED) ===== -->
|
||||
<tr>
|
||||
<td style="padding:7px 27px 26px 27px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1f1ed" style="background-color:#f1f1ed;border-radius:4px;">
|
||||
<tr>
|
||||
<td style="padding:12px 14px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td valign="middle" width="86" style="width:86px;padding-right:16px;line-height:0;">
|
||||
<img src="images/logo-jejubank.png" width="86" height="24" alt="제주은행" style="display:block;width:86px;height:24px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
|
||||
</td>
|
||||
<td valign="middle" style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;color:#0d0e11;">
|
||||
<p style="margin:0;font-size:10px;line-height:20px;color:#0d0e11;">본 메일은 발신 전용으로 회신되지 않습니다.</p>
|
||||
<p style="margin:0 0 9px 0;font-size:10px;line-height:20px;color:#0d0e11;">관련 문의사항은 고객센터 (????-????) 또는 홈페이지를 이용하시기 바랍니다.</p>
|
||||
<p style="margin:0 0 9px 0;font-size:10px;line-height:16px;color:#0d0e11;">제주 제주시 1100로 3351 (노형동)<span style="color:#c2c2bc;"> | </span>고객센터 : ????-?????<span style="color:#c2c2bc;"> | </span>사업자 등록번호 : 616-81-00615</p>
|
||||
<p style="margin:0;font-size:10px;line-height:16px;color:#454545;font-weight:500;">Copyright JEJUBANK. ALL Rights Reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- ============ /카드 ============ -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
# gradle 명령을 대체합니다.
|
||||
command gradle $@
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* CycloneDX SBOM(bom.json) -> Excel(xlsx) 변환 태스크.
|
||||
*
|
||||
* gradle sbomXlsx # cyclonedxBom 실행 후 변환
|
||||
* gradle sbomXlsx -PsbomJson=path.json # 기존 bom.json 사용(cyclonedxBom 생략)
|
||||
* gradle sbomXlsx -PsbomOut=out.xlsx # 출력 경로 지정
|
||||
*
|
||||
* buildscript 블록이 이 스크립트에만 적용되므로 POI 의존성이 메인 빌드
|
||||
* classpath 나 WAR 산출물에는 포함되지 않는다.
|
||||
*
|
||||
* 시트: 요약 / WAR 기준
|
||||
* 산출 기준은 war 태스크의 classpath(= runtimeClasspath) 이므로
|
||||
* test·annotationProcessor·developmentOnly·compileOnly 의존은 모두 제외된다.
|
||||
* bom.json 은 라이선스/해시/설명/직접-전이 판별을 위한 메타 소스로만 쓴다.
|
||||
*/
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url "https://nexus.eactive.synology.me:8090/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.apache.poi:poi-ooxml:3.17'
|
||||
}
|
||||
}
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import org.apache.poi.ss.usermodel.BorderStyle
|
||||
import org.apache.poi.ss.usermodel.FillPatternType
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
||||
import org.apache.poi.ss.usermodel.IndexedColors
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment
|
||||
import org.apache.poi.ss.util.CellRangeAddress
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||
|
||||
// 엑셀 셀 문자열 상한(32767)보다 여유를 둔 절단 길이
|
||||
ext.SBOM_CELL_LIMIT = 32000
|
||||
|
||||
task sbomXlsx {
|
||||
group = 'sbom'
|
||||
description = 'CycloneDX bom.json 을 WAR 수록 기준 xlsx 로 변환한다'
|
||||
|
||||
// -PsbomJson 으로 기존 산출물을 지정하면 재생성하지 않는다
|
||||
if (!project.hasProperty('sbomJson')) {
|
||||
dependsOn 'cyclonedxBom'
|
||||
}
|
||||
|
||||
doLast {
|
||||
File src = resolveBomJson(project)
|
||||
File out = project.hasProperty('sbomOut')
|
||||
? project.file(project.property('sbomOut'))
|
||||
: new File(project.buildDir, "reports/sbom/${sbomFileName(project)}")
|
||||
out.parentFile.mkdirs()
|
||||
|
||||
def bom = new JsonSlurper().parse(src, 'UTF-8')
|
||||
def deploy = collectDeployJars(project)
|
||||
def warRows = joinWarRows(deploy.jars, indexComponents(bom))
|
||||
|
||||
def wb = new XSSFWorkbook()
|
||||
def st = createStyles(wb)
|
||||
writeSummarySheet(wb, st, bom, warRows, src, deploy.label)
|
||||
writeWarSheet(wb, st, warRows)
|
||||
|
||||
out.withOutputStream { os -> wb.write(os) }
|
||||
wb.close()
|
||||
|
||||
int unmatched = warRows.count { it.matched == 'N' }
|
||||
logger.lifecycle("SBOM xlsx 생성: ${out.absolutePath} " +
|
||||
"(배포 수록 ${warRows.size()}개, SBOM 미매칭 ${unmatched}개, 원본 ${src.name})")
|
||||
}
|
||||
}
|
||||
|
||||
/** 산출 파일명: 배포 패키지명 기준 (war 있으면 war 파일명, 없으면 project 이름[-버전]) */
|
||||
String sbomFileName(Project p) {
|
||||
def warTask = p.tasks.findByName('war')
|
||||
if (warTask != null) {
|
||||
String archive = warTask.archiveFileName.get()
|
||||
return archive.replaceAll(/\.(war|jar|ear)$/, '') + '-sbom.xlsx'
|
||||
}
|
||||
String ver = (p.version == null || p.version.toString() in ['', 'unspecified']) ? '' : "-${p.version}"
|
||||
return "${p.name}${ver}-sbom.xlsx"
|
||||
}
|
||||
|
||||
/** bom.json 위치 결정: -PsbomJson > cyclonedxBom 산출 경로 후보 */
|
||||
File resolveBomJson(Project p) {
|
||||
if (p.hasProperty('sbomJson')) {
|
||||
File f = p.file(p.property('sbomJson'))
|
||||
if (!f.exists()) {
|
||||
throw new GradleException("bom.json 없음: ${f.absolutePath}")
|
||||
}
|
||||
return f
|
||||
}
|
||||
def candidates = [
|
||||
new File(p.buildDir, 'reports/cyclonedx/bom.json'),
|
||||
new File(p.buildDir, 'reports/bom.json'),
|
||||
]
|
||||
File found = candidates.find { it.exists() }
|
||||
if (found == null) {
|
||||
throw new GradleException(
|
||||
"bom.json 을 찾지 못했다. 확인한 경로: " + candidates*.absolutePath.join(', ') +
|
||||
"\n'gradle cyclonedxBom' 실행 후 재시도하거나 -PsbomJson=<경로> 로 지정한다.")
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 배포물에 packaging 되는 jar 목록.
|
||||
* war 프로젝트는 war 태스크 classpath(= runtimeClasspath), 그 외는 runtimeClasspath 를
|
||||
* 기준으로 하므로 test/annotationProcessor/developmentOnly/compileOnly 는 자동으로 빠진다.
|
||||
*
|
||||
* @return [label: 기준 설명, jars: 행 목록]
|
||||
*/
|
||||
Map collectDeployJars(Project p) {
|
||||
def cfg = p.configurations.findByName('runtimeClasspath')
|
||||
if (cfg == null) {
|
||||
p.logger.warn("[${p.name}] runtimeClasspath 가 없어 배포 기준 시트를 비운다")
|
||||
return [label: '(없음)', jars: []]
|
||||
}
|
||||
|
||||
def warTask = p.tasks.findByName('war')
|
||||
def files
|
||||
String label
|
||||
if (warTask != null) {
|
||||
files = warTask.classpath.files
|
||||
label = 'WAR WEB-INF/lib (war 태스크 classpath)'
|
||||
} else {
|
||||
files = cfg.files
|
||||
label = 'runtimeClasspath (war 태스크 없음)'
|
||||
}
|
||||
|
||||
def coordByFile = [:]
|
||||
cfg.resolvedConfiguration.resolvedArtifacts.each { a ->
|
||||
def id = a.moduleVersion.id
|
||||
coordByFile[a.file] = [group: id.group, name: id.name, version: id.version]
|
||||
}
|
||||
def jars = files.findAll { it.name.endsWith('.jar') }.collect { f ->
|
||||
def c = coordByFile[f]
|
||||
[
|
||||
file : f.name,
|
||||
group : c?.group ?: '',
|
||||
name : c?.name ?: f.name.replaceAll(/\.jar$/, ''),
|
||||
version: c?.version ?: '',
|
||||
coord : c ? "${c.group}:${c.name}:${c.version}".toString() : '',
|
||||
]
|
||||
}.sort { it.file }
|
||||
|
||||
return [label: label, jars: jars]
|
||||
}
|
||||
|
||||
/** bom.json 컴포넌트를 'group:name:version' 키로 색인 (라이선스/해시/설명/직접-전이) */
|
||||
Map indexComponents(bom) {
|
||||
String rootRef = bom.metadata?.component?.'bom-ref'
|
||||
Set directRefs = (bom.dependencies?.find { it.ref == rootRef }?.dependsOn ?: []) as Set
|
||||
|
||||
def index = [:]
|
||||
bom.components?.each { c ->
|
||||
def hashes = [:]
|
||||
c.hashes?.each { h -> hashes[h.alg] = h.content }
|
||||
|
||||
def licenses = (c.licenses ?: []).collect { l ->
|
||||
l.license?.id ?: l.license?.name ?: l.expression ?: ''
|
||||
}.findAll { it }
|
||||
|
||||
index["${c.group ?: ''}:${c.name ?: ''}:${c.version ?: ''}".toString()] = [
|
||||
direct : directRefs.contains(c.'bom-ref') ? '직접' : '전이',
|
||||
licenses : licenses.join('; '),
|
||||
licenseList: licenses.isEmpty() ? ['(미상)'] : licenses,
|
||||
purl : c.purl ?: '',
|
||||
sha256 : hashes['SHA-256'] ?: '',
|
||||
sha1 : hashes['SHA-1'] ?: '',
|
||||
description: c.description ?: '',
|
||||
]
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/** WAR jar 목록에 SBOM 메타를 좌표로 결합 */
|
||||
List joinWarRows(List warJars, Map index) {
|
||||
def result = []
|
||||
warJars.eachWithIndex { j, i ->
|
||||
def m = j.coord ? index[j.coord] : null
|
||||
result << [
|
||||
no : i + 1,
|
||||
file : j.file,
|
||||
group : j.group,
|
||||
name : j.name,
|
||||
version : j.version,
|
||||
direct : m?.direct ?: '',
|
||||
licenses : m?.licenses ?: '',
|
||||
licenseList: m?.licenseList ?: ['(미상)'],
|
||||
purl : m?.purl ?: '',
|
||||
sha256 : m?.sha256 ?: '',
|
||||
sha1 : m?.sha1 ?: '',
|
||||
matched : (m != null) ? 'Y' : 'N',
|
||||
description: m?.description ?: '',
|
||||
]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Map createStyles(wb) {
|
||||
def headFont = wb.createFont()
|
||||
headFont.setBold(true)
|
||||
headFont.setColor(IndexedColors.WHITE.getIndex())
|
||||
|
||||
def head = wb.createCellStyle()
|
||||
head.setFont(headFont)
|
||||
head.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex())
|
||||
head.setFillPattern(FillPatternType.SOLID_FOREGROUND)
|
||||
head.setAlignment(HorizontalAlignment.CENTER)
|
||||
head.setVerticalAlignment(VerticalAlignment.CENTER)
|
||||
head.setBorderBottom(BorderStyle.THIN)
|
||||
|
||||
def body = wb.createCellStyle()
|
||||
body.setVerticalAlignment(VerticalAlignment.TOP)
|
||||
|
||||
def wrap = wb.createCellStyle()
|
||||
wrap.setVerticalAlignment(VerticalAlignment.TOP)
|
||||
wrap.setWrapText(true)
|
||||
|
||||
def labelFont = wb.createFont()
|
||||
labelFont.setBold(true)
|
||||
def label = wb.createCellStyle()
|
||||
label.setFont(labelFont)
|
||||
|
||||
return [head: head, body: body, wrap: wrap, label: label]
|
||||
}
|
||||
|
||||
/** 헤더 행 생성 + 폭 지정 + 틀고정 */
|
||||
def writeHeader(sheet, style, List<String> headers, List<Integer> widths) {
|
||||
def row = sheet.createRow(0)
|
||||
row.setHeightInPoints(20f)
|
||||
headers.eachWithIndex { h, i ->
|
||||
def cell = row.createCell(i)
|
||||
cell.setCellValue(h)
|
||||
cell.setCellStyle(style)
|
||||
sheet.setColumnWidth(i, widths[i] * 256)
|
||||
}
|
||||
sheet.createFreezePane(0, 1)
|
||||
}
|
||||
|
||||
def cellOf(row, int idx, value, style) {
|
||||
def cell = row.createCell(idx)
|
||||
String s = (value == null) ? '' : value.toString()
|
||||
if (s.length() > SBOM_CELL_LIMIT) {
|
||||
s = s.substring(0, SBOM_CELL_LIMIT) + '…(생략)'
|
||||
}
|
||||
cell.setCellValue(s)
|
||||
cell.setCellStyle(style)
|
||||
return cell
|
||||
}
|
||||
|
||||
def writeSummarySheet(wb, st, bom, List warRows, File src, String basisLabel) {
|
||||
def sheet = wb.createSheet('요약')
|
||||
def comp = bom.metadata?.component ?: [:]
|
||||
def tool = bom.metadata?.tools?.components?.getAt(0)
|
||||
Set licenseKinds = warRows.collectMany { it.licenseList } as Set
|
||||
|
||||
def items = [
|
||||
['대상 프로젝트', "${comp.group ?: ''}:${comp.name ?: ''}:${comp.version ?: ''}"],
|
||||
['산출 기준', "${basisLabel} — test/annotationProcessor/compileOnly 제외"],
|
||||
['BOM 포맷', "${bom.bomFormat ?: ''} ${bom.specVersion ?: ''}"],
|
||||
['serialNumber', bom.serialNumber ?: ''],
|
||||
['생성 시각', bom.metadata?.timestamp ?: ''],
|
||||
['생성 도구', tool ? "${tool.name} ${tool.version}" : ''],
|
||||
['원본 파일', src.absolutePath],
|
||||
['배포 수록 jar', warRows.size()],
|
||||
[' └ 직접 의존', warRows.count { it.direct == '직접' }],
|
||||
[' └ 전이 의존', warRows.count { it.direct == '전이' }],
|
||||
[' └ SBOM 미매칭', warRows.count { it.matched == 'N' }],
|
||||
['라이선스 종류', licenseKinds.size()],
|
||||
['라이선스 미상', warRows.count { it.licenses.isEmpty() }],
|
||||
]
|
||||
|
||||
writeHeader(sheet, st.head, ['항목', '값'], [30, 90])
|
||||
items.eachWithIndex { item, i ->
|
||||
def row = sheet.createRow(i + 1)
|
||||
cellOf(row, 0, item[0], st.label)
|
||||
cellOf(row, 1, item[1], st.body)
|
||||
}
|
||||
|
||||
// 라이선스 분포 (요약 하단)
|
||||
def byLicense = [:].withDefault { 0 }
|
||||
warRows.each { r -> r.licenseList.each { lic -> byLicense[lic] = byLicense[lic] + 1 } }
|
||||
def sorted = byLicense.entrySet().sort { a, b -> (b.value <=> a.value) ?: (a.key <=> b.key) }
|
||||
|
||||
int base = items.size() + 2
|
||||
def hdr = sheet.createRow(base)
|
||||
cellOf(hdr, 0, '라이선스', st.head)
|
||||
cellOf(hdr, 1, 'jar 수', st.head)
|
||||
sorted.eachWithIndex { e, i ->
|
||||
def row = sheet.createRow(base + 1 + i)
|
||||
cellOf(row, 0, e.key, st.body)
|
||||
cellOf(row, 1, e.value, st.body)
|
||||
}
|
||||
}
|
||||
|
||||
/** 실제 배포물(WAR WEB-INF/lib) 기준 시트 */
|
||||
def writeWarSheet(wb, st, List warRows) {
|
||||
def sheet = wb.createSheet('WAR 기준')
|
||||
def headers = ['No', 'jar 파일명', 'Group', 'Name', 'Version', '구분',
|
||||
'License', 'purl', 'SHA-256', 'SHA-1', 'SBOM매칭', 'Description']
|
||||
def widths = [6, 46, 32, 34, 16, 7, 30, 60, 40, 30, 10, 60]
|
||||
writeHeader(sheet, st.head, headers, widths)
|
||||
|
||||
warRows.eachWithIndex { r, i ->
|
||||
def row = sheet.createRow(i + 1)
|
||||
cellOf(row, 0, r.no, st.body)
|
||||
cellOf(row, 1, r.file, st.body)
|
||||
cellOf(row, 2, r.group, st.body)
|
||||
cellOf(row, 3, r.name, st.body)
|
||||
cellOf(row, 4, r.version, st.body)
|
||||
cellOf(row, 5, r.direct, st.body)
|
||||
cellOf(row, 6, r.licenses, st.body)
|
||||
cellOf(row, 7, r.purl, st.body)
|
||||
cellOf(row, 8, r.sha256, st.body)
|
||||
cellOf(row, 9, r.sha1, st.body)
|
||||
cellOf(row, 10, r.matched, st.body)
|
||||
cellOf(row, 11, r.description, st.wrap)
|
||||
}
|
||||
if (!warRows.isEmpty()) {
|
||||
sheet.setAutoFilter(new CellRangeAddress(0, warRows.size(), 0, headers.size() - 1))
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
pipeline {
|
||||
// 25.10.01 : 기본 브랜치 staging -> jenkins_with_weblogic 로 변경
|
||||
|
||||
agent { label 'apipod01' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/5 * * * *')
|
||||
}
|
||||
|
||||
environment {
|
||||
// Java 환경 설정
|
||||
JAVA_HOME = '/App/jenkins/jdks/jdk1.8.0_441'
|
||||
PATH = "${JAVA_HOME}/bin:${env.PATH}"
|
||||
|
||||
// Gradle 환경 설정
|
||||
GRADLE_HOME = '/App/jenkins/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/App/jenkins/gradle-home'
|
||||
// GRADLE_OPTS = '--offline'
|
||||
|
||||
// Git 저장소 정보
|
||||
GIT_SERVER = '192.168.240.178:18081'
|
||||
GIT_CREDENTIALS_ID = 'git-ssh-credentials'
|
||||
|
||||
// 프로젝트 디렉토리 구조
|
||||
PROJECT_HOME = '/Data/jenkins/eapim'
|
||||
|
||||
BRANCH = 'jenkins_with_weblogic'
|
||||
}
|
||||
|
||||
|
||||
stages {
|
||||
stage('Git pull') {
|
||||
steps {
|
||||
echo "[portal] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/eapim-portal") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/eapim-portal", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
|
||||
echo "[online-core-jpa] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/eapim-online/elink-online-core-jpa") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/elink-online-core-jpa", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
|
||||
echo "[portal-common] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/elink-portal-common") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/elink-portal-common", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
|
||||
echo "[kjb-safedb] Git pull from branch: ${BRANCH}"
|
||||
dir("${PROJECT_HOME}/kjb-safedb") {
|
||||
git url: "ssh://git@${GIT_SERVER}/eapim/kjb-safedb", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Build') {
|
||||
steps {
|
||||
tool name: 'Oracle-JDK-1.8', type: 'hudson.model.JDK'
|
||||
// tool name: 'Gradle-8.7', type: 'hudson.tasks.Gradle'
|
||||
|
||||
// echo "[online-core-jpa] Build project"
|
||||
// dir("${PROJECT_HOME}/elink-online-multi/elink-online-core-jpa") {
|
||||
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
// }
|
||||
|
||||
// echo "[portal-common] Build project"
|
||||
// dir("${PROJECT_HOME}/elink-portal-common") {
|
||||
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
// }
|
||||
|
||||
echo "[portal] Build project"
|
||||
dir("${PROJECT_HOME}/eapim-portal") {
|
||||
sh '${GRADLE_HOME}/bin/gradle clean build -x test'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Archive WAR') {
|
||||
steps {
|
||||
echo "[portal] Archive WAR file"
|
||||
dir("${PROJECT_HOME}/eapim-portal/build/libs") {
|
||||
archiveArtifacts artifacts: '*.war', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# 광주은행 eAPIM
|
||||
|
||||
## 커스텀 항목
|
||||
### 포탈 Property (EMS 프로퍼티)
|
||||
- `disable_features.user_email_verify' == true
|
||||
이메일 인증 기능 비활성화(회원 가입시 이메일 인증은 한걸로 처리)
|
||||
|
||||
### 디비 접속 방법 Direct -> JNDI 로 변경
|
||||
- 은행 내부 환경에선 모두 JNDI 사용하기 때문에 서버환경에서 동작하는 profile 내 application.yaml 에서 디비 접속 정보 삭제
|
||||
|
||||
## 서버 운영 중 긴급 로깅이 필요할 경우 JVM 파라미터 추가 하여 재기동
|
||||
- DEBUG 레벨 root 로그 / debug.log 로 파일 작성
|
||||
```
|
||||
-Dlogging.config=classpath:logback-debug.xml
|
||||
```
|
||||
|
||||
## 참고자료
|
||||
### 개발용 파라미터
|
||||
```
|
||||
-Dlogging.config=classpath:logback-rinjae.xml
|
||||
-Dinst.Name=devSvr98
|
||||
-Duser.language=en
|
||||
-Duser.country=US
|
||||
-Dkjb_safedb.mode=fake
|
||||
```
|
||||
@@ -1,113 +0,0 @@
|
||||
# 사업자등록번호 중복 체크 구현 작업 계획서
|
||||
|
||||
## 1. 현황 분석
|
||||
|
||||
### 문제점
|
||||
- 법인 등록 시 사업자등록번호(compRegNo) 중복 검증이 없음
|
||||
- `PortalOrgRepository.findByCompRegNo()` 메서드는 존재하나 사용되지 않음
|
||||
- 동일한 사업자등록번호로 여러 법인이 등록될 수 있는 상태
|
||||
|
||||
### 검증 시점
|
||||
1. **실시간 검증**: 사용자가 사업자등록번호 입력 시 `/check-business-number` API 호출
|
||||
2. **등록 시 검증**: 실제 법인 생성 시점 (`registerOrgFromDTOWithFile`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 수정 대상 파일
|
||||
|
||||
| 파일 경로 | 수정 내용 |
|
||||
|-----------|-----------|
|
||||
| `src/main/java/.../apps/user/service/PortalOrgService.java` | 사업자등록번호 중복 체크 메서드 추가 |
|
||||
| `src/main/java/.../apps/user/facade/OrgRegisterFacadeImpl.java` | `checkBusinessNumber()` 중복 체크 로직 추가, 등록 전 중복 체크 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 상세
|
||||
|
||||
### 3.1 PortalOrgService.java
|
||||
|
||||
```java
|
||||
// 추가할 메서드
|
||||
public boolean existsByCompRegNo(String compRegNo) {
|
||||
return portalOrgRepository.findByCompRegNo(compRegNo).isPresent();
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 OrgRegisterFacadeImpl.java
|
||||
|
||||
#### checkBusinessNumber 메서드 수정
|
||||
- 형식 검증 + 중복 검증 동시 수행
|
||||
- 중복 시 "이미 등록된 사업자등록번호입니다." 메시지 반환
|
||||
|
||||
```java
|
||||
@Override
|
||||
public ResponseEntity<ValidationResponse> checkBusinessNumber(String compRegNo) {
|
||||
// 1. 형식 검증
|
||||
boolean isValidFormat = validationService.isValidBusinessNumber(compRegNo);
|
||||
if (!isValidFormat) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "유효하지 않은 사업자 등록번호입니다."));
|
||||
}
|
||||
|
||||
// 2. 중복 검증
|
||||
boolean isDuplicate = portalOrgService.existsByCompRegNo(compRegNo);
|
||||
if (isDuplicate) {
|
||||
return ResponseEntity.ok(new ValidationResponse(false, "이미 등록된 사업자등록번호입니다."));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new ValidationResponse(true, "사용 가능한 사업자 등록번호입니다."));
|
||||
}
|
||||
```
|
||||
|
||||
#### 법인 생성 메서드들에 중복 체크 추가
|
||||
- `registerNewCorporateUser()`
|
||||
- `convertExistingUserToCorporate()`
|
||||
- `registerNewCorporateUserWithExistingData()`
|
||||
|
||||
각 메서드에서 `portalOrgService.registerOrgFromDTOWithFile()` 호출 전 중복 체크 수행
|
||||
|
||||
---
|
||||
|
||||
## 4. 검증 흐름
|
||||
|
||||
```
|
||||
[사용자 입력]
|
||||
↓
|
||||
[실시간 검증] /check-business-number API
|
||||
├── 형식 검증 (10자리 숫자)
|
||||
└── 중복 검증 (DB 조회)
|
||||
↓
|
||||
[등록 요청]
|
||||
↓
|
||||
[등록 시 검증] registerOrgFromDTOWithFile 호출 전
|
||||
└── 중복 체크 (이중 방어)
|
||||
↓
|
||||
[법인 생성]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 기존 중복 데이터 처리
|
||||
|
||||
- 기존에 중복 등록된 데이터는 그대로 유지
|
||||
- 신규 등록 시에만 중복 체크 적용
|
||||
- 필요시 관리자 화면에서 중복 데이터 조회/정리 기능 별도 개발 가능
|
||||
|
||||
---
|
||||
|
||||
## 6. 테스트 시나리오
|
||||
|
||||
| 시나리오 | 예상 결과 |
|
||||
|----------|-----------|
|
||||
| 형식이 잘못된 사업자등록번호 입력 | "유효하지 않은 사업자 등록번호입니다." |
|
||||
| 이미 등록된 사업자등록번호 입력 | "이미 등록된 사업자등록번호입니다." |
|
||||
| 사용 가능한 사업자등록번호 입력 | "사용 가능한 사업자 등록번호입니다." |
|
||||
| 중복된 번호로 법인 등록 시도 | 등록 실패, 에러 메시지 표시 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 작업 완료
|
||||
|
||||
- [x] PortalOrgService에 existsByCompRegNo 메서드 추가
|
||||
- [x] OrgRegisterFacadeImpl.checkBusinessNumber 수정
|
||||
- [x] 법인 등록 메서드들에 중복 체크 로직 추가
|
||||
- [x] 빌드 검증 완료
|
||||
@@ -1,165 +0,0 @@
|
||||
# 네비게이션 바 디자인 Survey 기능 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
직원 대상 네비게이션 바 디자인 선호도 조사를 위한 UI 기능입니다.
|
||||
사용자가 A, B, C 등의 버튼을 클릭하면 실시간으로 네비게이션 바 디자인이 변경됩니다.
|
||||
|
||||
## 기능 활성화/비활성화
|
||||
|
||||
### PortalProperty 설정
|
||||
|
||||
DB의 `ptl_property` 테이블에서 `ui.design-survey` 값을 설정합니다.
|
||||
|
||||
```sql
|
||||
-- 활성화
|
||||
UPDATE ptl_property
|
||||
SET property_value = 'true'
|
||||
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
|
||||
|
||||
-- 비활성화
|
||||
UPDATE ptl_property
|
||||
SET property_value = 'false'
|
||||
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
|
||||
```
|
||||
|
||||
> 최초 페이지 로드 시 속성이 자동 생성됩니다 (기본값: `false`)
|
||||
|
||||
---
|
||||
|
||||
## 디자인 옵션 추가/수정 방법
|
||||
|
||||
### 파일 위치
|
||||
`src/main/resources/templates/views/fragment/kjbank/header_container.html`
|
||||
|
||||
### DESIGN_OPTIONS 배열
|
||||
|
||||
디자인 옵션은 JavaScript의 `DESIGN_OPTIONS` 배열에서 관리됩니다:
|
||||
|
||||
```javascript
|
||||
const DESIGN_OPTIONS = [
|
||||
{
|
||||
id: 'A', // 고유 식별자 (필수)
|
||||
label: 'A (현재)', // 버튼에 표시될 텍스트 (필수)
|
||||
isDefault: true, // 기본 선택 옵션 (선택, 하나만 true)
|
||||
styles: {} // 적용할 CSS 스타일 (필수)
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: 'B',
|
||||
styles: {
|
||||
'.logo-text': { 'font-size': '18px' },
|
||||
'.nav-link': { 'margin': '0 20px' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'C',
|
||||
styles: {
|
||||
'.nav-link': { 'margin': '0 20px', 'font-weight': 'bold' }
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 새 디자인 옵션 추가 예시
|
||||
|
||||
```javascript
|
||||
// D 옵션 추가
|
||||
{
|
||||
id: 'D',
|
||||
label: 'D',
|
||||
styles: {
|
||||
'.logo-text': {
|
||||
'font-size': '16px',
|
||||
'color': '#333333'
|
||||
},
|
||||
'.nav-link': {
|
||||
'padding': '10px 24px',
|
||||
'font-weight': '600'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### styles 객체 구조
|
||||
|
||||
```javascript
|
||||
styles: {
|
||||
'CSS 셀렉터': {
|
||||
'CSS 속성': '값',
|
||||
'CSS 속성2': '값2'
|
||||
},
|
||||
'다른 셀렉터': {
|
||||
'CSS 속성': '값'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자주 사용되는 CSS 셀렉터
|
||||
|
||||
| 셀렉터 | 설명 |
|
||||
|--------|------|
|
||||
| `.logo-text` | 로고 텍스트 "API Portal" |
|
||||
| `.nav-link` | 네비게이션 메뉴 링크 |
|
||||
| `.nav-menu` | 네비게이션 메뉴 컨테이너 |
|
||||
| `.header-content` | 헤더 콘텐츠 영역 |
|
||||
| `.global-header` | 전체 헤더 |
|
||||
| `.logo-wrapper` | 로고 래퍼 (이미지 + 텍스트) |
|
||||
|
||||
## 자주 사용되는 CSS 속성
|
||||
|
||||
| 속성 | 예시 값 | 설명 |
|
||||
|------|---------|------|
|
||||
| `font-size` | `18px`, `1.2rem` | 글자 크기 |
|
||||
| `font-weight` | `400`, `500`, `600`, `bold` | 글자 두께 |
|
||||
| `margin` | `0 20px`, `10px 15px` | 바깥 여백 |
|
||||
| `padding` | `8px 16px` | 안쪽 여백 |
|
||||
| `color` | `#333333`, `#0049b4` | 글자 색상 |
|
||||
| `background` | `#ffffff`, `#f5f5f5` | 배경 색상 |
|
||||
| `gap` | `12px`, `20px` | flex 아이템 간격 |
|
||||
|
||||
---
|
||||
|
||||
## 사용자 선택 저장
|
||||
|
||||
사용자의 디자인 선택은 브라우저의 `localStorage`에 저장됩니다.
|
||||
|
||||
- Key: `design-survey-selection`
|
||||
- Value: 선택한 디자인 ID (예: `A`, `B`, `C`)
|
||||
|
||||
```javascript
|
||||
// 저장된 선택 확인 (개발자 도구 콘솔)
|
||||
localStorage.getItem('design-survey-selection')
|
||||
|
||||
// 선택 초기화
|
||||
localStorage.removeItem('design-survey-selection')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `GlobalControllerAdvice.java` | `designSurveyEnabled` ModelAttribute 제공 |
|
||||
| `header_container.html` | Survey 바 HTML 및 DESIGN_OPTIONS 설정 |
|
||||
| `_header.scss` | Survey 바 스타일 |
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **디자인 옵션 ID는 고유해야 합니다** - 중복 ID 사용 시 오동작
|
||||
2. **isDefault는 하나의 옵션에만 설정** - 여러 개 설정 시 첫 번째만 적용
|
||||
3. **CSS 속성명은 kebab-case 사용** - `fontSize` (X) → `font-size` (O)
|
||||
4. **스타일 적용 시 `!important` 자동 추가** - 기존 스타일 덮어씀
|
||||
5. **Survey 기능 종료 후** - `ui.design-survey`를 `false`로 설정하여 비활성화
|
||||
|
||||
---
|
||||
|
||||
## 문의
|
||||
|
||||
기능 관련 문의는 개발팀에 연락해주세요.
|
||||
@@ -1,105 +0,0 @@
|
||||
flowchart TD
|
||||
Start([법인 관리자가 사용자 초대]) --> GenerateToken[8글자 토큰 생성<br/>예: K7MNPQ2R<br/>UserInvitation 저장<br/>상태: PENDING]
|
||||
|
||||
GenerateToken --> SendEmail[이메일 발송<br/>- 링크 포함<br/>- 8글자 토큰 표시]
|
||||
|
||||
SendEmail --> UserType{사용자 유형}
|
||||
|
||||
%% ========================================
|
||||
%% 신규 회원 플로우
|
||||
%% ========================================
|
||||
UserType -->|신규 회원<br/>미가입자| NewUserPath{초대 확인 방법}
|
||||
|
||||
NewUserPath -->|이메일 링크| EmailLink1[이메일 링크 클릭<br/>/signup/portalUser?invitation=CODE]
|
||||
NewUserPath -->|일반 회원가입| DirectSignup[일반 회원가입 진행<br/>ROLE_USER로 생성]
|
||||
|
||||
EmailLink1 --> SignupPage1[회원가입 페이지<br/>- 이메일 자동 입력됨<br/>- 법인명 표시]
|
||||
SignupPage1 --> RegisterForm[가입 정보 입력<br/>초대 수락]
|
||||
RegisterForm --> RegisterComplete[회원가입 완료<br/>ROLE_CORP_USER로 생성<br/>상태: COMPLETED]
|
||||
RegisterComplete --> NewUserEnd([가입 완료 안내])
|
||||
|
||||
DirectSignup --> Login4[로그인]
|
||||
Login4 --> AfterLogin4[로그인 성공]
|
||||
AfterLogin4 --> CheckInvitation
|
||||
|
||||
%% ========================================
|
||||
%% 기존 회원 플로우
|
||||
%% ========================================
|
||||
UserType -->|기존 회원<br/>ROLE_USER| ExistingUserPath{초대 확인 방법}
|
||||
|
||||
ExistingUserPath -->|이메일 링크<br/>비로그인 상태| EmailLink2[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
|
||||
ExistingUserPath -->|이메일 링크<br/>로그인 상태| EmailLinkLoggedIn[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
|
||||
ExistingUserPath -->|직접 로그인| DirectLogin[로그인 페이지에서 로그인]
|
||||
|
||||
%% 비로그인 상태에서 이메일 링크 클릭
|
||||
EmailLink2 --> DecisionPagePublic[초대 수락 페이지<br/>비로그인 상태]
|
||||
DecisionPagePublic --> RedirectLogin2[로그인 유도]
|
||||
RedirectLogin2 --> Login2[로그인]
|
||||
Login2 --> AfterLogin2[로그인 성공]
|
||||
|
||||
%% 로그인 상태에서 이메일 링크 클릭 → 바로 수락 페이지
|
||||
EmailLinkLoggedIn --> DecisionPageDirect[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
|
||||
DecisionPageDirect --> UserDecision
|
||||
|
||||
%% 직접 로그인
|
||||
DirectLogin --> Login3[로그인]
|
||||
Login3 --> AfterLogin3[로그인 성공]
|
||||
|
||||
%% ========================================
|
||||
%% 로그인 후 초대 확인 (PortalAuthenticationSuccessHandler)
|
||||
%% ========================================
|
||||
AfterLogin2 --> CheckInvitation[PortalAuthenticationSuccessHandler<br/>PENDING 초대 확인<br/>findFirstByInvitationEmailAndStatus]
|
||||
AfterLogin3 --> CheckInvitation
|
||||
|
||||
CheckInvitation --> HasInvitation{PENDING 초대 존재?}
|
||||
|
||||
HasInvitation -->|없음| NormalHome[메인 페이지 이동]
|
||||
HasInvitation -->|있음| SaveToSession[세션에 초대 정보 저장<br/>pendingInvitation=true<br/>pendingInvitationToken<br/>pendingInvitationOrgName]
|
||||
|
||||
SaveToSession --> RedirectHome[메인 페이지로 이동]
|
||||
|
||||
%% ========================================
|
||||
%% 메인 페이지 팝업 (index.html)
|
||||
%% ========================================
|
||||
RedirectHome --> ShowPopup[팝업 표시<br/>OOO에서 초대가 도착했습니다<br/>초대를 확인하시겠습니까?]
|
||||
|
||||
ShowPopup --> PopupChoice{사용자 선택}
|
||||
|
||||
PopupChoice -->|확인| GoDecisionPage[초대 수락 페이지 이동<br/>/signup/decision?invitation=TOKEN]
|
||||
PopupChoice -->|취소| StayHome[메인 페이지 유지<br/>다음 로그인 시 팝업 재표시]
|
||||
|
||||
StayHome --> NextLogin[다음 로그인]
|
||||
NextLogin --> CheckInvitation
|
||||
|
||||
GoDecisionPage --> DecisionPage[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
|
||||
|
||||
DecisionPage --> UserDecision{사용자 선택}
|
||||
|
||||
UserDecision -->|수락| AcceptProcess[초대 수락 처리<br/>1. PortalOrg 변경<br/>2. ROLE_CORP_USER 전환<br/>3. 약관 재동의<br/>상태: COMPLETED]
|
||||
UserDecision -->|거절| RejectProcess[초대 거절 처리<br/>상태: REJECTED]
|
||||
|
||||
AcceptProcess --> LogoutPrompt[로그아웃 후 재로그인 안내]
|
||||
LogoutPrompt --> ExistingUserEnd([초대 수락 완료])
|
||||
|
||||
RejectProcess --> RejectEnd([초대 거절 완료<br/>다음 로그인 시 팝업 안 뜸])
|
||||
|
||||
%% ========================================
|
||||
%% 스타일링
|
||||
%% ========================================
|
||||
style Start fill:#e1f5e1
|
||||
style NewUserEnd fill:#c8e6c9
|
||||
style ExistingUserEnd fill:#c8e6c9
|
||||
style RejectEnd fill:#fff9c4
|
||||
style NormalHome fill:#e3f2fd
|
||||
|
||||
style GenerateToken fill:#bbdefb
|
||||
style AcceptProcess fill:#c5cae9
|
||||
style RejectProcess fill:#ffecb3
|
||||
|
||||
style CheckInvitation fill:#ffe0b2
|
||||
style SaveToSession fill:#ffe0b2
|
||||
style ShowPopup fill:#f8bbd0
|
||||
style PopupChoice fill:#f8bbd0
|
||||
|
||||
style StayHome fill:#e1bee7
|
||||
style NextLogin fill:#e1bee7
|
||||
@@ -1,44 +0,0 @@
|
||||
flowchart TD
|
||||
Start([법인 관리자가 사용자 초대 시작]) --> CheckUser{가입된 사용자인가?}
|
||||
|
||||
CheckUser -->|아니오| Error1[에러: 가입된 사용자만 초대 가능]
|
||||
CheckUser -->|예| CheckRole{이미 법인 사용자인가?}
|
||||
|
||||
CheckRole -->|예| Error2[에러: 이미 기관에 등록된 사용자]
|
||||
CheckRole -->|아니오 ROLE_USER| CreateInvitation[8글자 토큰 생성<br/>UserInvitation 저장<br/>상태: PENDING]
|
||||
|
||||
CreateInvitation --> SendEmail[이메일 발송<br/>UserInvitationEvent]
|
||||
|
||||
SendEmail --> UserReceive{사용자가 토큰 받음}
|
||||
|
||||
UserReceive -->|폐쇄망| ManualInput[8글자 토큰 직접 입력<br/>예: K7MNPQ2R]
|
||||
UserReceive -->|이메일 링크| EmailLink[Base64 인코딩된<br/>링크 클릭]
|
||||
|
||||
ManualInput --> DecodeToken[토큰 디코딩<br/>decodeInvitationToken]
|
||||
EmailLink --> DecodeToken
|
||||
|
||||
DecodeToken --> ValidateToken{토큰 유효성 검증}
|
||||
|
||||
ValidateToken -->|무효| Error3[에러: 유효하지 않은 초대]
|
||||
ValidateToken -->|유효 & PENDING| ShowDecision[약관 동의 페이지<br/>/signup/decision]
|
||||
|
||||
ShowDecision --> UserDecision{사용자 선택}
|
||||
|
||||
UserDecision -->|수락| ProcessAccept[processInvitation: accept]
|
||||
UserDecision -->|거절| ProcessReject[processInvitation: reject]
|
||||
|
||||
ProcessAccept --> UpdateUser[1. PortalOrg 변경<br/>2. ROLE_CORP_USER로 전환<br/>3. 약관 재동의 PRIVACY_COLLECT_ORG]
|
||||
UpdateUser --> CompleteInvitation[Invitation 상태:<br/>COMPLETED + completeDate]
|
||||
CompleteInvitation --> Success([초대 수락 완료<br/>로그아웃 후 재로그인])
|
||||
|
||||
ProcessReject --> RejectInvitation[Invitation 상태:<br/>REJECTED + completeDate]
|
||||
RejectInvitation --> End([초대 거절 완료])
|
||||
|
||||
style Start fill:#e1f5e1
|
||||
style Success fill:#c8e6c9
|
||||
style End fill:#fff9c4
|
||||
style Error1 fill:#ffcdd2
|
||||
style Error2 fill:#ffcdd2
|
||||
style Error3 fill:#ffcdd2
|
||||
style CreateInvitation fill:#bbdefb
|
||||
style UpdateUser fill:#c5cae9
|
||||
@@ -1,122 +0,0 @@
|
||||
sequenceDiagram
|
||||
actor 법인관리자
|
||||
participant Portal as Portal UI
|
||||
participant UserManFacade
|
||||
participant UserInvitationRepo as UserInvitation Repository
|
||||
participant MessageService as Message Handler
|
||||
participant Email as Email System
|
||||
actor 초대받은사용자
|
||||
participant AuthHandler as PortalAuthenticationSuccessHandler
|
||||
participant IndexController
|
||||
participant MainPage as 메인 페이지 (index.html)
|
||||
participant UserRegisterController
|
||||
participant UserRegisterFacade
|
||||
participant PortalUserRepo as PortalUser Repository
|
||||
participant PortalOrgRepo as PortalOrg Repository
|
||||
|
||||
%% ========================================
|
||||
%% 1. 초대 발송
|
||||
%% ========================================
|
||||
법인관리자->>Portal: 사용자 이메일 입력<br/>초대 요청
|
||||
Portal->>UserManFacade: sendInvitation(sender, emailAddr)
|
||||
|
||||
UserManFacade->>PortalUserRepo: findByEmailAddr(emailAddr)
|
||||
alt 사용자가 없거나 이미 법인 사용자
|
||||
UserManFacade-->>Portal: 에러 반환
|
||||
Portal-->>법인관리자: 초대 불가 알림
|
||||
else 일반 사용자 ROLE_USER 또는 비회원
|
||||
UserManFacade->>UserManFacade: generateEightCharToken()<br/>예: K7MNPQ2R
|
||||
UserManFacade->>UserInvitationRepo: save(UserInvitation)<br/>status: PENDING<br/>expiresOn: 7일 후
|
||||
UserManFacade->>MessageService: publishEvent(UserInvitationEvent)
|
||||
MessageService->>Email: 초대 이메일 발송<br/>(법인명, 관리자명, 토큰)
|
||||
UserManFacade-->>Portal: 초대 완료
|
||||
Portal-->>법인관리자: 초대 성공 알림
|
||||
end
|
||||
|
||||
Email->>초대받은사용자: 초대 이메일 수신
|
||||
|
||||
%% ========================================
|
||||
%% 2-A. 로그인 상태에서 이메일 링크 클릭 (바로 수락 페이지)
|
||||
%% ========================================
|
||||
alt 로그인 상태에서 이메일 링크 클릭
|
||||
초대받은사용자->>Portal: 이메일 링크 클릭<br/>/signup/decision?invitation=TOKEN
|
||||
Portal->>UserRegisterController: showDecisionPage(invitationToken)
|
||||
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
|
||||
UserRegisterController-->>Portal: 약관 동의 페이지
|
||||
Portal-->>초대받은사용자: 초대 수락/거절 화면<br/>(바로 표시, 팝업 없음)
|
||||
Note over 초대받은사용자: 6. 초대 수락/거절 처리로 이동
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 2-B. 로그인 (기존 회원 또는 신규 가입 후)
|
||||
%% ========================================
|
||||
alt 기존 회원 (비로그인 상태)
|
||||
초대받은사용자->>Portal: 로그인
|
||||
else 비회원이 일반 회원가입 후
|
||||
초대받은사용자->>Portal: 회원가입 (ROLE_USER)
|
||||
초대받은사용자->>Portal: 로그인
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 3. 로그인 성공 처리 (AuthenticationSuccessHandler)
|
||||
%% ========================================
|
||||
Portal->>AuthHandler: onAuthenticationSuccess()
|
||||
AuthHandler->>AuthHandler: 이메일 인증, 휴면계정,<br/>비밀번호 만료 체크
|
||||
|
||||
AuthHandler->>UserInvitationRepo: findFirstByInvitationEmailAndStatus<br/>(loginId, PENDING)
|
||||
|
||||
alt PENDING 초대 없음
|
||||
AuthHandler->>Portal: redirect to /
|
||||
Portal-->>초대받은사용자: 메인 페이지 (팝업 없음)
|
||||
else PENDING 초대 있음 & 만료 안됨
|
||||
AuthHandler->>PortalOrgRepo: findById(invitation.orgId)
|
||||
PortalOrgRepo-->>AuthHandler: PortalOrg (기관명)
|
||||
AuthHandler->>AuthHandler: session.setAttribute<br/>("pendingInvitation", true)<br/>("pendingInvitationToken", token)<br/>("pendingInvitationOrgName", orgName)
|
||||
AuthHandler->>Portal: redirect to /
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 4. 메인 페이지 팝업 표시
|
||||
%% ========================================
|
||||
Portal->>IndexController: GET /
|
||||
IndexController->>MainPage: render index.html
|
||||
|
||||
MainPage->>MainPage: 세션에서 pendingInvitation 확인
|
||||
|
||||
alt pendingInvitation == true
|
||||
MainPage-->>초대받은사용자: 팝업 표시<br/>"[기관명]에서 초대가 도착했습니다.<br/>초대를 확인하시겠습니까?"
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 5. 사용자 팝업 선택
|
||||
%% ========================================
|
||||
alt 팝업에서 "확인" 클릭
|
||||
초대받은사용자->>Portal: /signup/decision?invitation=TOKEN
|
||||
Portal->>UserRegisterController: showDecisionPage(invitationToken)
|
||||
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
|
||||
UserRegisterController-->>Portal: 약관 동의 페이지
|
||||
Portal-->>초대받은사용자: 초대 수락/거절 화면
|
||||
else 팝업에서 "취소" 클릭
|
||||
Note over 초대받은사용자,MainPage: 메인 페이지 유지<br/>다음 로그인 시 팝업 재표시
|
||||
end
|
||||
|
||||
%% ========================================
|
||||
%% 6. 초대 수락/거절 처리
|
||||
%% ========================================
|
||||
초대받은사용자->>Portal: 수락 or 거절 선택
|
||||
Portal->>UserRegisterController: processInvitation(action)
|
||||
UserRegisterController->>UserRegisterFacade: processInvitation(action, invitation)
|
||||
|
||||
alt 수락
|
||||
UserRegisterFacade->>PortalUserRepo: findByEmailAddr()
|
||||
UserRegisterFacade->>PortalUserRepo: updateUserToCorpUser()<br/>(PortalOrg 변경<br/>ROLE_CORP_USER 전환)
|
||||
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: COMPLETED<br/>completeDate: now()
|
||||
UserRegisterFacade-->>Portal: 수락 성공
|
||||
Portal-->>초대받은사용자: 성공 메시지<br/>로그아웃 후 재로그인 안내
|
||||
Note over 초대받은사용자: 다음 로그인 시<br/>PENDING 초대 없음 → 팝업 안 뜸
|
||||
else 거절
|
||||
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: REJECTED<br/>completeDate: now()
|
||||
UserRegisterFacade-->>Portal: 거절 처리 완료
|
||||
Portal-->>초대받은사용자: 거절 완료 메시지
|
||||
Note over 초대받은사용자: 다음 로그인 시<br/>REJECTED 상태 → 팝업 안 뜸
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
stateDiagram-v2
|
||||
[*] --> 초대생성: 법인관리자가 초대 시작
|
||||
|
||||
초대생성 --> PENDING: 토큰 생성 및 저장
|
||||
|
||||
PENDING --> 이메일발송: 초대 이메일 발송
|
||||
|
||||
state 로그인후팝업 {
|
||||
이메일발송 --> 로그인: 사용자 로그인
|
||||
로그인 --> 초대확인: AuthSuccessHandler
|
||||
초대확인 --> 세션저장: 세션에 초대 정보 저장
|
||||
세션저장 --> 팝업표시: 메인 페이지 팝업
|
||||
팝업표시 --> 팝업확인: 확인 클릭
|
||||
팝업표시 --> 팝업취소: 취소 클릭
|
||||
팝업취소 --> 로그인: 다음 로그인 시 재표시
|
||||
}
|
||||
|
||||
팝업확인 --> 초대수락: 수락 페이지 이동
|
||||
|
||||
초대수락 --> COMPLETED: 수락
|
||||
초대수락 --> REJECTED: 거절
|
||||
|
||||
COMPLETED --> [*]: 초대 완료
|
||||
REJECTED --> [*]: 초대 거절
|
||||
PENDING --> CANCELED: 관리자 취소
|
||||
PENDING --> EXPIRED: 만료일 경과
|
||||
CANCELED --> [*]: 초대 취소
|
||||
EXPIRED --> [*]: 초대 만료
|
||||
|
||||
note right of PENDING
|
||||
PENDING 상태일 때만
|
||||
로그인 시 팝업 표시
|
||||
end note
|
||||
|
||||
note right of REJECTED
|
||||
REJECTED 상태가 되면
|
||||
더 이상 팝업 표시 안 함
|
||||
end note
|
||||
|
||||
note right of COMPLETED
|
||||
COMPLETED 상태가 되면
|
||||
더 이상 팝업 표시 안 함
|
||||
ROLE_CORP_USER로 전환됨
|
||||
end note
|
||||
|
Before Width: | Height: | Size: 6.7 MiB |
|
Before Width: | Height: | Size: 574 KiB |
@@ -1,33 +1,30 @@
|
||||
{
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "eapim-portal",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.57.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.69.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
"node-addon-api": "^7.0.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
@@ -37,25 +34,25 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
"@parcel/watcher-android-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.6",
|
||||
"@parcel/watcher-darwin-x64": "2.5.6",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.6",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.6",
|
||||
"@parcel/watcher-win32-arm64": "2.5.6",
|
||||
"@parcel/watcher-win32-ia32": "2.5.6",
|
||||
"@parcel/watcher-win32-x64": "2.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -74,9 +71,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -95,9 +92,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -116,9 +113,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -137,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -158,9 +155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -179,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -200,9 +197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -221,9 +218,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
|
||||
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -242,9 +239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
|
||||
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -263,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
|
||||
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -284,9 +281,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
|
||||
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -305,9 +302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
|
||||
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -325,82 +322,37 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -429,32 +381,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
@@ -464,57 +390,27 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
@@ -522,21 +418,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"version": "1.100.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
|
||||
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
@@ -551,284 +447,6 @@
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readdirp": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.57.0"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,5 @@
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.57.0"
|
||||
}
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 메뉴 관리 개발 가이드
|
||||
|
||||
포탈 GNB/마이페이지 메뉴는 `menu.yml` → DB(PTL_MENU_*) → 캐시 → 템플릿 렌더 구조로 동작하며,
|
||||
노출/배치 관리는 eapim-admin **포탈메뉴관리**(파트너포탈 > 포탈관리 > 메뉴 관리)에서 수행한다.
|
||||
|
||||
## 구성 요소
|
||||
|
||||
| 구성 | 위치 | 역할 |
|
||||
|---|---|---|
|
||||
| `menu.yml` | `src/main/resources/menu.yml` | 기본 메뉴 정의 (id/노출명/path/권한/기본 배치) |
|
||||
| `roles.yml` | `src/main/resources/roles.yml` | 역할 정의 (`portal.portal_security` 이동분) |
|
||||
| 엔티티/공유 서비스 | `elink-portal-common` `com.eactive.apim.portal.menu.*` | PTL_MENU_ITEM·PTL_MENU_PLACEMENT·PTL_ROLE(+AUTHORITY), `PortalMenuDataService` |
|
||||
| 시더 | `djb/menu/MenuSeeder.java` | 부팅 시 yml→DB 적재 (ApplicationReadyEvent) |
|
||||
| 캐시 | `djb/menu/MenuService.java` | role 비의존 트리 스냅샷, TTL 1시간(PTL_PROPERTY) |
|
||||
| 렌더 | `djb/menu/MenuModelAdvice.java` → 모델 `menuView` | 요청별 노출(EXPOSE_ROLES) 필터 |
|
||||
| 접근 제어 | `djb/menu/MenuAccessInterceptor.java` | ACCESS_ROLES 서버측 집행 (경로 정확 일치) |
|
||||
| 내부 API | `djb/menu/MenuInternalController.java` | `POST /internal/menu/reload` (admin 캐시 리로드 수신) |
|
||||
|
||||
메뉴를 소비하는 템플릿: `fragment/djbank/header_container.html`(데스크톱 nav·마이페이지 드롭다운·모바일 drawer),
|
||||
`fragment/djbank/service_sidebar.html`. 모두 `${menuView}` 를 반복 렌더하므로 **메뉴 추가 시 템플릿 수정 불필요**.
|
||||
|
||||
## menu.yml 스키마
|
||||
|
||||
```yaml
|
||||
portal-menu:
|
||||
items:
|
||||
- id: support # kebab-case 필수 (^[a-z0-9-]+$). 변경 금지(변경=신규 항목)
|
||||
name: "고객지원"
|
||||
group: true # 상위 그룹. path 생략 시 클릭 없음(자식 있어야 노출)
|
||||
section: GNB # GNB(기본) | MYPAGE. 자식은 부모 섹션 상속
|
||||
expose-roles: [] # 생략=전체(익명 포함), AUTHENTICATED=로그인자, 그 외 역할코드 any-of
|
||||
children:
|
||||
- { id: support-faq, name: "FAQ", path: /faq_list }
|
||||
- { id: my-page-webhook, name: "Webhook 관리", path: /webhook, icon: fa-bell,
|
||||
expose-roles: [ROLE_WEBHOOK], access-roles: [ROLE_WEBHOOK] }
|
||||
```
|
||||
|
||||
- `expose-roles` = 메뉴 **노출** 조건, `access-roles` = URL **접근** 조건(인터셉터 차단, redirect).
|
||||
- `icon` 은 마이페이지 드롭다운 전용(FontAwesome 클래스).
|
||||
- 정렬은 yml 나열 순서(기본 배치 sort = index×10).
|
||||
|
||||
## 시딩 규칙 (MenuSeeder)
|
||||
|
||||
1. **항목**: id 기준 upsert. yml 값이 바뀌면 DFLT_*(기본값 스냅샷)를 갱신하고,
|
||||
**관리자가 수정하지 않은 필드(현재값==구 기본값)만** 새 기본값을 따라간다.
|
||||
구조 필드(`group`/`section`/`icon`/`new-window`)는 항상 yml 이 이긴다.
|
||||
2. **배치**: `PTL_MENU_PLACEMENT` 가 **비어있을 때만** 기본 배치로 최초 시딩.
|
||||
이후 배치는 admin 이 소유한다 — 재배포/재기동에도 보존됨.
|
||||
3. yml 에서 항목을 제거해도 DB 는 삭제하지 않고 경고 로그만 남긴다(수동 정리).
|
||||
4. 부팅 시딩 주체는 인증 사용자가 없으므로 `CREATED_BY=SYSTEM`.
|
||||
|
||||
## 캐시와 리로드
|
||||
|
||||
- 스냅샷 TTL: PTL_PROPERTY `Portal / menu.cache.ttl-seconds` (기본 3600초).
|
||||
- 즉시 반영: `curl -X POST http://127.0.0.1:39130/internal/menu/reload`
|
||||
(admin 포탈메뉴관리의 [캐시 Reload] 버튼이 동일 호출 수행).
|
||||
- 내부 API 가드: `Portal / menu.internal.allow-ips` 허용 IP 목록(기본 loopback)
|
||||
+ X-Forwarded-For 동반 요청 거부. CSRF 면제(`/internal/menu/**`).
|
||||
- admin 측 호출 URL: `Portal / portal.internal.menu-reload-url`.
|
||||
|
||||
## 새 메뉴 추가 절차
|
||||
|
||||
**기본 메뉴(코드 배포와 함께)**
|
||||
1. 페이지/라우트 준비 (`portal.pages` 또는 `@GetMapping` — 기존 방식 그대로)
|
||||
2. `menu.yml` 에 항목 추가 (필요 시 breadcrumb 용 `page.home` 트리도 갱신 — 별도 체계 유지)
|
||||
3. 재기동 → 시딩 로그 확인 → 헤더/드로어 노출 확인
|
||||
4. 이미 운영 중인 DB 라면 배치는 자동 추가되지 않음(배치 시딩은 최초 1회) —
|
||||
admin 화면에서 미배치 → 원하는 위치로 드래그 후 저장
|
||||
|
||||
**운영자 임시 메뉴(외부 링크 등)**: admin 포탈메뉴관리 [메뉴 추가] → 미배치 생성 → 드래그 배치 → 저장 → 캐시 Reload.
|
||||
커스텀 항목은 미배치 시 삭제된다.
|
||||
|
||||
## 로컬 개발 주의
|
||||
|
||||
- `gradle bootRun` 으로 시딩까지 확인하려면 damo-manager 가 classpath 에 필요:
|
||||
`JAVA_TOOL_OPTIONS="-Xbootclasspath/a:<...>/apache-tomcat-9.0.115-djb/lib/damo-manager.jar"`
|
||||
(미지정 시 감사 컬럼 암호화 컨버터에서 NoClassDefFoundError).
|
||||
- 템플릿/메뉴 반영 확인은 서버 재시작 후 curl 로.
|
||||
- elink-portal-common 수정 후 Q클래스 duplicate 컴파일 오류 시 각 모듈 `build/generated` 삭제 후 재컴파일.
|
||||
|
||||
## 역할(roles.yml) 변경
|
||||
|
||||
- 로그인 권한 확장은 `PortalRolesProperties`(yml 바인딩)를 직접 사용 — DB 미러(PTL_ROLE*)는
|
||||
admin 권한 선택 체크박스 소스 전용.
|
||||
- 역할 추가 시 `roles.yml` 의 `authority-names` 에 한글 라벨을 함께 등록해야 admin 화면에 표기된다.
|
||||
@@ -0,0 +1,112 @@
|
||||
# DJB 커스텀 소스 납품본 수집
|
||||
|
||||
발주사 납품용으로, 기준 시점 이후 **신규로 추가된 커스텀 소스 파일**만 모아
|
||||
디렉터리 구조를 유지한 채 한곳에 복사하고 `MANIFEST.csv` / `SUMMARY.md` / zip 을 만든다.
|
||||
|
||||
## 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|---|---|
|
||||
| `export-custom.sh` | macOS / Linux 실행 스크립트 |
|
||||
| `export-custom.ps1` | Windows PowerShell 실행 스크립트 (실제 로직) |
|
||||
| `export-custom.bat` | Windows cmd 래퍼 — 인자를 `.ps1` 로 그대로 전달 |
|
||||
| `modules.conf` | 대상 모듈 / 소스 경로 / 수집 확장자 |
|
||||
| `export-filter.rules` | 포함·제외 규칙 (`.gitignore` 유사 문법) |
|
||||
| `filelist.txt` | 마지막으로 뽑은 수집 대상 목록 (검토용, 재생성 가능) |
|
||||
| `filelist-no-djb-marker.txt` | 그중 `djb` / `custom` 마커가 **없는** 파일 — 업그레이드 유입분 검토 대상 |
|
||||
|
||||
## 수집 방식
|
||||
|
||||
1. 모듈별로 **컷오프 날짜 직전의 마지막 커밋**을 base 로 잡는다.
|
||||
`git rev-list -1 --before=<CUTOFF> HEAD`
|
||||
컷오프 이전 커밋이 아예 없으면 저장소 전체를 신규로 본다.
|
||||
2. `git diff --name-only --diff-filter=A -M <base> HEAD -- <소스경로>` 로
|
||||
**신규 추가(A)** 파일만 뽑는다. `-M` 이라 단순 rename 은 신규로 잡히지 않는다.
|
||||
3. `modules.conf` 의 확장자 allowlist 로 거른다. (확장자 없는 파일은 제외)
|
||||
4. `export-filter.rules` 로 포함·제외를 최종 결정한다.
|
||||
5. 남은 파일을 `<out>/src/<모듈>/<원래 경로>` 로 복사한다.
|
||||
|
||||
## 사용법
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
./export-custom.sh --dry-run > filelist.txt # 목록만 뽑기 (로그는 stderr)
|
||||
./export-custom.sh --show-excluded # 규칙에 걸려 빠진 목록 + 걸린 규칙
|
||||
./export-custom.sh # 실제 수집
|
||||
./export-custom.sh --zip # 수집 + zip
|
||||
./export-custom.sh -c 2026-05-01 -m eapim-portal
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
.\export-custom.ps1 -DryRun > filelist.txt
|
||||
.\export-custom.ps1 -ShowExcluded
|
||||
.\export-custom.ps1 -Zip
|
||||
.\export-custom.ps1 -Cutoff 2026-05-01 -Module eapim-portal,eapim-admin
|
||||
|
||||
# cmd 에서
|
||||
export-custom.bat -DryRun > filelist.txt
|
||||
```
|
||||
|
||||
### 주요 옵션
|
||||
|
||||
| sh | ps1 | 설명 |
|
||||
|---|---|---|
|
||||
| `-c, --cutoff` | `-Cutoff` | 기준 날짜 (기본 `2026-05-01`) |
|
||||
| `-r, --root` | `-Root` | 저장소 루트. 미지정 시 스크립트 위치 기준 자동 탐지 |
|
||||
| `-o, --out` | `-Out` | 출력 디렉터리 (기본 `<root>/build/djb-custom-export`) |
|
||||
| `-m, --module` | `-Module` | 특정 모듈만 |
|
||||
| `--zip` | `-Zip` | zip 생성 |
|
||||
| `--include-untracked` | `-IncludeUntracked` | 미커밋 신규 파일도 포함 |
|
||||
| `--dry-run` | `-DryRun` | 복사 없이 목록만 |
|
||||
| `--show-excluded` | `-ShowExcluded` | 규칙에 걸려 빠진 목록 |
|
||||
|
||||
## `export-filter.rules` 문법
|
||||
|
||||
`.gitignore` 와 비슷하되 더 단순하다.
|
||||
|
||||
- 한 줄에 패턴 하나. `#` 주석, 빈 줄 무시
|
||||
- 기본은 **모두 포함**. 패턴에 걸리면 제외
|
||||
- `!` 로 시작하면 예외(다시 포함)
|
||||
- **뒤에 오는 규칙이 앞 규칙을 덮는다** — 순서가 중요하다
|
||||
- 패턴에 `/` 가 있으면 `<모듈>/<경로>` 전체와 매칭, 없으면 파일명만 매칭
|
||||
- `/` 로 끝나면 그 디렉터리 하위 전체
|
||||
- `*` 는 `/` 도 포함해 매칭한다 (단순 glob, `**` 없음)
|
||||
|
||||
매칭 대상 경로 예:
|
||||
|
||||
```
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
|
||||
```
|
||||
|
||||
규칙 파일은 세 구역으로 나뉜다.
|
||||
|
||||
1. **업그레이드 유입분 제외** — `filelist-no-djb-marker.txt` 를 검토하며 채운다
|
||||
2. **`!*djb*` 예외** — djb 마커가 붙은 파일은 커스텀이므로 되살린다
|
||||
3. **무조건 제외** — 빌드 산출물·서드파티. 2)보다 뒤에 있어 최종 승리한다
|
||||
|
||||
## 신규지만 커스텀이 아닌 파일 걸러내기
|
||||
|
||||
신규 파일이라도 제품 업그레이드로 딸려 온 것일 수 있다. 판별 순서:
|
||||
|
||||
```bash
|
||||
# 1) 전체 목록
|
||||
./export-custom.sh --dry-run > filelist.txt
|
||||
|
||||
# 2) djb / custom 마커가 없는 것 = 검토 대상
|
||||
grep -vi djb filelist.txt | grep -v '/custom/' > filelist-no-djb-marker.txt
|
||||
|
||||
# 3) 검토 후 업그레이드 유입분을 export-filter.rules 의 1) 구역에 추가
|
||||
|
||||
# 4) 반영 확인
|
||||
./export-custom.sh --show-excluded
|
||||
```
|
||||
|
||||
## 주의
|
||||
|
||||
- 컷오프 이전 커밋이 없으면 base 가 **빈 트리**가 되어 저장소 전체가 신규로 잡힌다.
|
||||
`SUMMARY.md` 의 base 컬럼에 `(컷오프 이전 커밋 없음 …)` 으로 표시된다.
|
||||
- 모듈에 컷오프 직전 커밋이 드문드문 있으면 base 가 컷오프보다 훨씬 이전으로 잡힐 수 있다.
|
||||
실행 로그의 `base:` 날짜를 항상 확인할 것.
|
||||
- 추가된 뒤 삭제·이동된 파일은 현재 트리에 없으므로 건너뛰고, 건수를 `SUMMARY.md` 에 남긴다.
|
||||
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM DJB 커스텀 소스 납품본 수집 (Windows cmd 래퍼)
|
||||
REM
|
||||
REM 실제 로직은 export-custom.ps1 에 있다. 이 배치는 PowerShell 을 띄우고
|
||||
REM 전달받은 인자를 그대로 넘기기만 한다.
|
||||
REM
|
||||
REM 사용 예:
|
||||
REM export-custom.bat
|
||||
REM export-custom.bat -Cutoff 2026-05-01 -Zip
|
||||
REM export-custom.bat -DryRun > filelist.txt
|
||||
REM export-custom.bat -ShowExcluded
|
||||
REM export-custom.bat -Module eapim-portal,eapim-admin
|
||||
REM ---------------------------------------------------------------------------
|
||||
setlocal
|
||||
|
||||
REM 한글 경로/출력이 깨지지 않도록 코드페이지를 UTF-8 로 전환
|
||||
chcp 65001 >nul
|
||||
|
||||
where powershell >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo [오류] PowerShell 을 찾을 수 없습니다. 1>&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0export-custom.ps1" %*
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,278 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
DJB 커스텀 소스 납품본 수집 스크립트 (Windows / PowerShell)
|
||||
|
||||
.DESCRIPTION
|
||||
기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
|
||||
모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
|
||||
MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
|
||||
|
||||
.EXAMPLE
|
||||
.\export-custom.ps1
|
||||
.\export-custom.ps1 -Cutoff 2026-05-01 -Zip
|
||||
.\export-custom.ps1 -DryRun
|
||||
.\export-custom.ps1 -Module eapim-portal,eapim-admin
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Cutoff = '2026-05-01',
|
||||
[string] $Root,
|
||||
[string] $Out,
|
||||
[string[]] $Module,
|
||||
[switch] $Zip,
|
||||
[switch] $IncludeUntracked,
|
||||
[switch] $DryRun,
|
||||
[switch] $ShowExcluded
|
||||
)
|
||||
|
||||
if ($ShowExcluded) { $DryRun = $true }
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# git 출력의 한글이 깨지지 않도록 콘솔 인코딩을 UTF-8로 고정한다.
|
||||
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
|
||||
$OutputEncoding = [Text.Encoding]::UTF8
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ConfFile = Join-Path $ScriptDir 'modules.conf'
|
||||
$RulesFile = Join-Path $ScriptDir 'export-filter.rules'
|
||||
|
||||
# 저장소 루트: 스크립트는 <root>\eapim-portal\script\djb-custom-export\ 에 있다.
|
||||
if (-not $Root) { $Root = (Resolve-Path (Join-Path $ScriptDir '..\..\..')).Path }
|
||||
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
||||
throw "저장소 루트를 찾을 수 없다: $Root"
|
||||
}
|
||||
if (-not $Out) { $Out = Join-Path $Root 'build\djb-custom-export' }
|
||||
if (-not (Test-Path -LiteralPath $ConfFile)) { throw "설정 파일 없음: $ConfFile" }
|
||||
|
||||
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
|
||||
$EmptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
||||
|
||||
# ---- 설정 로드 ------------------------------------------------------------
|
||||
$Modules = @()
|
||||
$AllowExt = @()
|
||||
|
||||
foreach ($line in (Get-Content -LiteralPath $ConfFile -Encoding UTF8)) {
|
||||
$t = $line.Trim()
|
||||
if ($t -eq '' -or $t.StartsWith('#')) { continue }
|
||||
$idx = $t.IndexOf('|')
|
||||
if ($idx -lt 0) { continue }
|
||||
$key = $t.Substring(0, $idx)
|
||||
$rest = $t.Substring($idx + 1)
|
||||
|
||||
switch ($key) {
|
||||
'MODULE' {
|
||||
$i2 = $rest.IndexOf('|')
|
||||
if ($i2 -lt 0) { continue }
|
||||
$Modules += [pscustomobject]@{
|
||||
Name = $rest.Substring(0, $i2)
|
||||
Paths = $rest.Substring($i2 + 1).Split(' ') | Where-Object { $_ -ne '' }
|
||||
}
|
||||
}
|
||||
'EXT' { $AllowExt = $rest.Split(',') | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ -ne '' } }
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 포함/제외 규칙 로드 (.gitignore 유사) --------------------------------
|
||||
$Rules = @()
|
||||
if (Test-Path -LiteralPath $RulesFile) {
|
||||
foreach ($line in (Get-Content -LiteralPath $RulesFile -Encoding UTF8)) {
|
||||
$t = $line.Trim()
|
||||
if ($t -eq '' -or $t.StartsWith('#')) { continue }
|
||||
$Rules += $t
|
||||
}
|
||||
}
|
||||
|
||||
# 마지막으로 매칭된 규칙이 이긴다. 기본 포함, '!' 는 예외(다시 포함).
|
||||
# 반환: @{ Keep = $true/$false; Rule = '<제외시킨 규칙>' }
|
||||
function Test-RuleKeep([string]$RelPath) {
|
||||
$leaf = Split-Path $RelPath -Leaf
|
||||
$keep = $true
|
||||
$matched = ''
|
||||
foreach ($raw in $Rules) {
|
||||
$rule = $raw
|
||||
$neg = $false
|
||||
if ($rule.StartsWith('!')) { $neg = $true; $rule = $rule.Substring(1) }
|
||||
if ($rule.EndsWith('/')) { $rule = $rule + '*' } # 디렉터리 규칙
|
||||
$target = if ($rule.Contains('/')) { $RelPath } else { $leaf }
|
||||
|
||||
if ($target -like $rule) {
|
||||
if ($neg) { $keep = $true; $matched = '' }
|
||||
else { $keep = $false; $matched = $rule }
|
||||
}
|
||||
}
|
||||
return @{ Keep = $keep; Rule = $matched }
|
||||
}
|
||||
|
||||
function Test-AllowedExt([string]$RelPath) {
|
||||
$leaf = Split-Path $RelPath -Leaf
|
||||
if ($leaf -notmatch '\.') { return $false } # 확장자 없는 파일 제외
|
||||
$ext = $leaf.Substring($leaf.LastIndexOf('.') + 1).ToLower()
|
||||
return $AllowExt -contains $ext
|
||||
}
|
||||
|
||||
function Invoke-Git([string]$Dir, [string[]]$GitArgs) {
|
||||
$all = @('-C', $Dir, '-c', 'core.quotepath=false') + $GitArgs
|
||||
$res = & git @all 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { return @() }
|
||||
return @($res)
|
||||
}
|
||||
|
||||
# ---- 준비 -----------------------------------------------------------------
|
||||
$Manifest = Join-Path $Out 'MANIFEST.csv'
|
||||
$Summary = Join-Path $Out 'SUMMARY.md'
|
||||
|
||||
if (-not $DryRun) {
|
||||
if (Test-Path -LiteralPath $Out) { Remove-Item -LiteralPath $Out -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path (Join-Path $Out 'src') -Force | Out-Null
|
||||
# Excel 한글용 UTF-8 BOM
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[IO.File]::WriteAllText($Manifest, "module,path,ext,added_commit,added_date,author`r`n", $utf8Bom)
|
||||
}
|
||||
|
||||
Write-Host "저장소 루트 : $Root"
|
||||
Write-Host "기준 날짜 : $Cutoff (이 날짜 직전 마지막 커밋이 base)"
|
||||
Write-Host "출력 경로 : $Out"
|
||||
if ($DryRun) { Write-Host '모드 : DRY-RUN (복사 안 함)' }
|
||||
Write-Host ''
|
||||
|
||||
$Rows = New-Object System.Collections.Generic.List[string]
|
||||
$SummaryRs = New-Object System.Collections.Generic.List[object]
|
||||
$Total = 0
|
||||
$Missing = 0
|
||||
$Excluded = 0
|
||||
|
||||
# ---- 모듈 순회 ------------------------------------------------------------
|
||||
foreach ($m in $Modules) {
|
||||
if ($Module -and ($Module -notcontains $m.Name)) { continue }
|
||||
|
||||
$modDir = Join-Path $Root $m.Name
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $modDir '.git'))) {
|
||||
Write-Host "[건너뜀] $($m.Name) — git 저장소 아님 ($modDir)"
|
||||
continue
|
||||
}
|
||||
|
||||
# base 커밋 결정
|
||||
$base = (Invoke-Git $modDir @('rev-list', '-1', "--before=$Cutoff", 'HEAD') | Select-Object -First 1)
|
||||
if ([string]::IsNullOrWhiteSpace($base)) {
|
||||
$base = $EmptyTree
|
||||
$baseDesc = '(컷오프 이전 커밋 없음 → 전체를 신규로 간주)'
|
||||
} else {
|
||||
$d = (Invoke-Git $modDir @('log', '-1', '--format=%h %ad %s', '--date=short', $base) | Select-Object -First 1)
|
||||
$baseDesc = if ($d.Length -gt 80) { $d.Substring(0, 80) } else { $d }
|
||||
}
|
||||
|
||||
# 실제 존재하는 소스 경로만 pathspec 으로 사용
|
||||
$spec = @()
|
||||
foreach ($p in $m.Paths) {
|
||||
if (Test-Path -LiteralPath (Join-Path $modDir ($p -replace '/', '\')) -PathType Container) { $spec += $p }
|
||||
}
|
||||
if ($spec.Count -eq 0) {
|
||||
Write-Host "[건너뜀] $($m.Name) — 설정된 소스 경로가 존재하지 않음"
|
||||
continue
|
||||
}
|
||||
|
||||
# 신규(Added) 파일만. -M 으로 rename 은 신규에서 제외한다.
|
||||
$files = @(Invoke-Git $modDir (@('diff', '--name-only', '--diff-filter=A', '-M', $base, 'HEAD', '--') + $spec))
|
||||
|
||||
if ($IncludeUntracked) {
|
||||
$files += @(Invoke-Git $modDir (@('ls-files', '--others', '--exclude-standard', '--') + $spec))
|
||||
}
|
||||
|
||||
$files = $files | Where-Object { $_ -ne '' } | Sort-Object -Unique
|
||||
|
||||
$count = 0
|
||||
$miss = 0
|
||||
foreach ($f in $files) {
|
||||
if (-not (Test-AllowedExt $f)) { continue }
|
||||
|
||||
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
|
||||
$rk = Test-RuleKeep "$($m.Name)/$f"
|
||||
if (-not $rk.Keep) {
|
||||
$Excluded++
|
||||
if ($ShowExcluded) { Write-Output "$($m.Name)/$f`t# $($rk.Rule)" }
|
||||
continue
|
||||
}
|
||||
if ($ShowExcluded) { continue }
|
||||
|
||||
$src = Join-Path $modDir ($f -replace '/', '\')
|
||||
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
|
||||
Write-Warning " [없음] $($m.Name)/$f"
|
||||
$miss++
|
||||
continue
|
||||
}
|
||||
|
||||
$count++
|
||||
# 목록은 Write-Output(성공 스트림) 으로 — 리다이렉트하면 목록만 파일로 떨어진다.
|
||||
if ($DryRun) { Write-Output "$($m.Name)/$f"; continue }
|
||||
|
||||
$dst = Join-Path (Join-Path $Out 'src') (Join-Path $m.Name ($f -replace '/', '\'))
|
||||
$dstDir = Split-Path -Parent $dst
|
||||
if (-not (Test-Path -LiteralPath $dstDir)) { New-Item -ItemType Directory -Path $dstDir -Force | Out-Null }
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
|
||||
$meta = (Invoke-Git $modDir @('log', '-1', '--diff-filter=A', '--format=%h|%ad|%an', '--date=short', '--', $f) | Select-Object -First 1)
|
||||
$cHash = ''; $cDate = ''; $cAuth = ''
|
||||
if ($meta) {
|
||||
$parts = $meta.Split('|')
|
||||
if ($parts.Count -ge 3) { $cHash = $parts[0]; $cDate = $parts[1]; $cAuth = $parts[2] }
|
||||
}
|
||||
$leaf = Split-Path $f -Leaf
|
||||
$ext = if ($leaf -match '\.') { $leaf.Substring($leaf.LastIndexOf('.') + 1) } else { '' }
|
||||
|
||||
$esc = { param($s) '"' + ($s -replace '"', '""') + '"' }
|
||||
$Rows.Add((@(
|
||||
(& $esc $m.Name), (& $esc $f), (& $esc $ext),
|
||||
(& $esc $cHash), (& $esc $cDate), (& $esc $cAuth)
|
||||
) -join ','))
|
||||
}
|
||||
|
||||
$Total += $count
|
||||
$Missing += $miss
|
||||
$SummaryRs.Add([pscustomobject]@{ Module = $m.Name; Count = $count; Base = $baseDesc })
|
||||
Write-Host ("[수집] {0,-22} {1,5} 개 base: {2}" -f $m.Name, $count, $baseDesc)
|
||||
}
|
||||
|
||||
# ---- 출력 ------------------------------------------------------------------
|
||||
if (-not $DryRun) {
|
||||
if ($Rows.Count -gt 0) {
|
||||
[IO.File]::AppendAllText($Manifest, (($Rows -join "`r`n") + "`r`n"), (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
[void]$sb.AppendLine('# DJB 커스텀 소스 납품본')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine("- 생성 일시: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
|
||||
[void]$sb.AppendLine("- 기준 날짜: ``$Cutoff`` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)")
|
||||
[void]$sb.AppendLine('- 수집 규칙: `git diff --diff-filter=A -M <base> HEAD` / 확장자 allowlist / `export-filter.rules` 필터')
|
||||
[void]$sb.AppendLine("- 대상 확장자: ``$($AllowExt -join ',')``")
|
||||
[void]$sb.AppendLine("- 규칙 파일: ``$(Split-Path $RulesFile -Leaf)`` ($($Rules.Count) 개 규칙, 제외 $Excluded 건)")
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('## 모듈별 수집 결과')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('| 모듈 | 파일 수 | base 커밋 |')
|
||||
[void]$sb.AppendLine('|---|---:|---|')
|
||||
foreach ($r in $SummaryRs) { [void]$sb.AppendLine("| $($r.Module) | $($r.Count) | $($r.Base) |") }
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine("**합계: $Total 개**")
|
||||
if ($Missing -gt 0) { [void]$sb.AppendLine("> 추가 후 삭제/이동되어 현재 트리에 없는 파일 $Missing 개는 제외됨.") }
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('## 파일 목록')
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('`MANIFEST.csv` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자).')
|
||||
|
||||
[IO.File]::WriteAllText($Summary, $sb.ToString(), (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "MANIFEST : $Manifest"
|
||||
Write-Host "SUMMARY : $Summary"
|
||||
|
||||
if ($Zip) {
|
||||
$zipName = "djb-custom-export-$(Get-Date -Format 'yyyyMMdd').zip"
|
||||
$zipPath = Join-Path (Split-Path -Parent $Out) $zipName
|
||||
if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
|
||||
Compress-Archive -Path (Join-Path $Out '*') -DestinationPath $zipPath
|
||||
Write-Host "ZIP : $zipPath"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# DJB 커스텀 소스 납품본 수집 스크립트 (macOS / Linux)
|
||||
#
|
||||
# 기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
|
||||
# 모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
|
||||
# MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
|
||||
#
|
||||
# 사용법:
|
||||
# ./export-custom.sh # 기본값으로 실행
|
||||
# ./export-custom.sh -c 2026-05-01 --zip
|
||||
# ./export-custom.sh --dry-run
|
||||
# ./export-custom.sh -m eapim-portal -m eapim-admin
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONF_FILE="$SCRIPT_DIR/modules.conf"
|
||||
RULES_FILE="$SCRIPT_DIR/export-filter.rules"
|
||||
|
||||
# ---- 기본값 ---------------------------------------------------------------
|
||||
CUTOFF="2026-05-01"
|
||||
REPO_ROOT=""
|
||||
OUT_DIR=""
|
||||
MAKE_ZIP=0
|
||||
DRY_RUN=0
|
||||
INCLUDE_UNTRACKED=0
|
||||
ONLY_MODULES=""
|
||||
SHOW_EXCLUDED=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
DJB 커스텀 소스 납품본 수집
|
||||
|
||||
옵션:
|
||||
-c, --cutoff <YYYY-MM-DD> 기준 날짜. 이 날짜 직전 마지막 커밋을 base로 삼는다.
|
||||
(기본: 2026-05-01)
|
||||
-r, --root <DIR> 저장소 루트(djb-eapim). 미지정 시 스크립트 위치에서 자동 탐지.
|
||||
-o, --out <DIR> 출력 디렉터리. (기본: <root>/build/djb-custom-export)
|
||||
-m, --module <NAME> 특정 모듈만 수집. 여러 번 지정 가능.
|
||||
--zip 수집 후 zip 아카이브 생성.
|
||||
--include-untracked git에 아직 커밋되지 않은 신규 파일도 포함.
|
||||
--dry-run 복사하지 않고 대상 목록만 stdout 으로 출력(로그는 stderr).
|
||||
--show-excluded --dry-run 과 함께. 규칙에 걸려 제외된 목록을 대신 출력.
|
||||
-h, --help 도움말.
|
||||
|
||||
포함/제외 규칙은 export-filter.rules (.gitignore 유사 문법) 에서 관리한다.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-c|--cutoff) CUTOFF="$2"; shift 2 ;;
|
||||
-r|--root) REPO_ROOT="$2"; shift 2 ;;
|
||||
-o|--out) OUT_DIR="$2"; shift 2 ;;
|
||||
-m|--module) ONLY_MODULES="$ONLY_MODULES $2"; shift 2 ;;
|
||||
--zip) MAKE_ZIP=1; shift ;;
|
||||
--include-untracked) INCLUDE_UNTRACKED=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--show-excluded) SHOW_EXCLUDED=1; DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "알 수 없는 옵션: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- 저장소 루트 탐지 -----------------------------------------------------
|
||||
# 스크립트는 <root>/eapim-portal/script/djb-custom-export/ 에 있다.
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
fi
|
||||
if [ ! -d "$REPO_ROOT" ]; then
|
||||
echo "저장소 루트를 찾을 수 없다: $REPO_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$OUT_DIR" ] && OUT_DIR="$REPO_ROOT/build/djb-custom-export"
|
||||
|
||||
if [ ! -f "$CONF_FILE" ]; then
|
||||
echo "설정 파일 없음: $CONF_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 설정 로드 ------------------------------------------------------------
|
||||
ALLOW_EXT=""
|
||||
EXCLUDE_PAT=""
|
||||
MODULE_LINES=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
key="${line%%|*}"
|
||||
rest="${line#*|}"
|
||||
case "$key" in
|
||||
MODULE) MODULE_LINES="$MODULE_LINES
|
||||
$rest" ;;
|
||||
EXT) ALLOW_EXT="$rest" ;;
|
||||
EXCLUDE) EXCLUDE_PAT="$rest" ;;
|
||||
esac
|
||||
done < "$CONF_FILE"
|
||||
|
||||
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
|
||||
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
is_allowed_ext() {
|
||||
_f="$1"
|
||||
_base="${_f##*/}"
|
||||
case "$_base" in
|
||||
*.*) _ext="${_base##*.}" ;;
|
||||
*) return 1 ;; # 확장자 없는 파일 제외
|
||||
esac
|
||||
# 소문자 변환 (bash 3.2 호환)
|
||||
_ext="$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')"
|
||||
case ",$ALLOW_EXT," in
|
||||
*",$_ext,"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- 포함/제외 규칙 (.gitignore 유사) --------------------------------------
|
||||
RULES=()
|
||||
if [ -f "$RULES_FILE" ]; then
|
||||
while IFS= read -r r || [ -n "$r" ]; do
|
||||
# 앞뒤 공백 제거
|
||||
r="${r#"${r%%[![:space:]]*}"}"
|
||||
r="${r%"${r##*[![:space:]]}"}"
|
||||
case "$r" in
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
RULES[${#RULES[@]}]="$r"
|
||||
done < "$RULES_FILE"
|
||||
fi
|
||||
|
||||
# 경로 하나에 규칙을 순서대로 적용한다. 마지막으로 매칭된 규칙이 이긴다.
|
||||
# 기본은 포함(0), '!' 규칙은 다시 포함, 일반 규칙은 제외(1).
|
||||
# 반환: 0 = 포함, 1 = 제외. 제외시킨 규칙은 MATCHED_RULE 에 담긴다.
|
||||
MATCHED_RULE=""
|
||||
rule_keep() {
|
||||
_path="$1"
|
||||
_leaf="${_path##*/}"
|
||||
_keep=0
|
||||
MATCHED_RULE=""
|
||||
_i=0
|
||||
while [ $_i -lt ${#RULES[@]} ]; do
|
||||
_rule="${RULES[$_i]}"
|
||||
_i=$((_i + 1))
|
||||
|
||||
_neg=0
|
||||
case "$_rule" in
|
||||
'!'*) _neg=1; _rule="${_rule#!}" ;;
|
||||
esac
|
||||
# 디렉터리 규칙: 하위 전체
|
||||
case "$_rule" in
|
||||
*/) _rule="${_rule}*" ;;
|
||||
esac
|
||||
# '/' 가 있으면 전체 경로, 없으면 파일명과 매칭
|
||||
case "$_rule" in
|
||||
*/*) _target="$_path" ;;
|
||||
*) _target="$_leaf" ;;
|
||||
esac
|
||||
|
||||
# shellcheck disable=SC2254 # 의도적으로 unquoted — glob 패턴으로 쓴다
|
||||
case "$_target" in
|
||||
$_rule)
|
||||
if [ $_neg -eq 1 ]; then
|
||||
_keep=0; MATCHED_RULE=""
|
||||
else
|
||||
_keep=1; MATCHED_RULE="$_rule"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return $_keep
|
||||
}
|
||||
|
||||
csv_escape() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
|
||||
}
|
||||
|
||||
# ---- 준비 -----------------------------------------------------------------
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
rm -rf "$OUT_DIR"
|
||||
mkdir -p "$OUT_DIR/src"
|
||||
fi
|
||||
|
||||
MANIFEST="$OUT_DIR/MANIFEST.csv"
|
||||
SUMMARY="$OUT_DIR/SUMMARY.md"
|
||||
TMP_SUMMARY="$(mktemp)"
|
||||
trap 'rm -f "$TMP_SUMMARY"' EXIT
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
printf '\357\273\277' > "$MANIFEST" # Excel 한글용 UTF-8 BOM
|
||||
echo 'module,path,ext,added_commit,added_date,author' >> "$MANIFEST"
|
||||
fi
|
||||
|
||||
# dry-run(목록 뽑기) 모드에서는 파일 목록만 stdout으로 내보내고
|
||||
# 진행 로그는 전부 stderr로 보낸다. 그래야 파이프/리다이렉트로 목록을 바로 가공할 수 있다.
|
||||
if [ "$DRY_RUN" -eq 1 ]; then LOG=2; else LOG=1; fi
|
||||
|
||||
{
|
||||
echo "저장소 루트 : $REPO_ROOT"
|
||||
echo "기준 날짜 : $CUTOFF (이 날짜 직전 마지막 커밋이 base)"
|
||||
echo "출력 경로 : $OUT_DIR"
|
||||
[ "$DRY_RUN" -eq 1 ] && echo "모드 : DRY-RUN (목록만 출력, 복사 안 함)"
|
||||
echo
|
||||
} >&$LOG
|
||||
|
||||
TOTAL=0
|
||||
MISSING=0
|
||||
EXCLUDED=0
|
||||
|
||||
# ---- 모듈 순회 ------------------------------------------------------------
|
||||
# 파이프 대신 process substitution 을 써서 집계 변수가 서브셸에 갇히지 않게 한다.
|
||||
while IFS= read -r mline; do
|
||||
[ -z "$mline" ] && continue
|
||||
MOD="${mline%%|*}"
|
||||
PATHS="${mline#*|}"
|
||||
|
||||
if [ -n "$ONLY_MODULES" ]; then
|
||||
case " $ONLY_MODULES " in
|
||||
*" $MOD "*) : ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
MOD_DIR="$REPO_ROOT/$MOD"
|
||||
if [ ! -d "$MOD_DIR/.git" ]; then
|
||||
echo "[건너뜀] $MOD — git 저장소 아님 ($MOD_DIR)" >&$LOG
|
||||
continue
|
||||
fi
|
||||
|
||||
# base 커밋 결정
|
||||
BASE="$(git -C "$MOD_DIR" -c core.quotepath=false rev-list -1 --before="$CUTOFF" HEAD 2>/dev/null || true)"
|
||||
if [ -z "$BASE" ]; then
|
||||
BASE="$EMPTY_TREE"
|
||||
BASE_DESC="(컷오프 이전 커밋 없음 → 전체를 신규로 간주)"
|
||||
else
|
||||
BASE_DESC="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --format='%h %ad %s' --date=short "$BASE" | cut -c1-80)"
|
||||
fi
|
||||
|
||||
# 실제 존재하는 소스 경로만 pathspec으로 사용
|
||||
SPEC=""
|
||||
for p in $PATHS; do
|
||||
[ -d "$MOD_DIR/$p" ] && SPEC="$SPEC $p"
|
||||
done
|
||||
if [ -z "$SPEC" ]; then
|
||||
echo "[건너뜀] $MOD — 설정된 소스 경로가 존재하지 않음" >&$LOG
|
||||
continue
|
||||
fi
|
||||
|
||||
# 신규(Added) 파일만. -M 으로 rename은 신규에서 제외한다.
|
||||
FILES="$(git -C "$MOD_DIR" -c core.quotepath=false diff --name-only --diff-filter=A -M "$BASE" HEAD -- $SPEC 2>/dev/null || true)"
|
||||
|
||||
if [ "$INCLUDE_UNTRACKED" -eq 1 ]; then
|
||||
UNTRACKED="$(git -C "$MOD_DIR" -c core.quotepath=false ls-files --others --exclude-standard -- $SPEC 2>/dev/null || true)"
|
||||
FILES="$FILES
|
||||
$UNTRACKED"
|
||||
fi
|
||||
|
||||
COUNT=0
|
||||
MISS=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
is_allowed_ext "$f" || continue
|
||||
|
||||
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
|
||||
if ! rule_keep "$MOD/$f"; then
|
||||
EXCLUDED=$((EXCLUDED + 1))
|
||||
[ "$SHOW_EXCLUDED" -eq 1 ] && printf '%s\t# %s\n' "$MOD/$f" "$MATCHED_RULE"
|
||||
continue
|
||||
fi
|
||||
[ "$SHOW_EXCLUDED" -eq 1 ] && continue
|
||||
|
||||
SRC="$MOD_DIR/$f"
|
||||
if [ ! -f "$SRC" ]; then
|
||||
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
|
||||
echo " [없음] $MOD/$f" >&2
|
||||
MISS=$((MISS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
COUNT=$((COUNT + 1))
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "$MOD/$f" # 목록은 stdout으로 (가공하기 쉽게 접두 공백 없음)
|
||||
continue
|
||||
fi
|
||||
|
||||
DST="$OUT_DIR/src/$MOD/$f"
|
||||
mkdir -p "$(dirname "$DST")"
|
||||
cp -p "$SRC" "$DST"
|
||||
|
||||
META="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --diff-filter=A --format='%h|%ad|%an' --date=short -- "$f" 2>/dev/null || true)"
|
||||
C_HASH="${META%%|*}"; _r="${META#*|}"
|
||||
C_DATE="${_r%%|*}"
|
||||
C_AUTH="${_r#*|}"
|
||||
BASE_F="${f##*/}"
|
||||
case "$BASE_F" in *.*) EXT="${BASE_F##*.}" ;; *) EXT="" ;; esac
|
||||
|
||||
{
|
||||
csv_escape "$MOD"; printf ','
|
||||
csv_escape "$f"; printf ','
|
||||
csv_escape "$EXT"; printf ','
|
||||
csv_escape "$C_HASH"; printf ','
|
||||
csv_escape "$C_DATE"; printf ','
|
||||
csv_escape "$C_AUTH"; printf '\n'
|
||||
} >> "$MANIFEST"
|
||||
done < <(printf '%s\n' "$FILES" | sort -u)
|
||||
|
||||
TOTAL=$((TOTAL + COUNT))
|
||||
MISSING=$((MISSING + MISS))
|
||||
printf '%s|%s|%s\n' "$MOD" "$COUNT" "$BASE_DESC" >> "$TMP_SUMMARY"
|
||||
printf '[수집] %-22s %5s 개 base: %s\n' "$MOD" "$COUNT" "$BASE_DESC" >&$LOG
|
||||
done < <(printf '%s\n' "$MODULE_LINES")
|
||||
|
||||
# ---- 요약 ------------------------------------------------------------------
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
{
|
||||
echo "# DJB 커스텀 소스 납품본"
|
||||
echo
|
||||
echo "- 생성 일시: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "- 기준 날짜: \`$CUTOFF\` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)"
|
||||
echo "- 수집 규칙: \`git diff --diff-filter=A -M <base> HEAD\` / 확장자 allowlist / \`export-filter.rules\` 필터"
|
||||
echo "- 대상 확장자: \`$ALLOW_EXT\`"
|
||||
echo "- 규칙 파일: \`$(basename "$RULES_FILE")\` (${#RULES[@]} 개 규칙, 제외 ${EXCLUDED} 건)"
|
||||
echo
|
||||
echo "## 모듈별 수집 결과"
|
||||
echo
|
||||
echo "| 모듈 | 파일 수 | base 커밋 |"
|
||||
echo "|---|---:|---|"
|
||||
while IFS='|' read -r m n d; do
|
||||
[ -z "$m" ] && continue
|
||||
echo "| $m | $n | $d |"
|
||||
done < "$TMP_SUMMARY"
|
||||
echo
|
||||
echo "**합계: ${TOTAL} 개**"
|
||||
[ "$MISSING" -gt 0 ] && echo "> 추가 후 삭제/이동되어 현재 트리에 없는 파일 ${MISSING} 개는 제외됨."
|
||||
echo
|
||||
echo "## 파일 목록"
|
||||
echo
|
||||
echo "\`MANIFEST.csv\` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자)."
|
||||
} > "$SUMMARY"
|
||||
|
||||
echo
|
||||
echo "MANIFEST : $MANIFEST"
|
||||
echo "SUMMARY : $SUMMARY"
|
||||
|
||||
if [ "$MAKE_ZIP" -eq 1 ]; then
|
||||
ZIP_NAME="djb-custom-export-$(date '+%Y%m%d').zip"
|
||||
ZIP_PATH="$(dirname "$OUT_DIR")/$ZIP_NAME"
|
||||
rm -f "$ZIP_PATH"
|
||||
( cd "$OUT_DIR" && zip -qr "$ZIP_PATH" . )
|
||||
echo "ZIP : $ZIP_PATH"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,74 @@
|
||||
# DJB 커스텀 소스 납품 - 포함/제외 규칙
|
||||
#
|
||||
# .gitignore 와 비슷하게 동작한다.
|
||||
# - 한 줄에 패턴 하나. '#' 주석, 빈 줄 무시.
|
||||
# - 기본은 "모두 포함". 패턴에 걸리면 제외된다.
|
||||
# - '!' 로 시작하면 예외(다시 포함).
|
||||
# - 뒤에 오는 규칙이 앞 규칙을 덮는다 (마지막 매칭 승리). → 순서가 중요하다.
|
||||
# - 패턴에 '/' 가 있으면 <모듈>/<경로> 전체와 glob 매칭,
|
||||
# 없으면 파일명(basename)과만 매칭한다.
|
||||
# - '/' 로 끝나면 그 디렉터리 하위 전체를 뜻한다.
|
||||
# - '*' 는 '/' 도 포함해 매칭한다(단순 glob).
|
||||
#
|
||||
# 매칭 대상 경로 예:
|
||||
# eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
|
||||
# eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
|
||||
#
|
||||
# 확인: ./export-custom.sh --dry-run (남는 목록)
|
||||
# ./export-custom.sh --dry-run --show-excluded (규칙에 걸려 빠진 목록)
|
||||
|
||||
# ===========================================================================
|
||||
# 1) 제품 업그레이드로 유입된 신규 파일 — 커스텀 아니므로 제외
|
||||
# filelist-no-djb-marker.txt(djb/custom 마커 없는 신규 파일)를 검토하며 채운다.
|
||||
# ===========================================================================
|
||||
# 예시)
|
||||
# eapim-online/src/main/java/com/eactive/eai/agent/inflow/
|
||||
# eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/
|
||||
|
||||
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/
|
||||
eapim-portal/src/test/
|
||||
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2) 위 제외에 걸렸더라도 되살릴 예외
|
||||
# djb 마커가 붙은 파일은 어떤 경우에도 커스텀이다.
|
||||
# ※ 아래 3)의 빌드 산출물 제외보다 반드시 앞에 와야 한다.
|
||||
# ===========================================================================
|
||||
!*djb*
|
||||
!*DJB*
|
||||
!*Djb*
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3) 무조건 제외 (최종 승리) — 소스가 아니거나 서드파티
|
||||
# ===========================================================================
|
||||
*/build/
|
||||
*/out/
|
||||
*/bin/
|
||||
*/.gradle/
|
||||
*/.metadata/
|
||||
*/node_modules/
|
||||
*/generated/
|
||||
*/design-backup/
|
||||
*/WebContent/plugins/
|
||||
|
||||
# SafeDB(damo) 벤더 API stub — 컴파일용 껍데기라 납품 대상 아님
|
||||
damo-manager/src/stub/
|
||||
|
||||
# 서드파티 폴더 안에 섞여 있는 우리 커스텀 파일은 되살린다
|
||||
# (예: WebContent/plugins/swaggerUI/djb-swagger-i18n.js)
|
||||
!*/WebContent/plugins/*djb*
|
||||
@@ -0,0 +1,157 @@
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDay.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDayId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHour.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHourId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonth.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonthId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwApiStatus.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwAuthClient.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwSvcInfo.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsDayRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsHourRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsMonthRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwApiStatusRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwAuthClientRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwSvcInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/filter/ApiTesterAuditLogger.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorChannel.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorInfoResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorSendResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorVerifyResponse.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpAuthInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpPasswordController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorCleanupScheduler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorContext.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/notice/dto/IncidentAffectedApiDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/partnership/dto/PartnershipApplicationSummaryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginFailureReason.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginType.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/controller/DuplicateLoginController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/DuplicateLoginService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/controller/SessionApiController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/entity/UserSession.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/filter/SessionValidationFilter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/dto/ApiStatisticsPeriodDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/entity/JobInfo.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/JobInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolver.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/ClientGuardService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordChangeEnforcementInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordEnforcementPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/config/StartupInfoPrinter.java
|
||||
eapim-portal/src/main/resources/menu.yml
|
||||
eapim-portal/src/main/resources/roles.yml
|
||||
eapim-portal/src/main/resources/static/js/api-selector.js
|
||||
eapim-portal/src/main/resources/static/js/password-policy.js
|
||||
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
|
||||
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_toast.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/cancelInvitationPopup.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/terminateRequestPopup.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/popup/twoFactorAuthPopup.html
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolverTest.java
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java
|
||||
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapServiceImpl.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/ui/SitemapNode.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/authserver/ClientBlockApiController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUIMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUIMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentTimelineUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java
|
||||
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/inflow/inflow/InflowClientControlManController.java
|
||||
eapim-admin/src/main/resources/apistatus-draft.yml
|
||||
eapim-admin/src/main/resources/logback-rinjae_ma.xml
|
||||
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java
|
||||
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java
|
||||
eapim-admin/WebContent/damo.jsp
|
||||
eapim-admin/WebContent/jsp/common/acl/sitemap/sitemapMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/rule/transform2/transform2ManApiPopup.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/portalmenu/portalMenuMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp
|
||||
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp
|
||||
eapim-admin/WebContent/jsp/onl/kjb/statistics/apiUseStatsMan.jsp
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/IpAddressMatcher.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/PhoneNumberUtil.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/PersonalDataEncryptConverter.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/UuidV7Generator.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
|
||||
elink-portal-common/src/test/java/com/eactive/apim/portal/common/util/IpAddressMatcherTest.java
|
||||
@@ -0,0 +1,173 @@
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/controller/ApiStatusController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ActiveIncidentDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/AffectedApiDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiCurrentStatusDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiOptionDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/DailyStatDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/IssueDateEntryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MaintenanceCardDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MyApiStatusDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/PastIssueCardDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/TimelineEntryDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/MyApiStatusQueryService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/controller/InquiryCommentController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentCreateRequest.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/InquiryCommentRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacade.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbInquiryStatus.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryClosedException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryCommentNotOwnedException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/support/InquiryCommentPermissionChecker.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuAccessInterceptor.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuModelAdvice.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuNode.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuSeeder.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuView.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalMenuYmlProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalRolesProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/advice/DjbTestbedExceptionHandler.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/config/DjbTestbedGatewayProperty.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedAuthController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedSpecController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialOptionDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialSecretDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbAuthType.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbGatewayMode.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/exception/DjbUnsupportedAuthTypeException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbSwaggerSpecEnricher.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedSpecServerRewriter.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookCreatedResult.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookEventTypeDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookRegistrationDTO.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookAlreadyExistsException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookNotFoundException.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/mapper/WebhookMapper.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestApiRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestEventRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestRepository.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequest.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApi.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApiId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEvent.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEventId.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookEventTypeProvider.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookSecretGenerator.java
|
||||
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookService.java
|
||||
eapim-portal/src/main/resources/menu.yml
|
||||
eapim-portal/src/main/resources/roles.yml
|
||||
eapim-portal/src/main/resources/static/favicon.png
|
||||
eapim-portal/src/main/resources/static/img/avatar1.svg
|
||||
eapim-portal/src/main/resources/static/img/avatar2.svg
|
||||
eapim-portal/src/main/resources/static/img/favicon/apple-touch-icon.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-16x16.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-180x180.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-192x192.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-32x32.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-48x48.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon-512x512.png
|
||||
eapim-portal/src/main/resources/static/img/favicon/favicon.png
|
||||
eapim-portal/src/main/resources/static/img/icon/icon_check_green.png
|
||||
eapim-portal/src/main/resources/static/img/icon/img_icon.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/api_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/api_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/faq_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/faq_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/feedback_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/notice_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/notice_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.png
|
||||
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/sigdnUp_img.svg
|
||||
eapim-portal/src/main/resources/static/img/keyimage/signUp_img.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-djb.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-jjb.png
|
||||
eapim-portal/src/main/resources/static/img/logo/logo-jjb_white.png
|
||||
eapim-portal/src/main/resources/static/js/api-selector.js
|
||||
eapim-portal/src/main/resources/static/js/djb/api-status-issues.js
|
||||
eapim-portal/src/main/resources/static/js/djb/api-status.js
|
||||
eapim-portal/src/main/resources/static/js/djb/client-guard.js
|
||||
eapim-portal/src/main/resources/static/js/djb/inquiry-comments.js
|
||||
eapim-portal/src/main/resources/static/js/djb/inquiry-view.js
|
||||
eapim-portal/src/main/resources/static/js/djb/toast.js
|
||||
eapim-portal/src/main/resources/static/js/password-policy.js
|
||||
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-response.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippet-panel.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippets.js
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-testbed.css
|
||||
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-validator.js
|
||||
eapim-portal/src/main/resources/static/sass/base/_djb-font.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_djb-inquiry-comments.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_toast.scss
|
||||
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
|
||||
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/djb/fragments-inquiry-comments.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
|
||||
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
|
||||
eapim-portal/src/main/resources/templates/views/djb/apistatus/index.html
|
||||
eapim-portal/src/main/resources/templates/views/djb/apistatus/issues.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/footer.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/header_container.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/service_sidebar.html
|
||||
eapim-portal/src/main/resources/templates/views/fragment/djbank/terms_agreements.html
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncident.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApi.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApiId.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentTimeline.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentKind.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentState.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentApiRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentTimelineRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
|
||||
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
|
||||
damo-manager/src/main/java/com/eactive/ext/djb/DamoCli.java
|
||||
damo-manager/src/main/java/com/eactive/ext/djb/DamoManager.java
|
||||
@@ -0,0 +1,26 @@
|
||||
# DJB 커스텀 소스 납품 수집 설정
|
||||
#
|
||||
# 형식:
|
||||
# MODULE|<모듈 디렉터리>|<소스 경로들, 공백 구분>
|
||||
# EXT|<수집 대상 확장자, 콤마 구분>
|
||||
#
|
||||
# - 모듈 디렉터리는 저장소 루트(djb-eapim) 기준 상대 경로.
|
||||
# - 소스 경로가 실제로 없으면 조용히 건너뛴다.
|
||||
# - '#'으로 시작하는 줄과 빈 줄은 무시된다.
|
||||
# - 파일 단위 포함/제외는 export-filter.rules 에서 관리한다.
|
||||
|
||||
MODULE|eapim-portal|src/main
|
||||
#MODULE|eapim-admin|src WebContent
|
||||
MODULE|elink-portal-common|src/main
|
||||
MODULE|damo-manager|src
|
||||
|
||||
# eapim-online 은 납품 범위에서 제외한다.
|
||||
# Gateway 제품(elink-online) 본체라 신규 파일 중 무엇이 DJB 커스텀이고
|
||||
# 무엇이 제품 업그레이드분인지 포털 쪽에서 판정할 수 없다.
|
||||
# 규칙 파일이 아니라 여기서 빼는 이유: export-filter.rules 의 '!*djb*' 예외가
|
||||
# 뒤에 있어 DJBEncrypt.java 같은 파일이 되살아나기 때문.
|
||||
#MODULE|eapim-online|src elink-online-common/src elink-online-core/src elink-online-core-jpa/src elink-online-emsclient/src elink-online-transformer/src
|
||||
|
||||
# 수집 대상 확장자 (확장자 없는 파일은 제외된다)
|
||||
# EXT|java,xml,yml,yaml,properties,sql,html,jsp,jspf,tag,js,ts,css,scss,json,txt,md
|
||||
EXT|java,xml,yml,yaml,properties,html,jsp,jspf,tag,js,ts,css,scss,jpg,jpeg,png,gif,svg,ico
|
||||
@@ -254,7 +254,8 @@ CREATE TABLE DVPOWN.PT_MESSAGE_RECIPIENT
|
||||
)
|
||||
;
|
||||
|
||||
create table DVPOWN.PT_TOKEN
|
||||
create table PT_TOKEN
|
||||
(
|
||||
(
|
||||
TOKEN VARCHAR2(255) not null
|
||||
primary key,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
-- =============================================================================
|
||||
-- 세션 타이머 / 유휴 자동 로그아웃 / 중복로그인 처리용 사용자 세션 추적 테이블
|
||||
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
|
||||
-- 엔티티: com.eactive.apim.portal.apps.session.entity.UserSession
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE PTL_USER_SESSION
|
||||
(
|
||||
SESSION_ID VARCHAR2(128) NOT NULL,
|
||||
USER_ID VARCHAR2(36) NOT NULL,
|
||||
LOGIN_ID VARCHAR2(200) NOT NULL,
|
||||
LOGIN_TIME TIMESTAMP(6) NOT NULL,
|
||||
LAST_ACCESS_TIME TIMESTAMP(6) NOT NULL,
|
||||
IP_ADDRESS VARCHAR2(45),
|
||||
USER_AGENT VARCHAR2(500),
|
||||
FORCE_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL,
|
||||
CONSTRAINT PK_PTL_USER_SESSION PRIMARY KEY (SESSION_ID)
|
||||
);
|
||||
|
||||
-- 중복로그인 강제 로그아웃 / 활성 세션 조회는 LOGIN_ID 기준
|
||||
CREATE INDEX IX_PTL_USER_SESSION_LOGIN ON PTL_USER_SESSION (LOGIN_ID);
|
||||
|
||||
COMMENT ON TABLE PTL_USER_SESSION IS '포털 사용자 세션 추적 (타이머/유휴 로그아웃/중복로그인)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.SESSION_ID IS 'HTTP 세션 ID (PK)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.USER_ID IS '포털 사용자 ID';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_ID IS '로그인 ID (이메일, 소문자)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_TIME IS '로그인 시각';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.LAST_ACCESS_TIME IS '마지막 접근 시각 (만료 판단 기준)';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.IP_ADDRESS IS '접속 IP';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.USER_AGENT IS 'User-Agent';
|
||||
COMMENT ON COLUMN PTL_USER_SESSION.FORCE_LOGOUT IS '강제 로그아웃 플래그 (Y/N)';
|
||||
|
||||
-- (선택) 세션 타임아웃(분) 설정값을 미리 지정. 미삽입 시 최초 접근 때 기본값 15로 자동 생성됨.
|
||||
-- INSERT INTO PTL_PROPERTY (PROPERTY_GROUP_NAME, PROPERTY_NAME, PROPERTY_VALUE, PROPERTY_DESC)
|
||||
-- VALUES ('Portal', 'session.timeout.minutes', '15', '세션 타임아웃 시간 (분)');
|
||||
-- COMMIT;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- =============================================================================
|
||||
-- 2FA 인증코드 조회용 결정적 해시 키 컬럼 추가
|
||||
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
|
||||
-- 엔티티: com.eactive.apim.portal.portaluser.entity.TwoFactorAuth
|
||||
--
|
||||
-- 배경: RECIPIENT(이메일/휴대폰) 컬럼에 PersonalDataEncryptConverter 암호화가
|
||||
-- 적용되면서, 값 동등 조회(WHERE RECIPIENT = ?)와 중복정리가 깨져
|
||||
-- 인증코드 검증이 "일치하지 않습니다"로 실패함.
|
||||
-- 해결: RECIPIENT 는 암호화 보관(감사/표시용)을 유지하고, 평문의 결정적 해시
|
||||
-- (HMAC-SHA256, 소문자 hex 64자)를 RECIPIENT_KEY 에 저장하여 조회/정리에 사용.
|
||||
--
|
||||
-- ※ hibernate.hbm2ddl.auto=validate 이므로, 신규 엔티티 배포(재기동) "전"에
|
||||
-- 반드시 이 스크립트를 먼저 실행해야 기동 검증을 통과함.
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE PTL_TWO_FACTOR_AUTH ADD (RECIPIENT_KEY VARCHAR2(64));
|
||||
|
||||
-- 인증코드 조회/중복정리는 RECIPIENT_KEY 기준
|
||||
CREATE INDEX IX_PTL_TWO_FACTOR_AUTH_RKEY ON PTL_TWO_FACTOR_AUTH (RECIPIENT_KEY);
|
||||
|
||||
COMMENT ON COLUMN PTL_TWO_FACTOR_AUTH.RECIPIENT_KEY IS '수신자 결정적 해시 조회키 (HMAC-SHA256 hex). RECIPIENT 암호화로 깨지는 동등조회/중복정리용';
|
||||
|
||||
COMMIT;
|
||||
@@ -5,7 +5,3 @@ include 'elink-portal-common'
|
||||
|
||||
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
|
||||
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||
|
||||
|
||||
//include 'kjb-safedb'
|
||||
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
||||
@@ -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,31 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
* 다건 org 소속 CLIENTID 목록 (인덱스 페이지 전체 통계 집계용).
|
||||
*/
|
||||
@Query("SELECT c.clientId FROM GwAuthClient c WHERE c.orgId IN :orgIds")
|
||||
List<String> findClientIdsByOrgIdIn(@Param("orgIds") List<String> orgIds);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||