From a69293fb51fc25d7915e40c242ea5d76cafb178b Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 09:31:01 +0900 Subject: [PATCH 01/66] =?UTF-8?q?[api]=20=EC=A0=A0=ED=82=A8=EC=8A=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=85=8C=EA=B7=B8=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 7ec1c07..69aa1cc 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -8,7 +8,10 @@ pipeline { environment { // ===== Registry ===== - DUMMY = "true" + REGISTRY_HOST = "registry.pjt.kr" + IMAGE_REPO = "alist/api" + IMAGE_TAG = "${BUILD_NUMBER}" + DOCKER_CRED = "docker-registry-cred" // ← Jenkins Credentials ID } stages { @@ -27,5 +30,15 @@ pipeline { ''' } } + + stage('3) image tab create') { + steps { + script { + env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() + env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" + echo "IMAGE_TAG = ${env.IMAGE_TAG}" + } + } + } } } From 38813d8ef6382a9d98a4343f65ecaa96dc65b693 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:21:08 +0900 Subject: [PATCH 02/66] =?UTF-8?q?[api]=20=EB=B9=8C=EB=93=9C=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 69aa1cc..66de36b 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -10,7 +10,7 @@ pipeline { // ===== Registry ===== REGISTRY_HOST = "registry.pjt.kr" IMAGE_REPO = "alist/api" - IMAGE_TAG = "${BUILD_NUMBER}" + IMAGE_TAG = "" DOCKER_CRED = "docker-registry-cred" // ← Jenkins Credentials ID } From 8649829e51b886bb8eb3a9851ebb569c62874366 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:22:34 +0900 Subject: [PATCH 03/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 66de36b..6aadebd 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -35,6 +35,7 @@ pipeline { steps { script { env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() + echo "SHORT_SHA = ${env.SHORT_SHA}" env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" echo "IMAGE_TAG = ${env.IMAGE_TAG}" } From 81ef72f8437b3e3381121ec5d18ea97c864f9661 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:24:06 +0900 Subject: [PATCH 04/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 6aadebd..afa9862 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -31,12 +31,14 @@ pipeline { } } - stage('3) image tab create') { + stage('3) image tag create') { steps { script { - env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() - echo "SHORT_SHA = ${env.SHORT_SHA}" - env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" + env.IMAGE_TAG = sh( + script: 'echo "${BUILD_NUMBER}-$(git rev-parse --short HEAD)"', + returnStdout: true + ).trim() + echo "IMAGE_TAG = ${env.IMAGE_TAG}" } } From e0f731718e95d311d7297cca01d33bfd4e075774 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:26:15 +0900 Subject: [PATCH 05/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index afa9862..6c4e7b2 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -10,7 +10,6 @@ pipeline { // ===== Registry ===== REGISTRY_HOST = "registry.pjt.kr" IMAGE_REPO = "alist/api" - IMAGE_TAG = "" DOCKER_CRED = "docker-registry-cred" // ← Jenkins Credentials ID } @@ -31,14 +30,12 @@ pipeline { } } - stage('3) image tag create') { + stage('3) image tab create') { steps { script { - env.IMAGE_TAG = sh( - script: 'echo "${BUILD_NUMBER}-$(git rev-parse --short HEAD)"', - returnStdout: true - ).trim() - + env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() + echo "SHORT_SHA = ${env.SHORT_SHA}" + env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" echo "IMAGE_TAG = ${env.IMAGE_TAG}" } } From c1b4f4a6fda7b2301fce0e71a2b246b2a48fbec0 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:35:59 +0900 Subject: [PATCH 06/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91=20-=20=EB=8F=84=EC=BB=A4=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 6c4e7b2..d882f46 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -40,5 +40,14 @@ pipeline { } } } + + stage('4) Docker Build') { + steps { + sh ''' + set -e + docker build -t ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} . + ''' + } + } } } From 2a2193fdcdd8c7ec2bf5fd0c4aa48c7c19d3da42 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 10:56:31 +0900 Subject: [PATCH 07/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91=20-=20gradle=20=EC=97=90=20jar=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EB=A7=8C=20=EC=83=9D=EC=84=B1=EB=90=98=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index 8a9143f..fecf192 100644 --- a/build.gradle +++ b/build.gradle @@ -30,3 +30,9 @@ dependencies { tasks.named('test') { useJUnitPlatform() } +bootJar { + archiveFileName = "api.jar" +} +jar { + enabled = false +} \ No newline at end of file From 7341f28f9d763d267e21b95d295f3416de46e8e0 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 11:05:28 +0900 Subject: [PATCH 08/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91=20-=20=EB=8F=84=EC=BB=A4=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index d882f46..0c3d1c9 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -10,7 +10,7 @@ pipeline { // ===== Registry ===== REGISTRY_HOST = "registry.pjt.kr" IMAGE_REPO = "alist/api" - DOCKER_CRED = "docker-registry-cred" // ← Jenkins Credentials ID + DOCKER_CRED = "docker-registry-login" } stages { @@ -49,5 +49,26 @@ pipeline { ''' } } + + stage('5) Docker Login & Push') { + steps { + withCredentials([usernamePassword( + credentialsId: DOCKER_LOGIN, + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' + )]) { + sh ''' + set -e + + echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} \ + -u "$DOCKER_USER" --password-stdin + + docker push ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} + + docker logout ${REGISTRY_HOST} + ''' + } + } + } } } From 6de8ebca9bdfdfa6c883e16e3f18e03eeb3a4900 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 11:07:35 +0900 Subject: [PATCH 09/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91=20-=20=EB=8F=84=EC=BB=A4=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 0c3d1c9..ecba0be 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -53,7 +53,7 @@ pipeline { stage('5) Docker Login & Push') { steps { withCredentials([usernamePassword( - credentialsId: DOCKER_LOGIN, + credentialsId: DOCKER_CRED, usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { From f87828b4e5d8b75c2a2fb13d6869ee645102ceb6 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 15:49:41 +0900 Subject: [PATCH 10/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index ecba0be..b0d88b9 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -10,7 +10,7 @@ pipeline { // ===== Registry ===== REGISTRY_HOST = "registry.pjt.kr" IMAGE_REPO = "alist/api" - DOCKER_CRED = "docker-registry-login" + DOCKER_CRED = "registry-pjt" } stages { From 18eb159f26c39d3d39eb275183b805f03af6392a Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 16:23:49 +0900 Subject: [PATCH 11/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A6=88=20=EB=B3=B5=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index b0d88b9..d349826 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -8,9 +8,20 @@ pipeline { environment { // ===== Registry ===== - REGISTRY_HOST = "registry.pjt.kr" - IMAGE_REPO = "alist/api" - DOCKER_CRED = "registry-pjt" + REGISTRY_HOST = "registry.pjt.kr" + IMAGE_REPO = "alist/api" + DOCKER_CRED = "registry-pjt" + + // ===== api ssh ===== + SSH_HOST = "121.160.234.222" + SSH_PORT = "2001" + SSH_USER = "bigfuntnp" + SSH_COMPOSE_DIR = "/srv/project/alist/compose" + SSH_COMPOSE_FILE = "api-compose.pjt.yml" + + // ===== jenkins workspace path ===== + WORK_COMPOSE_DIR = "deploy/pjt/compose/" + WORK_COMPOSE_FILE = "api-compose.pjt.yml" } stages { @@ -70,5 +81,19 @@ pipeline { } } } + + stage('6) Copy compose') { + steps { + sshagent(['ssh-pjt']) { + sh ''' + set -e + SRC="${WORK_COMPOSE_DIR}/${WORK_COMPOSE_FILE}" + DST="${SSH_USER}@${SSH_HOST}:${SSH_COMPOSE_DIR}/${SSH_COMPOSE_FILE}" + + scp -P ${SSH_PORT} -o StrictHostKeyChecking=no "$SRC" "$DST" + ''' + } + } + } } } From 1113355c74bd2577acd0e6d3e311a23fd7333e95 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 16:24:51 +0900 Subject: [PATCH 12/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A6=88=20=EB=B3=B5=EC=82=AC2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index d349826..1dee48c 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -20,7 +20,7 @@ pipeline { SSH_COMPOSE_FILE = "api-compose.pjt.yml" // ===== jenkins workspace path ===== - WORK_COMPOSE_DIR = "deploy/pjt/compose/" + WORK_COMPOSE_DIR = "deploy/pjt/compose" WORK_COMPOSE_FILE = "api-compose.pjt.yml" } From a5ec049040d337c6a12cc6a43aa04ae5e8afe066 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 16:56:46 +0900 Subject: [PATCH 13/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 1dee48c..36a6052 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -95,5 +95,51 @@ pipeline { } } } + + stage('7) Remote docker login') { + steps { + sshagent(['ssh-pjt']) { + withCredentials([usernamePassword( + credentialsId: "${DOCKER_CRED}", + usernameVariable: 'REG_USER', + passwordVariable: 'REG_PASS' + )]) { + sh ''' + set -e + + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no \ + ${SSH_USER}@${SSH_HOST} \ + REGISTRY_HOST="${REGISTRY_HOST}" \ + REG_USER="${REG_USER}" \ + REG_PASS="${REG_PASS}" \ + 'set -e + echo "$REG_PASS" | docker login "$REGISTRY_HOST" -u "$REG_USER" --password-stdin + ' + ''' + } + } + } + } + + stage('8) Remote docker pull') { + steps { + sshagent(['ssh-pjt']) { + sh ''' + set -e + + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no \ + ${SSH_USER}@${SSH_HOST} \ + REGISTRY_HOST="${REGISTRY_HOST}" \ + IMAGE_REPO="${IMAGE_REPO}" \ + IMAGE_TAG="${IMAGE_TAG}" \ + 'set -e + cd ${SSH_COMPOSE_DIR} + docker compose -f ${SSH_COMPOSE_FILE} pull + ' + ''' + } + } + } + } } From cbf40f30995648c2b69d41e04ee1ef5e8bf548ed Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:00:54 +0900 Subject: [PATCH 14/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 36a6052..c1d786d 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -100,20 +100,22 @@ pipeline { steps { sshagent(['ssh-pjt']) { withCredentials([usernamePassword( - credentialsId: "${DOCKER_CRED}", - usernameVariable: 'REG_USER', - passwordVariable: 'REG_PASS' + credentialsId: DOCKER_CRED, + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' )]) { sh ''' set -e - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no \ + ssh -p ${SSH_PORT} \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ REGISTRY_HOST="${REGISTRY_HOST}" \ - REG_USER="${REG_USER}" \ - REG_PASS="${REG_PASS}" \ + DOCKER_USER="${DOCKER_USER}" \ + DOCKER_PASS="${DOCKER_PASS}" \ 'set -e - echo "$REG_PASS" | docker login "$REGISTRY_HOST" -u "$REG_USER" --password-stdin + echo "$DOCKER_PASS" | docker login "$REGISTRY_HOST" -u "$DOCKER_USER" --password-stdin ' ''' } @@ -127,19 +129,20 @@ pipeline { sh ''' set -e - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no \ + ssh -p ${SSH_PORT} \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ REGISTRY_HOST="${REGISTRY_HOST}" \ IMAGE_REPO="${IMAGE_REPO}" \ IMAGE_TAG="${IMAGE_TAG}" \ 'set -e - cd ${SSH_COMPOSE_DIR} - docker compose -f ${SSH_COMPOSE_FILE} pull + cd '"${SSH_COMPOSE_DIR}"' + docker compose -f '"${SSH_COMPOSE_FILE}"' pull ' ''' } } - } } } From fd432ff0ddd3128ec27c0de11ba2fabde0736764 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:01:29 +0900 Subject: [PATCH 15/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index c1d786d..8afd40c 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -143,6 +143,8 @@ pipeline { ''' } } + } + } } From 494ba056908d8390f5c8a2ff518eddd953f7db52 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:03:27 +0900 Subject: [PATCH 16/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 8afd40c..d87df2b 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -111,10 +111,11 @@ pipeline { -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ - REGISTRY_HOST="${REGISTRY_HOST}" \ - DOCKER_USER="${DOCKER_USER}" \ - DOCKER_PASS="${DOCKER_PASS}" \ 'set -e + REGISTRY_HOST='"${REGISTRY_HOST}"' + DOCKER_USER='"${DOCKER_USER}"' + DOCKER_PASS='"${DOCKER_PASS}"' + echo "$DOCKER_PASS" | docker login "$REGISTRY_HOST" -u "$DOCKER_USER" --password-stdin ' ''' From 02b13d18feb0bbca54a8ad7bc94d6d0abd560468 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:19:30 +0900 Subject: [PATCH 17/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index d87df2b..b276ca2 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -129,16 +129,17 @@ pipeline { sshagent(['ssh-pjt']) { sh ''' set -e - - ssh -p ${SSH_PORT} \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ - REGISTRY_HOST="${REGISTRY_HOST}" \ - IMAGE_REPO="${IMAGE_REPO}" \ - IMAGE_TAG="${IMAGE_TAG}" \ 'set -e cd '"${SSH_COMPOSE_DIR}"' + export REGISTRY_HOST='"${REGISTRY_HOST}"' + export IMAGE_REPO='"${IMAGE_REPO}"' + export IMAGE_TAG='"${IMAGE_TAG}"' + export SPRING_PROFILE='"${SPRING_PROFILE:-}"' + export JVM_XMS='"${JVM_XMS:-}"' + export JVM_XMX='"${JVM_XMX:-}"' + export JVM_META='"${JVM_META:-}"' docker compose -f '"${SSH_COMPOSE_FILE}"' pull ' ''' @@ -147,5 +148,6 @@ pipeline { } + } } From 1cdf8977ae02c78eab57361dccda11fc87b2200b Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:24:32 +0900 Subject: [PATCH 18/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20pull=EA=B9=8C=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index b276ca2..a41be8d 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -133,21 +133,13 @@ pipeline { ${SSH_USER}@${SSH_HOST} \ 'set -e cd '"${SSH_COMPOSE_DIR}"' - export REGISTRY_HOST='"${REGISTRY_HOST}"' - export IMAGE_REPO='"${IMAGE_REPO}"' export IMAGE_TAG='"${IMAGE_TAG}"' - export SPRING_PROFILE='"${SPRING_PROFILE:-}"' - export JVM_XMS='"${JVM_XMS:-}"' - export JVM_XMX='"${JVM_XMX:-}"' - export JVM_META='"${JVM_META:-}"' - docker compose -f '"${SSH_COMPOSE_FILE}"' pull + docker compose --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' pull ' ''' } } } - - } } From e4f353b097bf472ec9a0c9bc04494f95f0869de4 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 3 Feb 2026 17:55:10 +0900 Subject: [PATCH 19/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index a41be8d..33acde9 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -141,5 +141,22 @@ pipeline { } } + stage('9) Remote docker up') { + steps { + sshagent(['ssh-pjt']) { + sh ''' + set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + ${SSH_USER}@${SSH_HOST} \ + 'set -e + cd '"${SSH_COMPOSE_DIR}"' + export IMAGE_TAG='"${IMAGE_TAG}"' + docker compose --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' up -d --remove-orphans + ' + ''' + } + } + } + } } From 4d4c998770d0e6e2cc814df6c69b772092f5d351 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 10:19:05 +0900 Subject: [PATCH 20/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=ED=91=B8=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 33acde9..e0f84dc 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -27,6 +27,8 @@ pipeline { stages { stage('1) Git check out') { steps { + echo "[1/1] Git check out" + checkout scm } } @@ -35,15 +37,21 @@ pipeline { steps { sh ''' set -e + + echo "[1/1] Gradle build" + chmod +x ./gradlew ./gradlew clean build -x test ''' } } - stage('3) image tab create') { + stage('3) ImageTag create') { steps { script { + + echo "[1/1] ImageTag create" + env.SHORT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() echo "SHORT_SHA = ${env.SHORT_SHA}" env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.SHORT_SHA}" @@ -56,12 +64,15 @@ pipeline { steps { sh ''' set -e + + echo "[1/1] docker build" + docker build -t ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} . ''' } } - stage('5) Docker Login & Push') { + stage('5) Docker registry Login & Push') { steps { withCredentials([usernamePassword( credentialsId: DOCKER_CRED, @@ -71,9 +82,11 @@ pipeline { sh ''' set -e + echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} \ -u "$DOCKER_USER" --password-stdin + echo "[2/2] docker image push" docker push ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} docker logout ${REGISTRY_HOST} @@ -87,16 +100,20 @@ pipeline { sshagent(['ssh-pjt']) { sh ''' set -e + + echo "[1/2] ssh login" + SRC="${WORK_COMPOSE_DIR}/${WORK_COMPOSE_FILE}" DST="${SSH_USER}@${SSH_HOST}:${SSH_COMPOSE_DIR}/${SSH_COMPOSE_FILE}" + echo "[2/2] Copy compose $SRC -> $DST" scp -P ${SSH_PORT} -o StrictHostKeyChecking=no "$SRC" "$DST" ''' } } } - stage('7) Remote docker login') { + stage('7) Remote docker registry login') { steps { sshagent(['ssh-pjt']) { withCredentials([usernamePassword( @@ -107,6 +124,8 @@ pipeline { sh ''' set -e + echo "[1/1] docker registry login" + ssh -p ${SSH_PORT} \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ @@ -134,14 +153,16 @@ pipeline { 'set -e cd '"${SSH_COMPOSE_DIR}"' export IMAGE_TAG='"${IMAGE_TAG}"' - docker compose --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' pull + + echo "[1/1] docker pull" + docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' pull ' ''' } } } - stage('9) Remote docker up') { + stage('9) Remote docker down & up') { steps { sshagent(['ssh-pjt']) { sh ''' @@ -151,7 +172,15 @@ pipeline { 'set -e cd '"${SSH_COMPOSE_DIR}"' export IMAGE_TAG='"${IMAGE_TAG}"' - docker compose --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' up -d --remove-orphans + + echo "[1/3] docker down (remove orphans)" + docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f "'"${SSH_COMPOSE_FILE}"'" down --remove-orphans + + echo "[2/3] docker up" + docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' up -d + + echo "[3/3] docker ps" + docker compose -p alist-api --env-file /srv/project/alist/env/api/.env -f '"${SSH_COMPOSE_FILE}"' ps ' ''' } From 0add5e1cd843cb1605f1f0d135964d69c9b23f2d Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 10:28:35 +0900 Subject: [PATCH 21/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=ED=91=B8=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index cc18bfa..142dc67 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -1,5 +1,6 @@ services: app: + container_name: alist-api image: registry.pjt.kr/alist/api:${IMAGE_TAG} restart: always From a3be7202a33f2d772df63f3b81fce6a723d0d80d Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 14:25:52 +0900 Subject: [PATCH 22/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 2 +- src/main/resources/application-local.yaml | 2 +- src/main/resources/application.yaml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index 142dc67..771a4aa 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -5,7 +5,7 @@ services: restart: always ports: - - "127.0.0.1:8006:8080" + - "127.0.0.1:8106:8080" env_file: - /srv/project/alist/env/api/.env diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 8abea22..edd8d29 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -1,5 +1,5 @@ server: - port: 8006 + port: 8106 spring: application: name: api diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8a2082b..e4f4dcc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,3 +1,5 @@ +server: + forward-headers-strategy: framework spring: application: name: api From 5618d54a7c1a6d02cb05f03b3456cacb83b22533 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 15:20:27 +0900 Subject: [PATCH 23/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 157 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index e0f84dc..6d44fa7 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -95,7 +95,27 @@ pipeline { } } - stage('6) Copy compose') { + stage('6) Jenkins cleanup') { + steps { + sh ''' + set -e + + echo "[1/4] Remove pushed image from Jenkins" + docker rmi ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} 2>/dev/null || true + + echo "[2/4] Remove dangling images" + docker image prune -f + + echo "[3/4] Remove old build cache (safe-ish)" + # 빌드 캐시만 정리: 다른 컨테이너/볼륨에는 영향 적음 + docker builder prune -f || true + + echo "[4/4] Skip docker system prune --volumes (too aggressive on shared nodes)" + ''' + } + } + + stage('7) Copy compose') { steps { sshagent(['ssh-pjt']) { sh ''' @@ -113,7 +133,7 @@ pipeline { } } - stage('7) Remote docker registry login') { + stage('8) Remote docker registry login') { steps { sshagent(['ssh-pjt']) { withCredentials([usernamePassword( @@ -143,7 +163,7 @@ pipeline { } } - stage('8) Remote docker pull') { + stage('9) Remote docker pull') { steps { sshagent(['ssh-pjt']) { sh ''' @@ -162,7 +182,7 @@ pipeline { } } - stage('9) Remote docker down & up') { + stage('10) Remote docker down & up') { steps { sshagent(['ssh-pjt']) { sh ''' @@ -187,5 +207,134 @@ pipeline { } } + stage('11) Remote cleanup') { + steps { + sshagent(['ssh-pjt']) { + sh ''' + set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + ${SSH_USER}@${SSH_HOST} \ + 'set -e + + echo "[0] Collect images used by running containers" + RUNNING_IMAGES=$(docker ps -q | xargs -r docker inspect --format="{{.Image}}" | sort -u) + + echo "[1/3] Remove old registry images (except running ones)" + docker images '"${REGISTRY_HOST}/${IMAGE_REPO}"' --format "{{.ID}} {{.CreatedAt}}" | \ + sort -k2 -r | \ + awk "{print \$1}" | \ + while read IMAGE_ID; do + echo "$RUNNING_IMAGES" | grep -q "$IMAGE_ID" && continue + docker rmi -f "$IMAGE_ID" 2>/dev/null || true + done + + echo "[2/3] Remove dangling images (safe)" + docker image prune -f + + echo "[3/3] Remove unused volumes (safe)" + docker volume prune -f + ' + ''' + } + } + } + + stage('12) Registry cleanup (keep last 5 + protect running)') { + steps { + sshagent(['ssh-pjt']) { + sh ''' + set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + ${SSH_USER}@${SSH_HOST} \ + 'set -e + + REGISTRY_URL="https://'${REGISTRY_HOST}'" # 예: https://registry.pjt.kr + REPO="'${IMAGE_REPO}'" # 예: alist/api + KEEP=5 + + # (옵션) Basic Auth 쓰면 아래 활성화하고 curl에 -u "$AUTH" 추가 + # AUTH="user:pass" + + echo "[0/5] Ensure jq exists" + if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -y + sudo apt-get install -y jq + fi + + echo "[1/5] Collect running tags to protect" + # 현재 서버에서 실행 중인 컨테이너가 사용하는 해당 레포 이미지 태그만 추출 + RUNNING_TAGS=$(docker ps --format "{{.Image}}" | \ + grep -E "^'${REGISTRY_HOST}'/'${IMAGE_REPO}':" | \ + awk -F: "{print \\$2}" | sort -u || true) + + if [ -n "$RUNNING_TAGS" ]; then + echo "Running tags (protected):" + echo "$RUNNING_TAGS" | sed "s/^/ - /" + else + echo "No running tags found for ${REGISTRY_HOST}/${IMAGE_REPO} (ok)" + fi + + echo "[2/5] Fetch tags from registry: $REPO" + TAGS=$(curl -fsS "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) + # Basic Auth 쓰면 위 curl을 아래로 바꿔: + # TAGS=$(curl -fsS -u "$AUTH" "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) + + if [ -z "$TAGS" ]; then + echo "No tags found in registry. Skip." + exit 0 + fi + + echo "[3/5] Sort tags desc (tag must be sortable for 'latest' semantics)" + SORTED=$(echo "$TAGS" | sort -r) + + KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) + DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) + + echo "Keep tags (latest $KEEP):" + echo "$KEEP_TAGS" | sed "s/^/ - /" + + echo "[4/5] Delete old tags except protected running tags" + for TAG in $DEL_CANDIDATES; do + # 실행 중 태그는 무조건 스킵 + if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then + echo "Skip running tag: $REPO:$TAG" + continue + fi + + echo "Deleting $REPO:$TAG" + + DIGEST=$(curl -fsSI \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ + | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") + + # Basic Auth 쓰면 위 curl을 아래로 바꿔: + # DIGEST=$(curl -fsSI -u "$AUTH" \ + # -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + # "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ + # | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") + + if [ -z "$DIGEST" ]; then + echo " digest not found. skip $TAG" + continue + fi + + curl -fsS -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true + # Basic Auth 쓰면: + # curl -fsS -u "$AUTH" -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true + done + + echo "[5/5] Garbage collect (reclaim disk space)" + # push가 진행 중이면 위험할 수 있으니, 배포 성공 후 마지막 단계에서만 실행 추천 + docker exec registry registry garbage-collect /etc/docker/registry/config.yml + + echo "Registry cleanup done." + ' + ''' + } + } + } + + } } From 9c85d89f07e96ee426e950a302f3e2ba67db9455 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 15:41:34 +0900 Subject: [PATCH 24/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 136 ++++++++++++++++++------------------------------ 1 file changed, 51 insertions(+), 85 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 6d44fa7..0785569 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -100,17 +100,14 @@ pipeline { sh ''' set -e - echo "[1/4] Remove pushed image from Jenkins" + echo "[1/3] Remove pushed image from Jenkins" docker rmi ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} 2>/dev/null || true - echo "[2/4] Remove dangling images" + echo "[2/3] Remove dangling images" docker image prune -f - echo "[3/4] Remove old build cache (safe-ish)" - # 빌드 캐시만 정리: 다른 컨테이너/볼륨에는 영향 적음 + echo "[3/3] Remove old build cache (safe-ish)" docker builder prune -f || true - - echo "[4/4] Skip docker system prune --volumes (too aggressive on shared nodes)" ''' } } @@ -241,100 +238,69 @@ pipeline { stage('12) Registry cleanup (keep last 5 + protect running)') { steps { - sshagent(['ssh-pjt']) { - sh ''' - set -e - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - ${SSH_USER}@${SSH_HOST} \ - 'set -e + withCredentials([usernamePassword( + credentialsId: DOCKER_CRED, + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' + )]) { + sshagent(['ssh-pjt']) { + sh ''' + set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + ${SSH_USER}@${SSH_HOST} \ + 'set -e + REGISTRY_URL="https://${REGISTRY_HOST}" + REPO="${IMAGE_REPO}" + KEEP=5 + AUTH="'${DOCKER_USER}':'${DOCKER_PASS}'" - REGISTRY_URL="https://'${REGISTRY_HOST}'" # 예: https://registry.pjt.kr - REPO="'${IMAGE_REPO}'" # 예: alist/api - KEEP=5 + command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) - # (옵션) Basic Auth 쓰면 아래 활성화하고 curl에 -u "$AUTH" 추가 - # AUTH="user:pass" + echo "[1/5] Collect running tags to protect" + RUNNING_TAGS=$(docker ps --format "{{.Image}}" | \ + grep -E "^${REGISTRY_HOST}/${IMAGE_REPO}:" | \ + awk -F: "{print \\$2}" | sort -u || true) - echo "[0/5] Ensure jq exists" - if ! command -v jq >/dev/null 2>&1; then - sudo apt-get update -y - sudo apt-get install -y jq - fi + echo "[2/5] Fetch tags from registry" + TAGS=$(curl -fsS -u "$AUTH" "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) - echo "[1/5] Collect running tags to protect" - # 현재 서버에서 실행 중인 컨테이너가 사용하는 해당 레포 이미지 태그만 추출 - RUNNING_TAGS=$(docker ps --format "{{.Image}}" | \ - grep -E "^'${REGISTRY_HOST}'/'${IMAGE_REPO}':" | \ - awk -F: "{print \\$2}" | sort -u || true) + [ -z "$TAGS" ] && { echo "No tags found. Skip."; exit 0; } - if [ -n "$RUNNING_TAGS" ]; then - echo "Running tags (protected):" - echo "$RUNNING_TAGS" | sed "s/^/ - /" - else - echo "No running tags found for ${REGISTRY_HOST}/${IMAGE_REPO} (ok)" - fi + echo "[3/5] Sort tags desc" + SORTED=$(echo "$TAGS" | sort -r) + KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) + DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) - echo "[2/5] Fetch tags from registry: $REPO" - TAGS=$(curl -fsS "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) - # Basic Auth 쓰면 위 curl을 아래로 바꿔: - # TAGS=$(curl -fsS -u "$AUTH" "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) + echo "Keep tags:" + echo "$KEEP_TAGS" | sed "s/^/ - /" - if [ -z "$TAGS" ]; then - echo "No tags found in registry. Skip." - exit 0 - fi + echo "[4/5] Delete old tags except running tags" + for TAG in $DEL_CANDIDATES; do + if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then + echo "Skip running tag: $REPO:$TAG" + continue + fi - echo "[3/5] Sort tags desc (tag must be sortable for 'latest' semantics)" - SORTED=$(echo "$TAGS" | sort -r) + DIGEST=$(curl -fsSI -u "$AUTH" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ + | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") - KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) - DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) + [ -z "$DIGEST" ] && { echo "digest not found. skip $TAG"; continue; } - echo "Keep tags (latest $KEEP):" - echo "$KEEP_TAGS" | sed "s/^/ - /" + curl -fsS -u "$AUTH" -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true + done - echo "[4/5] Delete old tags except protected running tags" - for TAG in $DEL_CANDIDATES; do - # 실행 중 태그는 무조건 스킵 - if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then - echo "Skip running tag: $REPO:$TAG" - continue - fi + echo "[5/5] Garbage collect" + docker exec registry registry garbage-collect /etc/docker/registry/config.yml - echo "Deleting $REPO:$TAG" - - DIGEST=$(curl -fsSI \ - -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ - "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ - | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") - - # Basic Auth 쓰면 위 curl을 아래로 바꿔: - # DIGEST=$(curl -fsSI -u "$AUTH" \ - # -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ - # "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ - # | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") - - if [ -z "$DIGEST" ]; then - echo " digest not found. skip $TAG" - continue - fi - - curl -fsS -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true - # Basic Auth 쓰면: - # curl -fsS -u "$AUTH" -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true - done - - echo "[5/5] Garbage collect (reclaim disk space)" - # push가 진행 중이면 위험할 수 있으니, 배포 성공 후 마지막 단계에서만 실행 추천 - docker exec registry registry garbage-collect /etc/docker/registry/config.yml - - echo "Registry cleanup done." - ' - ''' + echo "Registry cleanup done." + ' + ''' + } } } } - } } From 0e8876e66f4afea69b5f76bd78bcf03d9e49d607 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 15:44:48 +0900 Subject: [PATCH 25/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 88 +++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 0785569..6fcbb7a 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -246,61 +246,77 @@ pipeline { sshagent(['ssh-pjt']) { sh ''' set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ ${SSH_USER}@${SSH_HOST} \ - 'set -e - REGISTRY_URL="https://${REGISTRY_HOST}" - REPO="${IMAGE_REPO}" - KEEP=5 - AUTH="'${DOCKER_USER}':'${DOCKER_PASS}'" + "REGISTRY_HOST='${REGISTRY_HOST}' IMAGE_REPO='${IMAGE_REPO}' DOCKER_USER='${DOCKER_USER}' DOCKER_PASS='${DOCKER_PASS}' bash -se" <<'EOF' + set -e - command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) + REGISTRY_URL="https://${REGISTRY_HOST}" + REPO="${IMAGE_REPO}" + KEEP=5 + AUTH="${DOCKER_USER}:${DOCKER_PASS}" - echo "[1/5] Collect running tags to protect" - RUNNING_TAGS=$(docker ps --format "{{.Image}}" | \ - grep -E "^${REGISTRY_HOST}/${IMAGE_REPO}:" | \ - awk -F: "{print \\$2}" | sort -u || true) + command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) - echo "[2/5] Fetch tags from registry" - TAGS=$(curl -fsS -u "$AUTH" "$REGISTRY_URL/v2/$REPO/tags/list" | jq -r ".tags[]?" || true) + echo "[1/6] Debug URL" + echo "REGISTRY_URL=${REGISTRY_URL}" + echo "TAGS_URL=${REGISTRY_URL}/v2/${REPO}/tags/list" - [ -z "$TAGS" ] && { echo "No tags found. Skip."; exit 0; } + echo "[2/6] Collect running tags to protect" + RUNNING_TAGS=$(docker ps --format '{{.Image}}' | \ + grep -E "^${REGISTRY_HOST}/${REPO}:" | \ + awk -F: '{print $2}' | sort -u || true) - echo "[3/5] Sort tags desc" - SORTED=$(echo "$TAGS" | sort -r) - KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) - DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) + echo "[3/6] Fetch tags from registry" + TAGS=$(curl -fsS -u "${AUTH}" "${REGISTRY_URL}/v2/${REPO}/tags/list" | jq -r '.tags[]?' || true) - echo "Keep tags:" - echo "$KEEP_TAGS" | sed "s/^/ - /" + if [ -z "$TAGS" ]; then + echo "No tags found in registry. Skip." + exit 0 + fi - echo "[4/5] Delete old tags except running tags" - for TAG in $DEL_CANDIDATES; do - if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then - echo "Skip running tag: $REPO:$TAG" - continue - fi + echo "[4/6] Sort tags desc" + SORTED=$(echo "$TAGS" | sort -r) + KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) + DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) - DIGEST=$(curl -fsSI -u "$AUTH" \ - -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ - "$REGISTRY_URL/v2/$REPO/manifests/$TAG" \ - | awk -F": " "/Docker-Content-Digest/ {print \\$2}" | tr -d "\\r") + echo "Keep tags:" + echo "$KEEP_TAGS" | sed 's/^/ - /' - [ -z "$DIGEST" ] && { echo "digest not found. skip $TAG"; continue; } + echo "[5/6] Delete old tags except running tags" + for TAG in $DEL_CANDIDATES; do + if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then + echo "Skip running tag: ${REPO}:${TAG}" + continue + fi - curl -fsS -u "$AUTH" -X DELETE "$REGISTRY_URL/v2/$REPO/manifests/$DIGEST" || true - done + echo "Deleting ${REPO}:${TAG}" - echo "[5/5] Garbage collect" - docker exec registry registry garbage-collect /etc/docker/registry/config.yml + DIGEST=$(curl -fsSI -u "${AUTH}" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + "${REGISTRY_URL}/v2/${REPO}/manifests/${TAG}" \ + | awk -F': ' '/Docker-Content-Digest/ {print $2}' | tr -d '\r') - echo "Registry cleanup done." - ' + if [ -z "$DIGEST" ]; then + echo " digest not found. skip ${TAG}" + continue + fi + + curl -fsS -u "${AUTH}" -X DELETE "${REGISTRY_URL}/v2/${REPO}/manifests/${DIGEST}" || true + done + + echo "[6/6] Garbage collect (reclaim disk)" + docker exec registry registry garbage-collect /etc/docker/registry/config.yml + + echo "Registry cleanup done." + EOF ''' } } } } + } } From 6722d774a7b33a565c3df4bd9e60f695248bf8f5 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 15:47:04 +0900 Subject: [PATCH 26/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 104 ++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 6fcbb7a..eb8687c 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -244,79 +244,79 @@ pipeline { passwordVariable: 'DOCKER_PASS' )]) { sshagent(['ssh-pjt']) { - sh ''' - set -e + sh '''#!/usr/bin/env bash + set -e - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - ${SSH_USER}@${SSH_HOST} \ - "REGISTRY_HOST='${REGISTRY_HOST}' IMAGE_REPO='${IMAGE_REPO}' DOCKER_USER='${DOCKER_USER}' DOCKER_PASS='${DOCKER_PASS}' bash -se" <<'EOF' - set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + ${SSH_USER}@${SSH_HOST} \ + "REGISTRY_HOST='${REGISTRY_HOST}' IMAGE_REPO='${IMAGE_REPO}' DOCKER_USER='${DOCKER_USER}' DOCKER_PASS='${DOCKER_PASS}' bash -se" <<'REMOTE_EOF' + set -e - REGISTRY_URL="https://${REGISTRY_HOST}" - REPO="${IMAGE_REPO}" - KEEP=5 - AUTH="${DOCKER_USER}:${DOCKER_PASS}" + REGISTRY_URL="https://${REGISTRY_HOST}" + REPO="${IMAGE_REPO}" + KEEP=5 + AUTH="${DOCKER_USER}:${DOCKER_PASS}" - command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) + command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) - echo "[1/6] Debug URL" - echo "REGISTRY_URL=${REGISTRY_URL}" - echo "TAGS_URL=${REGISTRY_URL}/v2/${REPO}/tags/list" + echo "[1/6] Debug URL" + echo "TAGS_URL=${REGISTRY_URL}/v2/${REPO}/tags/list" - echo "[2/6] Collect running tags to protect" - RUNNING_TAGS=$(docker ps --format '{{.Image}}' | \ - grep -E "^${REGISTRY_HOST}/${REPO}:" | \ - awk -F: '{print $2}' | sort -u || true) + echo "[2/6] Collect running tags to protect" + RUNNING_TAGS=$(docker ps --format '{{.Image}}' | \ + grep -E "^${REGISTRY_HOST}/${REPO}:" | \ + awk -F: '{print $2}' | sort -u || true) - echo "[3/6] Fetch tags from registry" - TAGS=$(curl -fsS -u "${AUTH}" "${REGISTRY_URL}/v2/${REPO}/tags/list" | jq -r '.tags[]?' || true) + echo "[3/6] Fetch tags from registry" + TAGS=$(curl -fsS -u "${AUTH}" "${REGISTRY_URL}/v2/${REPO}/tags/list" | jq -r '.tags[]?' || true) - if [ -z "$TAGS" ]; then - echo "No tags found in registry. Skip." - exit 0 - fi + if [ -z "$TAGS" ]; then + echo "No tags found in registry. Skip." + exit 0 + fi - echo "[4/6] Sort tags desc" - SORTED=$(echo "$TAGS" | sort -r) - KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) - DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) + echo "[4/6] Sort tags desc" + SORTED=$(echo "$TAGS" | sort -r) + KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) + DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) - echo "Keep tags:" - echo "$KEEP_TAGS" | sed 's/^/ - /' + echo "Keep tags:" + echo "$KEEP_TAGS" | sed 's/^/ - /' - echo "[5/6] Delete old tags except running tags" - for TAG in $DEL_CANDIDATES; do - if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then - echo "Skip running tag: ${REPO}:${TAG}" - continue - fi + echo "[5/6] Delete old tags except running tags" + for TAG in $DEL_CANDIDATES; do + if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then + echo "Skip running tag: ${REPO}:${TAG}" + continue + fi - echo "Deleting ${REPO}:${TAG}" + echo "Deleting ${REPO}:${TAG}" - DIGEST=$(curl -fsSI -u "${AUTH}" \ - -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ - "${REGISTRY_URL}/v2/${REPO}/manifests/${TAG}" \ - | awk -F': ' '/Docker-Content-Digest/ {print $2}' | tr -d '\r') + DIGEST=$(curl -fsSI -u "${AUTH}" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + "${REGISTRY_URL}/v2/${REPO}/manifests/${TAG}" \ + | awk -F': ' '/Docker-Content-Digest/ {print $2}' | tr -d '\r') - if [ -z "$DIGEST" ]; then - echo " digest not found. skip ${TAG}" - continue - fi + if [ -z "$DIGEST" ]; then + echo " digest not found. skip ${TAG}" + continue + fi - curl -fsS -u "${AUTH}" -X DELETE "${REGISTRY_URL}/v2/${REPO}/manifests/${DIGEST}" || true - done + curl -fsS -u "${AUTH}" -X DELETE "${REGISTRY_URL}/v2/${REPO}/manifests/${DIGEST}" || true + done - echo "[6/6] Garbage collect (reclaim disk)" - docker exec registry registry garbage-collect /etc/docker/registry/config.yml + echo "[6/6] Garbage collect (reclaim disk)" + docker exec registry registry garbage-collect /etc/docker/registry/config.yml - echo "Registry cleanup done." - EOF - ''' + echo "Registry cleanup done." + REMOTE_EOF + ''' } } } } + } } From 8d38ae1a9c084addd0a0975869a10bfdfca4f5e7 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 15:53:09 +0900 Subject: [PATCH 27/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 124 ++++++++++++------------------------------------ 1 file changed, 30 insertions(+), 94 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index eb8687c..8ad3f9b 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -213,22 +213,40 @@ pipeline { ${SSH_USER}@${SSH_HOST} \ 'set -e - echo "[0] Collect images used by running containers" - RUNNING_IMAGES=$(docker ps -q | xargs -r docker inspect --format="{{.Image}}" | sort -u) + REPO="${REGISTRY_HOST}/${IMAGE_REPO}" - echo "[1/3] Remove old registry images (except running ones)" - docker images '"${REGISTRY_HOST}/${IMAGE_REPO}"' --format "{{.ID}} {{.CreatedAt}}" | \ - sort -k2 -r | \ - awk "{print \$1}" | \ - while read IMAGE_ID; do - echo "$RUNNING_IMAGES" | grep -q "$IMAGE_ID" && continue - docker rmi -f "$IMAGE_ID" 2>/dev/null || true - done + echo "[0] Repo: $REPO" - echo "[2/3] Remove dangling images (safe)" + echo "[1] Collect running image refs (repo:tag) to protect" + RUNNING_REFS=$(docker ps --format "{{.Image}}" | grep -E "^${REPO}:" | sort -u || true) + + if [ -n "$RUNNING_REFS" ]; then + echo "Running refs (protected):" + echo "$RUNNING_REFS" | sed "s/^/ - /" + else + echo "No running refs found for $REPO (check if you run by digest or different name)" + fi + + echo "[2] Remove stopped containers (so they dont hold images)" + docker container prune -f + + echo "[3] List all local refs for repo" + ALL_REFS=$(docker images "$REPO" --format "{{.Repository}}:{{.Tag}}" | grep -v "" | sort -u || true) + + echo "[4] Remove repo images except protected running refs" + for REF in $ALL_REFS; do + if [ -n "$RUNNING_REFS" ] && echo "$RUNNING_REFS" | grep -qx "$REF"; then + echo "Skip running: $REF" + continue + fi + echo "Removing: $REF" + docker rmi -f "$REF" || true + done + + echo "[5] Remove dangling images" docker image prune -f - echo "[3/3] Remove unused volumes (safe)" + echo "[6] Remove unused volumes" docker volume prune -f ' ''' @@ -236,87 +254,5 @@ pipeline { } } - stage('12) Registry cleanup (keep last 5 + protect running)') { - steps { - withCredentials([usernamePassword( - credentialsId: DOCKER_CRED, - usernameVariable: 'DOCKER_USER', - passwordVariable: 'DOCKER_PASS' - )]) { - sshagent(['ssh-pjt']) { - sh '''#!/usr/bin/env bash - set -e - - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - ${SSH_USER}@${SSH_HOST} \ - "REGISTRY_HOST='${REGISTRY_HOST}' IMAGE_REPO='${IMAGE_REPO}' DOCKER_USER='${DOCKER_USER}' DOCKER_PASS='${DOCKER_PASS}' bash -se" <<'REMOTE_EOF' - set -e - - REGISTRY_URL="https://${REGISTRY_HOST}" - REPO="${IMAGE_REPO}" - KEEP=5 - AUTH="${DOCKER_USER}:${DOCKER_PASS}" - - command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) - - echo "[1/6] Debug URL" - echo "TAGS_URL=${REGISTRY_URL}/v2/${REPO}/tags/list" - - echo "[2/6] Collect running tags to protect" - RUNNING_TAGS=$(docker ps --format '{{.Image}}' | \ - grep -E "^${REGISTRY_HOST}/${REPO}:" | \ - awk -F: '{print $2}' | sort -u || true) - - echo "[3/6] Fetch tags from registry" - TAGS=$(curl -fsS -u "${AUTH}" "${REGISTRY_URL}/v2/${REPO}/tags/list" | jq -r '.tags[]?' || true) - - if [ -z "$TAGS" ]; then - echo "No tags found in registry. Skip." - exit 0 - fi - - echo "[4/6] Sort tags desc" - SORTED=$(echo "$TAGS" | sort -r) - KEEP_TAGS=$(echo "$SORTED" | head -n $KEEP) - DEL_CANDIDATES=$(echo "$SORTED" | tail -n +$((KEEP+1))) - - echo "Keep tags:" - echo "$KEEP_TAGS" | sed 's/^/ - /' - - echo "[5/6] Delete old tags except running tags" - for TAG in $DEL_CANDIDATES; do - if [ -n "$RUNNING_TAGS" ] && echo "$RUNNING_TAGS" | grep -qx "$TAG"; then - echo "Skip running tag: ${REPO}:${TAG}" - continue - fi - - echo "Deleting ${REPO}:${TAG}" - - DIGEST=$(curl -fsSI -u "${AUTH}" \ - -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ - "${REGISTRY_URL}/v2/${REPO}/manifests/${TAG}" \ - | awk -F': ' '/Docker-Content-Digest/ {print $2}' | tr -d '\r') - - if [ -z "$DIGEST" ]; then - echo " digest not found. skip ${TAG}" - continue - fi - - curl -fsS -u "${AUTH}" -X DELETE "${REGISTRY_URL}/v2/${REPO}/manifests/${DIGEST}" || true - done - - echo "[6/6] Garbage collect (reclaim disk)" - docker exec registry registry garbage-collect /etc/docker/registry/config.yml - - echo "Registry cleanup done." - REMOTE_EOF - ''' - } - } - } - } - - - } } From 06faa992673f5277ff173079fbd8e3a48610a9c2 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:06:01 +0900 Subject: [PATCH 28/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 8ad3f9b..a391414 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -217,37 +217,6 @@ pipeline { echo "[0] Repo: $REPO" - echo "[1] Collect running image refs (repo:tag) to protect" - RUNNING_REFS=$(docker ps --format "{{.Image}}" | grep -E "^${REPO}:" | sort -u || true) - - if [ -n "$RUNNING_REFS" ]; then - echo "Running refs (protected):" - echo "$RUNNING_REFS" | sed "s/^/ - /" - else - echo "No running refs found for $REPO (check if you run by digest or different name)" - fi - - echo "[2] Remove stopped containers (so they dont hold images)" - docker container prune -f - - echo "[3] List all local refs for repo" - ALL_REFS=$(docker images "$REPO" --format "{{.Repository}}:{{.Tag}}" | grep -v "" | sort -u || true) - - echo "[4] Remove repo images except protected running refs" - for REF in $ALL_REFS; do - if [ -n "$RUNNING_REFS" ] && echo "$RUNNING_REFS" | grep -qx "$REF"; then - echo "Skip running: $REF" - continue - fi - echo "Removing: $REF" - docker rmi -f "$REF" || true - done - - echo "[5] Remove dangling images" - docker image prune -f - - echo "[6] Remove unused volumes" - docker volume prune -f ' ''' } From 1f9399837ba8bf5eb407a6d4096c4825d3476c32 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:15:12 +0900 Subject: [PATCH 29/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index a391414..28b655a 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -209,15 +209,14 @@ pipeline { sshagent(['ssh-pjt']) { sh ''' set -e - ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - ${SSH_USER}@${SSH_HOST} \ - 'set -e + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${SSH_USER}@${SSH_HOST} ' + set -e - REPO="${REGISTRY_HOST}/${IMAGE_REPO}" + REPO='"${REGISTRY_HOST}/${IMAGE_REPO}"' - echo "[0] Repo: $REPO" + echo "[0] Repo: $REPO" - ' + ' ''' } } From 18863643eada636d5090ee8725d256bc411ffea5 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:20:59 +0900 Subject: [PATCH 30/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 28b655a..ea41628 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -212,10 +212,9 @@ pipeline { ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${SSH_USER}@${SSH_HOST} ' set -e - REPO='"${REGISTRY_HOST}/${IMAGE_REPO}"' - - echo "[0] Repo: $REPO" + Repo='"${REGISTRY_HOST}/${IMAGE_REPO}"' + echo "${Repo}" ' ''' } From 1967654d4a0ee0089291d75defc24e4eea7dc29a Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:22:21 +0900 Subject: [PATCH 31/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index ea41628..2bca6d4 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -215,6 +215,7 @@ pipeline { Repo='"${REGISTRY_HOST}/${IMAGE_REPO}"' echo "${Repo}" + docker images "$Repo" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') ' ''' } From 92d18791d62a9bd9c8fd9638a3e1bc2a5cef40c8 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:23:42 +0900 Subject: [PATCH 32/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 2bca6d4..dff08ca 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -214,8 +214,7 @@ pipeline { Repo='"${REGISTRY_HOST}/${IMAGE_REPO}"' - echo "${Repo}" - docker images "$Repo" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') + docker images "${Repo}" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') ' ''' } From 203d65ac36f2e6e8ac262799e70769d9a739a7ef Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:26:07 +0900 Subject: [PATCH 33/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index dff08ca..12650fd 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -214,7 +214,7 @@ pipeline { Repo='"${REGISTRY_HOST}/${IMAGE_REPO}"' - docker images "${Repo}" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') + docker images "${Repo}" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') | xargs -r docker rmi ' ''' } From d28f9eca2692bd78090cb1a900a9faa3028fd7a2 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:31:44 +0900 Subject: [PATCH 34/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 12650fd..8c47e22 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -83,8 +83,7 @@ pipeline { set -e echo "[1/2] docker registry login" - echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} \ - -u "$DOCKER_USER" --password-stdin + echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin echo "[2/2] docker image push" docker push ${REGISTRY_HOST}/${IMAGE_REPO}:${IMAGE_TAG} @@ -214,7 +213,14 @@ pipeline { Repo='"${REGISTRY_HOST}/${IMAGE_REPO}"' + echo "[1/3] Remove old image" docker images "${Repo}" --format '{{.Repository}}:{{.Tag}}' | grep -v -F -f <(docker ps --format '{{.Image}}') | xargs -r docker rmi + + echo "[2/3] Remove dangling images" + docker image prune -f + + echo "[3/3] Remove old build cache (safe-ish)" + docker builder prune -f || true ' ''' } From bbedb7a46d59acfd6638741bd658d025bbce1707 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:33:54 +0900 Subject: [PATCH 35/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=A0=95=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 8c47e22..bf0d869 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -142,16 +142,13 @@ pipeline { echo "[1/1] docker registry login" - ssh -p ${SSH_PORT} \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - ${SSH_USER}@${SSH_HOST} \ - 'set -e - REGISTRY_HOST='"${REGISTRY_HOST}"' - DOCKER_USER='"${DOCKER_USER}"' - DOCKER_PASS='"${DOCKER_PASS}"' + ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${SSH_USER}@${SSH_HOST} ' + set -e + REGISTRY_HOST='"${REGISTRY_HOST}"' + DOCKER_USER='"${DOCKER_USER}"' + DOCKER_PASS='"${DOCKER_PASS}"' - echo "$DOCKER_PASS" | docker login "$REGISTRY_HOST" -u "$DOCKER_USER" --password-stdin + echo "$DOCKER_PASS" | docker login "$REGISTRY_HOST" -u "$DOCKER_USER" --password-stdin ' ''' } From 494f18c451556927b279f2a4e35e18282941fa0e Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:43:50 +0900 Subject: [PATCH 36/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index bf0d869..3d7c29f 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -224,5 +224,27 @@ pipeline { } } + + stage('12) registry docker image cleanup') { + steps { + withCredentials([usernamePassword( + credentialsId: DOCKER_CRED, + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' + )]) { + sh ''' + set -e + + echo "[1/2] docker registry login" + echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin + + curl -u $REG_USER:$REG_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list + + docker logout ${REGISTRY_HOST} + ''' + } + } + } + } } From 87f382aaeca9e14fd44460f1e6f1585bf41d1efa Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 16:45:37 +0900 Subject: [PATCH 37/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 3d7c29f..f81c5f2 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -238,7 +238,7 @@ pipeline { echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin - curl -u $REG_USER:$REG_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list + curl -u DOCKER_USER:DOCKER_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list docker logout ${REGISTRY_HOST} ''' From 77454edd414dfd71d750d8875d3a292a4bf3cdb0 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:04:21 +0900 Subject: [PATCH 38/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index f81c5f2..3e8ed9a 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -238,7 +238,7 @@ pipeline { echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin - curl -u DOCKER_USER:DOCKER_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list + curl -u $DOCKER_USER:$DOCKER_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list docker logout ${REGISTRY_HOST} ''' From 68cb556370a0a2253fa4900fb56229eff9293fef Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:04:47 +0900 Subject: [PATCH 39/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 3e8ed9a..f1a5480 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -238,7 +238,7 @@ pipeline { echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin - curl -u $DOCKER_USER:$DOCKER_PASS https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list + curl -u "$DOCKER_USER":"$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list docker logout ${REGISTRY_HOST} ''' From b952a8d8414c6458a640390c3dd7a6a9a53ff360 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:27:23 +0900 Subject: [PATCH 40/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index f1a5480..0b95d38 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -238,7 +238,9 @@ pipeline { echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin - curl -u "$DOCKER_USER":"$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list + TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | sed -n 's/.*"tags":\[\(.*\)\].*/\1/p' | tr -d '"' | tr ',' '\n' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) + + echo "$TAGS_TO_DELETE" docker logout ${REGISTRY_HOST} ''' From 567d6587731c611097f422dc2b4292ec48a331aa Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:34:59 +0900 Subject: [PATCH 41/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 0b95d38..27f5dbb 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -224,6 +224,16 @@ pipeline { } } + stage('jq tools install') { + steps { + sh ''' + if ! command -v jq >/dev/null; then + sudo apt-get update -y + sudo apt-get install -y jq + fi + ''' + } + } stage('12) registry docker image cleanup') { steps { @@ -238,7 +248,7 @@ pipeline { echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin - TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | sed -n 's/.*"tags":\[\(.*\)\].*/\1/p' | tr -d '"' | tr ',' '\n' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) + TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) echo "$TAGS_TO_DELETE" From a956a94cbf2f47f40e36413517c8efb1667d04ca Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:38:08 +0900 Subject: [PATCH 42/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 27f5dbb..d983cf0 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -224,17 +224,6 @@ pipeline { } } - stage('jq tools install') { - steps { - sh ''' - if ! command -v jq >/dev/null; then - sudo apt-get update -y - sudo apt-get install -y jq - fi - ''' - } - } - stage('12) registry docker image cleanup') { steps { withCredentials([usernamePassword( @@ -245,6 +234,8 @@ pipeline { sh ''' set -e + command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) + echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin From af4b2d40a3d389fd2b3c081971e56052aab6b58e Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:39:16 +0900 Subject: [PATCH 43/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index d983cf0..1097b92 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -234,8 +234,6 @@ pipeline { sh ''' set -e - command -v jq >/dev/null 2>&1 || (sudo apt-get update -y && sudo apt-get install -y jq) - echo "[1/2] docker registry login" echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin From 355441c32284987c48dd8649e854071b4e9c0004 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:54:54 +0900 Subject: [PATCH 44/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 1097b92..6fbb727 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -239,7 +239,11 @@ pipeline { TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) - echo "$TAGS_TO_DELETE" + for TAG in $TAGS_TO_DELETE; do + DIGEST=$(curl -sI -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG | grep Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') + + echo "[DRY] $TAG -> $DIGEST" + done docker logout ${REGISTRY_HOST} ''' From 64382f67414a4cb17f8cbbef3880cc6d06ba56f5 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 4 Feb 2026 17:58:49 +0900 Subject: [PATCH 45/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index 6fbb727..e27640d 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -240,7 +240,7 @@ pipeline { TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) for TAG in $TAGS_TO_DELETE; do - DIGEST=$(curl -sI -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG | grep Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') + DIGEST=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" -D - https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG -o /dev/null | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') echo "[DRY] $TAG -> $DIGEST" done From 738562762226864f4f5d41782208c5837867c38a Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 09:24:05 +0900 Subject: [PATCH 46/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index e27640d..ad64445 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -240,7 +240,7 @@ pipeline { TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) for TAG in $TAGS_TO_DELETE; do - DIGEST=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" -D - https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG -o /dev/null | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') + DIGEST=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" -D - https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG -o /dev/null | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') echo "[DRY] $TAG -> $DIGEST" done From b5bf7d795c04da4bcdb02785d2909a2519bf9d90 Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 09:25:33 +0900 Subject: [PATCH 47/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index ad64445..fe8134c 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -234,8 +234,6 @@ pipeline { sh ''' set -e - echo "[1/2] docker registry login" - echo "$DOCKER_PASS" | docker login ${REGISTRY_HOST} -u "$DOCKER_USER" --password-stdin TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) @@ -245,7 +243,6 @@ pipeline { echo "[DRY] $TAG -> $DIGEST" done - docker logout ${REGISTRY_HOST} ''' } } From c3d65c8874ec99247c86d5894bb41d5ab716ff06 Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 09:57:38 +0900 Subject: [PATCH 48/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index fe8134c..d093e1f 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -240,7 +240,14 @@ pipeline { for TAG in $TAGS_TO_DELETE; do DIGEST=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" -D - https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG -o /dev/null | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') + if [ -z "$DIGEST" ]; then + echo "[SKIP] $TAG digest not found" + continue + fi + echo "[DRY] $TAG -> $DIGEST" + + curl -s -u "$DOCKER_USER:$DOCKER_PASS" -X DELETE "https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/${DIGEST}" -o /dev/null -w "HTTP %{http_code}\n" done ''' From cfd6ec852c7e044374806460335f55d9d3e041d2 Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 11:15:33 +0900 Subject: [PATCH 49/66] =?UTF-8?q?[api]=20Jenkinsfile.pjt=20-=20=EB=A0=88?= =?UTF-8?q?=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile.pjt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.pjt b/Jenkinsfile.pjt index d093e1f..f3dd9cc 100644 --- a/Jenkinsfile.pjt +++ b/Jenkinsfile.pjt @@ -234,9 +234,10 @@ pipeline { sh ''' set -e - + echo "[1/2] Remove registry old image tag list" TAGS_TO_DELETE=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/tags/list | jq -r '.tags[]' | grep -E '^[0-9]+' | sort -t- -k1,1n | head -n -5) + echo "[2/2] Remove registry old image sha list and delete registry" for TAG in $TAGS_TO_DELETE; do DIGEST=$(curl -s -u "$DOCKER_USER:$DOCKER_PASS" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" -D - https://${REGISTRY_HOST}/v2/${IMAGE_REPO}/manifests/$TAG -o /dev/null | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d $'\r') From 61eb97215e935b46a652de2141947897f2f9f408 Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 15:04:41 +0900 Subject: [PATCH 50/66] =?UTF-8?q?[api]=20logback=20-=20=EC=95=95=EC=B6=95?= =?UTF-8?q?=EC=95=88=ED=95=98=EA=B2=8C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-pjt.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/logback-pjt.xml b/src/main/resources/logback-pjt.xml index 07198af..d22c31d 100644 --- a/src/main/resources/logback-pjt.xml +++ b/src/main/resources/logback-pjt.xml @@ -21,7 +21,7 @@ - ${LOG_PATH}/%d{yyyy/MM}/app.%d{yyyy-MM-dd}.%i.log.gz + ${LOG_PATH}/%d{yyyy/MM}/app.%d{yyyy-MM-dd}.%i.log Date: Thu, 5 Feb 2026 16:45:08 +0900 Subject: [PATCH 51/66] =?UTF-8?q?[api]=20=ED=8F=AC=ED=8A=B8=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 2 +- src/main/resources/application-local.yaml | 2 -- src/main/resources/application.yaml | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index 771a4aa..db5791f 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -5,7 +5,7 @@ services: restart: always ports: - - "127.0.0.1:8106:8080" + - "127.0.0.1:8106:8106" env_file: - /srv/project/alist/env/api/.env diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index edd8d29..642f4b9 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -1,5 +1,3 @@ -server: - port: 8106 spring: application: name: api diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e4f4dcc..e9aeac8 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,4 +1,5 @@ server: + port: 8106 forward-headers-strategy: framework spring: application: From 5b905bb520ecd9a0b4da10ca104641b5826bc40d Mon Sep 17 00:00:00 2001 From: shindw Date: Thu, 5 Feb 2026 17:10:02 +0900 Subject: [PATCH 52/66] =?UTF-8?q?[api]=20=ED=8F=AC=ED=8A=B8=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index db5791f..d909d86 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -10,7 +10,6 @@ services: env_file: - /srv/project/alist/env/api/.env - # 🔴 핵심: 로그 볼륨 연결 volumes: - /srv/project/alist/logs/api/app:/logs @@ -30,7 +29,6 @@ services: retries: 10 start_period: 30s - # 🟡 보험: 도커 로그 폭주 방지 logging: driver: json-file options: From 454ddeb95a68fba4cae1caf58cd5829ad3f3b8f1 Mon Sep 17 00:00:00 2001 From: shindw Date: Fri, 6 Feb 2026 10:37:04 +0900 Subject: [PATCH 53/66] =?UTF-8?q?[api]=20=ED=8F=AC=ED=8A=B8=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index d909d86..12305ac 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -23,7 +23,7 @@ services: -Duser.timezone=Asia/Seoul healthcheck: - test: ["CMD-SHELL", "curl -fsS http://localhost:8080/actuator/health || exit 1"] + test: ["CMD-SHELL", "curl -fsS http://localhost:8106/actuator/health || exit 1"] interval: 10s timeout: 3s retries: 10 From 98da8b9c3030aab69f9c6110269390ce1cde54ba Mon Sep 17 00:00:00 2001 From: shindw Date: Mon, 9 Feb 2026 17:55:12 +0900 Subject: [PATCH 54/66] =?UTF-8?q?[api]=20=EB=A1=9C=EA=B7=B8=EB=B0=B1?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=A1=9C=EA=B7=B8=EB=B0=B1=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-local.xml | 2 +- src/main/resources/logback-pjt.xml | 28 ++++++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main/resources/logback-local.xml b/src/main/resources/logback-local.xml index eb719dd..b6cc10e 100644 --- a/src/main/resources/logback-local.xml +++ b/src/main/resources/logback-local.xml @@ -7,7 +7,7 @@ - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %clr(%d{HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n diff --git a/src/main/resources/logback-pjt.xml b/src/main/resources/logback-pjt.xml index d22c31d..10a6864 100644 --- a/src/main/resources/logback-pjt.xml +++ b/src/main/resources/logback-pjt.xml @@ -4,37 +4,29 @@ - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - + - - ${LOG_PATH}/app.log + ${LOG_PATH}/api.log - + - + - ${LOG_PATH}/%d{yyyy/MM}/app.%d{yyyy-MM-dd}.%i.log + ${LOG_PATH}/%d{yyyy/MM}/api.log-%d{yyyy-MM-dd-HHmmss}.%i - - 50MB - + + 50MB + 180 + true - %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS} | %-5level | %-15thread | %-40logger{40} | %msg%n From d40ec9488bc8262b3845b9b75cfead2482f62cbf Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 10 Feb 2026 09:28:30 +0900 Subject: [PATCH 55/66] =?UTF-8?q?[api]=20=EB=A1=9C=EA=B7=B8=EB=B0=B1?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=A1=9C=EA=B7=B8=EB=B0=B1=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-pjt.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/resources/logback-pjt.xml b/src/main/resources/logback-pjt.xml index 10a6864..83f287d 100644 --- a/src/main/resources/logback-pjt.xml +++ b/src/main/resources/logback-pjt.xml @@ -7,21 +7,21 @@ + ${LOG_PATH}/api.log - + - + - ${LOG_PATH}/%d{yyyy/MM}/api.log-%d{yyyy-MM-dd-HHmmss}.%i + ${LOG_PATH}/%d{yyyy/MM}/api.log-%d{yyyy-MM-dd}.%i - - 50MB + + 50MB + - 180 - true From 9bc499ab978bafde80bd5047fe16c1c58961cfbf Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 11 Feb 2026 11:36:41 +0900 Subject: [PATCH 56/66] =?UTF-8?q?[api]=20swagger=20=EC=85=8B=ED=8C=85?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 22 ++++++++++++++++++ .../com/alist/api/config/OpenApiConfig.java | 16 +++++++++++++ src/main/resources/application-local.yaml | 12 ++++++++++ src/main/resources/application-pjt.yaml | 14 +++++++++++ src/main/resources/application.yaml | 23 +++++++++++++++++++ src/main/resources/log4jdbc.log4j2.properties | 3 +++ src/main/resources/logback-local.xml | 22 +++++++++++++++++- src/main/resources/logback-pjt.xml | 16 ++++++++++++- 8 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/alist/api/config/OpenApiConfig.java create mode 100644 src/main/resources/log4jdbc.log4j2.properties diff --git a/build.gradle b/build.gradle index fecf192..e4a47e1 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,28 @@ dependencies { // Web 기본 implementation 'org.springframework.boot:spring-boot-starter-web' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // jdbc + implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' + + // MariaDB + implementation 'org.mariadb.jdbc:mariadb-java-client' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + + // MyBatis (Boot 3 전용) + implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' + + // validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // health + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/src/main/java/com/alist/api/config/OpenApiConfig.java b/src/main/java/com/alist/api/config/OpenApiConfig.java new file mode 100644 index 0000000..9a74fe0 --- /dev/null +++ b/src/main/java/com/alist/api/config/OpenApiConfig.java @@ -0,0 +1,16 @@ +package com.alist.api.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "A*List Api App", + version = "v1", + description = "A*List Api App 입니다." + ) +) +public class OpenApiConfig { +} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 642f4b9..c08debb 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -1,5 +1,17 @@ spring: application: name: api + datasource: + url: jdbc:log4jdbc:mariadb://121.160.234.222:3000/ALISTLMS?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_unicode_ci + username: alist_dev + password: 1qaz2wsx!@ + driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + logging: config: classpath:logback-local.xml + +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true \ No newline at end of file diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index 792502f..42cab57 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -1,5 +1,19 @@ spring: application: name: api + main: + banner-mode: log + datasource: + url: jdbc:log4jdbc:mariadb://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_unicode_ci + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + logging: config: classpath:logback-pjt.xml + +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e9aeac8..926d19e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,29 @@ server: port: 8106 forward-headers-strategy: framework + spring: application: name: api + +mybatis: + mapper-locations: classpath:mapper/**/*.xml + type-aliases-package: com.alist.api + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + +springdoc: + swagger-ui: + path: /swagger + api-docs: + path: /v3/api-docs + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always \ No newline at end of file diff --git a/src/main/resources/log4jdbc.log4j2.properties b/src/main/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..1b22fc5 --- /dev/null +++ b/src/main/resources/log4jdbc.log4j2.properties @@ -0,0 +1,3 @@ +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator +log4jdbc.drivers=org.mariadb.jdbc.Driver +log4jdbc.dump.sql.maxlinelength=0 \ No newline at end of file diff --git a/src/main/resources/logback-local.xml b/src/main/resources/logback-local.xml index b6cc10e..17ddb53 100644 --- a/src/main/resources/logback-local.xml +++ b/src/main/resources/logback-local.xml @@ -4,6 +4,9 @@ + + + @@ -11,12 +14,29 @@ + + + - + + + + + + + + + + + + + + + diff --git a/src/main/resources/logback-pjt.xml b/src/main/resources/logback-pjt.xml index 83f287d..772bdd7 100644 --- a/src/main/resources/logback-pjt.xml +++ b/src/main/resources/logback-pjt.xml @@ -36,7 +36,21 @@ - + + + + + + + + + + + + + + + From 5a59c78d839ebbf1deeb5d623efdcabdc501ec66 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 11 Feb 2026 17:04:06 +0900 Subject: [PATCH 57/66] =?UTF-8?q?[api]=20swagger=20=EC=85=8B=ED=8C=85?= =?UTF-8?q?=EC=A4=91=20-=20swagger=20login=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 ++ .../java/com/alist/api/ApiApplication.java | 2 + .../api/common/response/ApiResponse.java | 34 ++++++ .../api/common/response/ApiResponseCode.java | 53 +++++++++ .../api/common/utils/ApiKeyGenerator.java | 16 +++ .../com/alist/api/config/OpenApiConfig.java | 21 ++++ .../com/alist/api/config/SecurityConfig.java | 86 ++++++++++++++ .../exception/GlobalExceptionHandler.java | 105 ++++++++++++++++++ .../config/jwt/JwtAccessDeniedHandler.java | 29 +++++ .../jwt/JwtAuthenticationEntryPoint.java | 28 +++++ .../config/jwt/JwtAuthenticationFilter.java | 60 ++++++++++ .../api/config/jwt/JwtTokenProvider.java | 92 +++++++++++++++ .../api/config/properties/JwtProperties.java | 14 +++ .../api/modules/main/MainController.java | 14 +++ src/main/resources/application-local.yaml | 10 ++ src/main/resources/application-pjt.yaml | 10 ++ src/main/resources/application.yaml | 2 +- 17 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/alist/api/common/response/ApiResponse.java create mode 100644 src/main/java/com/alist/api/common/response/ApiResponseCode.java create mode 100644 src/main/java/com/alist/api/common/utils/ApiKeyGenerator.java create mode 100644 src/main/java/com/alist/api/config/SecurityConfig.java create mode 100644 src/main/java/com/alist/api/config/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/alist/api/config/jwt/JwtAccessDeniedHandler.java create mode 100644 src/main/java/com/alist/api/config/jwt/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/com/alist/api/config/jwt/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java create mode 100644 src/main/java/com/alist/api/config/properties/JwtProperties.java create mode 100644 src/main/java/com/alist/api/modules/main/MainController.java diff --git a/build.gradle b/build.gradle index e4a47e1..6c412ac 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,15 @@ dependencies { // Web 기본 implementation 'org.springframework.boot:spring-boot-starter-web' + // security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/alist/api/ApiApplication.java b/src/main/java/com/alist/api/ApiApplication.java index 50a4ea8..222e71e 100644 --- a/src/main/java/com/alist/api/ApiApplication.java +++ b/src/main/java/com/alist/api/ApiApplication.java @@ -2,8 +2,10 @@ package com.alist.api; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication +@ConfigurationPropertiesScan public class ApiApplication { public static void main(String[] args) { diff --git a/src/main/java/com/alist/api/common/response/ApiResponse.java b/src/main/java/com/alist/api/common/response/ApiResponse.java new file mode 100644 index 0000000..d0ff98a --- /dev/null +++ b/src/main/java/com/alist/api/common/response/ApiResponse.java @@ -0,0 +1,34 @@ +package com.alist.api.common.response; + +import lombok.Getter; +import org.springframework.http.ResponseEntity; + +@Getter +public class ApiResponse { + private T data; // 실제 데이터 + private String code; // 결과 코드 (SUCCESS, ERROR_001 등) + private String message; // 사용자 메시지 + + public ApiResponse(T data, String code, String message) { + this.data = data; + this.code = code; + this.message = message; + } + + public static ApiResponse body(ApiResponseCode code, Object... args) { + return new ApiResponse<>(null, code.code(), code.message(args)); + } + + public static ApiResponse body(T data, ApiResponseCode code, Object... args) { + return new ApiResponse<>(data, code.code(), code.message(args)); + } + + public static ResponseEntity> entity(ApiResponseCode code, Object... args) { + return ResponseEntity.status(code.httpStatus()).body(ApiResponse.body(code, args)); + } + + public static ResponseEntity> entity(T data, ApiResponseCode code, Object... args) { + return ResponseEntity.status(code.httpStatus()).body(ApiResponse.body(data, code, args)); + } + +} diff --git a/src/main/java/com/alist/api/common/response/ApiResponseCode.java b/src/main/java/com/alist/api/common/response/ApiResponseCode.java new file mode 100644 index 0000000..c515e84 --- /dev/null +++ b/src/main/java/com/alist/api/common/response/ApiResponseCode.java @@ -0,0 +1,53 @@ +package com.alist.api.common.response; + +import org.springframework.http.HttpStatus; + +import java.text.MessageFormat; + +public enum ApiResponseCode { + // ===== Common ===== + CODE_200 ("200", "성공", HttpStatus.OK), + + CODE_400 ("400", "잘못된 요청", HttpStatus.BAD_REQUEST), + CODE_401 ("401", "인증 필요 합니다.", HttpStatus.UNAUTHORIZED), + CODE_403 ("403", "접근 권한 필요 합니다.", HttpStatus.FORBIDDEN), + CODE_404 ("404", "페이지를 찾을 수 없습니다. 입력하신 주소가 올바른지 확인해주세요.", HttpStatus.NOT_FOUND), + CODE_405 ("405", "잘못된 요청입니다. 요청 방식을 확인해 주세요.", HttpStatus.METHOD_NOT_ALLOWED), + CODE_500 ("500", "요청을 처리하는 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + + // ===== Success detail ===== + CODE_2001("2001", "{0} 정보 조회에 성공하였습니다.", HttpStatus.OK), + CODE_2002("2002", "{0} 등록 되었습니다.", HttpStatus.CREATED), + CODE_2003("2003", "조회된 정보가 없습니다.", HttpStatus.OK), + CODE_2004("2004", "중복된 {0} 정보 입니다.", HttpStatus.CONFLICT), + + // ===== Client input errors ===== + // @Valid / 바인딩 / 타입미스매치 / JSON 파싱 실패 등은 다 여기로 + CODE_4001("4001", "입력값을 확인해주세요.", HttpStatus.BAD_REQUEST), + + // 필수 요청 파라미터 누락 + CODE_4003("4003", "필수 요청 파라미터가 누락되었습니다.", HttpStatus.BAD_REQUEST), + ; + + private final String code; + private final String message; + private final HttpStatus httpStatus; + + ApiResponseCode(String code, String message, HttpStatus httpStatus) { + this.code = code; + this.message = message; + this.httpStatus = httpStatus; + } + + public String code() { return code; } + + public String message() { return message; } + + public String message(Object... args) { + return MessageFormat.format(this.message, args); + } + + public HttpStatus httpStatus() { + return httpStatus; + } +} diff --git a/src/main/java/com/alist/api/common/utils/ApiKeyGenerator.java b/src/main/java/com/alist/api/common/utils/ApiKeyGenerator.java new file mode 100644 index 0000000..6b52442 --- /dev/null +++ b/src/main/java/com/alist/api/common/utils/ApiKeyGenerator.java @@ -0,0 +1,16 @@ +package com.alist.api.common.utils; + +import java.security.SecureRandom; +import java.util.Base64; + +public class ApiKeyGenerator { + + private static final SecureRandom secureRandom = new SecureRandom(); + private static final Base64.Encoder base64Encoder = Base64.getUrlEncoder().withoutPadding(); + + public static String userApiKeyProc() { + byte[] randomBytes = new byte[32]; // 256-bit + secureRandom.nextBytes(randomBytes); + return base64Encoder.encodeToString(randomBytes); + } +} diff --git a/src/main/java/com/alist/api/config/OpenApiConfig.java b/src/main/java/com/alist/api/config/OpenApiConfig.java index 9a74fe0..484ff1c 100644 --- a/src/main/java/com/alist/api/config/OpenApiConfig.java +++ b/src/main/java/com/alist/api/config/OpenApiConfig.java @@ -2,6 +2,11 @@ package com.alist.api.config; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @@ -13,4 +18,20 @@ import org.springframework.context.annotation.Configuration; ) ) public class OpenApiConfig { + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .components(new Components() + .addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme() + .name("Authorization") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + ) + ); + } } diff --git a/src/main/java/com/alist/api/config/SecurityConfig.java b/src/main/java/com/alist/api/config/SecurityConfig.java new file mode 100644 index 0000000..536c467 --- /dev/null +++ b/src/main/java/com/alist/api/config/SecurityConfig.java @@ -0,0 +1,86 @@ +package com.alist.api.config; + +import com.alist.api.config.jwt.JwtAccessDeniedHandler; +import com.alist.api.config.jwt.JwtAuthenticationEntryPoint; +import com.alist.api.config.jwt.JwtAuthenticationFilter; +import com.alist.api.config.jwt.JwtTokenProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Slf4j +@Configuration +@EnableWebSecurity +public class SecurityConfig { + @Value("${swagger.login.id}") + private String swaggerLoginId; + + @Value("${swagger.login.password}") + private String swaggerLoginPassword; + + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { + return new JwtAuthenticationFilter(jwtTokenProvider); + } + + @Bean + @Order(1) + public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher("/", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + .httpBasic(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()); + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain apiFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(ex -> ex + .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) + .accessDeniedHandler(new JwtAccessDeniedHandler()) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/", "/actuator/health", "/auth/**", "/api/user/signup").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public UserDetailsService userDetailsService() { + UserDetails alist = User.builder() + .username(swaggerLoginId) + .password(passwordEncoder().encode(swaggerLoginPassword)) + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(alist); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/alist/api/config/exception/GlobalExceptionHandler.java b/src/main/java/com/alist/api/config/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..e3f6cc6 --- /dev/null +++ b/src/main/java/com/alist/api/config/exception/GlobalExceptionHandler.java @@ -0,0 +1,105 @@ +package com.alist.api.config.exception; + +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.javassist.NotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + + log.warn("IllegalArgumentException: {}", e.getMessage()); + + return ApiResponse.entity(ApiResponseCode.CODE_400); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParam(MissingServletRequestParameterException e) { + + log.warn("Missing request parameter: {} (type={})", e.getParameterName(), e.getParameterType()); + + return ApiResponse.entity(ApiResponseCode.CODE_4003); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) { + + log.warn("Method not supported: {} (supported={})", e.getMethod(), e.getSupportedHttpMethods()); + + return ApiResponse.entity(ApiResponseCode.CODE_405); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity> handleNotFound(NotFoundException e) { + + log.warn("Not found: {}", e.getMessage()); + + return ApiResponse.entity(ApiResponseCode.CODE_404); + } + + // 입력오류 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleMethodArgumentNotValid(MethodArgumentNotValidException e) { + Map fieldErrors = new LinkedHashMap<>(); + + for (FieldError fe : e.getBindingResult().getFieldErrors()) { + fieldErrors.putIfAbsent(fe.getField(), fe.getDefaultMessage()); + } + + log.warn("Validation failed: {}", e.getMessage()); + + return ApiResponse.entity(fieldErrors, ApiResponseCode.CODE_4001); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { + + log.warn("Type mismatch: name={}, value={}", e.getName(), e.getValue()); + + return ApiResponse.entity(ApiResponseCode.CODE_4001); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleNotReadable(HttpMessageNotReadableException e) { + + log.warn("Unreadable message (json parse?) : {}", e.getMessage()); + + return ApiResponse.entity(ApiResponseCode.CODE_4001); + } + + @ExceptionHandler(BindException.class) + public ResponseEntity>> handleBindException(BindException e) { + Map fieldErrors = new LinkedHashMap<>(); + for (FieldError fe : e.getBindingResult().getFieldErrors()) { + fieldErrors.putIfAbsent(fe.getField(), fe.getDefaultMessage()); + } + log.warn("Bind failed: {}", e.getMessage()); + + return ApiResponse.entity(fieldErrors, ApiResponseCode.CODE_4001); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + + log.error("Unhandled exception occurred", e); + + return ApiResponse.entity(ApiResponseCode.CODE_500); + } +} diff --git a/src/main/java/com/alist/api/config/jwt/JwtAccessDeniedHandler.java b/src/main/java/com/alist/api/config/jwt/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..2f4f115 --- /dev/null +++ b/src/main/java/com/alist/api/config/jwt/JwtAccessDeniedHandler.java @@ -0,0 +1,29 @@ +package com.alist.api.config.jwt; + +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException e) throws IOException { + + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + ApiResponse body = ApiResponse.body(ApiResponseCode.CODE_403); + objectMapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/com/alist/api/config/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/alist/api/config/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..478aaac --- /dev/null +++ b/src/main/java/com/alist/api/config/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,28 @@ +package com.alist.api.config.jwt; + +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException { + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + ApiResponse body = ApiResponse.body(ApiResponseCode.CODE_401); + objectMapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/com/alist/api/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/alist/api/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..231cffb --- /dev/null +++ b/src/main/java/com/alist/api/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,60 @@ +package com.alist.api.config.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + try { + String token = resolveToken(request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + String userId = jwtTokenProvider.getUserId(token); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + SecurityContextHolder.clearContext(); + // 로깅은 여기서 해도 됨 (SLF4J) + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearer = request.getHeader(HttpHeaders.AUTHORIZATION); + if (bearer == null) return null; + + // "Bearer " 뒤 토큰만 추출 + if (bearer.startsWith("Bearer ")) { + String token = bearer.substring(7).trim(); + return token.isEmpty() ? null : token; + } + return null; + } +} diff --git a/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..a75701b --- /dev/null +++ b/src/main/java/com/alist/api/config/jwt/JwtTokenProvider.java @@ -0,0 +1,92 @@ +package com.alist.api.config.jwt; + +import com.alist.api.config.properties.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; + +@Component +public class JwtTokenProvider { + private final JwtProperties jwtProperties; + private final SecretKey secretKey; + + public JwtTokenProvider(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + this.secretKey = Keys.hmacShaKeyFor( + jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8) + ); + } + + /** 토큰생성 **/ + public String createToken(String userId) { + Date now = new Date(); + Date expiry = new Date( + now.getTime() + jwtProperties.getAccessTokenValiditySeconds() * 1000 + ); + + return Jwts.builder() + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + /* 엑세스 토큰 생성 */ + public String createAccessToken(long userTokenIdx, String role) { + Instant now = Instant.now(); + Instant expiry = now.plusSeconds(jwtProperties.getAccessTokenValiditySeconds()); + + return Jwts.builder() + .setSubject(String.valueOf(userTokenIdx)) + .claim("role", role) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(expiry)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + /* 리프레시 토큰 생성 */ + public String createRefreshToken(long userTokenIdx) { + Instant now = Instant.now(); + Instant expiry = now.plusSeconds(jwtProperties.getAccessTokenValiditySeconds()); + + return Jwts.builder() + .setSubject(String.valueOf(userTokenIdx)) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(expiry)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + /** 토큰에서 subject 추출 */ + public String getUserId(String token) { + return parseClaims(token).getSubject(); + } + + /** 토큰 검증 */ + public boolean validateToken(String token) { + try { + parseClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + private Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/com/alist/api/config/properties/JwtProperties.java b/src/main/java/com/alist/api/config/properties/JwtProperties.java new file mode 100644 index 0000000..dbda00a --- /dev/null +++ b/src/main/java/com/alist/api/config/properties/JwtProperties.java @@ -0,0 +1,14 @@ +package com.alist.api.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + String secret; + long accessTokenValiditySeconds; + long refreshTokenValiditySeconds; +} diff --git a/src/main/java/com/alist/api/modules/main/MainController.java b/src/main/java/com/alist/api/modules/main/MainController.java new file mode 100644 index 0000000..49c43e2 --- /dev/null +++ b/src/main/java/com/alist/api/modules/main/MainController.java @@ -0,0 +1,14 @@ +package com.alist.api.modules.main; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/") +public class MainController { + @GetMapping(value = "/") + public String index() { + return "redirect:/swagger-ui/index.html"; + } +} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index c08debb..159f684 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -10,6 +10,16 @@ spring: logging: config: classpath:logback-local.xml +jwt: + secret: "F9k3s!29dkF#1lP0X9QZx8eW!2m@0AbD" + access-token-validity-seconds: 3600 + refresh-token-validity-seconds: 2592000 + +swagger: + login: + id: alist + password: "1qaz2wsx!@" + springdoc: api-docs: enabled: true diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index 42cab57..33ef9c3 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -12,6 +12,16 @@ spring: logging: config: classpath:logback-pjt.xml +jwt: + secret: ${JWT_SECRET} + access-token-validity-seconds: 3600 + refresh-token-validity-seconds: 2592000 + +swagger: + login: + id: ${SWAGGER_ID} + password: ${SWAGGER_PASSWORD} + springdoc: api-docs: enabled: true diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 926d19e..fce5e21 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,7 +15,7 @@ mybatis: springdoc: swagger-ui: - path: /swagger + path: /swagger-ui.html api-docs: path: /v3/api-docs From aa6bb2b579115ac0d856c9dd25984913af050112 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 11 Feb 2026 17:16:07 +0900 Subject: [PATCH 58/66] =?UTF-8?q?[api]=20swagger=20=EC=85=8B=ED=8C=85?= =?UTF-8?q?=EC=A4=91=20-=20=EC=B2=B4=EC=9D=B8=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/alist/api/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/alist/api/config/SecurityConfig.java b/src/main/java/com/alist/api/config/SecurityConfig.java index 536c467..b64fd05 100644 --- a/src/main/java/com/alist/api/config/SecurityConfig.java +++ b/src/main/java/com/alist/api/config/SecurityConfig.java @@ -41,7 +41,7 @@ public class SecurityConfig { @Order(1) public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception { http - .securityMatcher("/", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .securityMatcher("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()); From cf892816169e46f1596421d0b061cee063e0d7f9 Mon Sep 17 00:00:00 2001 From: shindw Date: Wed, 11 Feb 2026 17:18:16 +0900 Subject: [PATCH 59/66] =?UTF-8?q?[api]=20swagger=20=EC=85=8B=ED=8C=85?= =?UTF-8?q?=EC=A4=91=20-=20=ED=8C=A8=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/alist/api/modules/main/MainController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/alist/api/modules/main/MainController.java b/src/main/java/com/alist/api/modules/main/MainController.java index 49c43e2..8a69cd6 100644 --- a/src/main/java/com/alist/api/modules/main/MainController.java +++ b/src/main/java/com/alist/api/modules/main/MainController.java @@ -2,11 +2,10 @@ package com.alist.api.modules.main; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; @Controller -@RequestMapping("/") public class MainController { + @GetMapping(value = "/") public String index() { return "redirect:/swagger-ui/index.html"; From bfa5166941cb24b71c2bb51e7ac50e7263f8d5b3 Mon Sep 17 00:00:00 2001 From: shindw Date: Mon, 23 Feb 2026 11:03:53 +0900 Subject: [PATCH 60/66] =?UTF-8?q?[api]=20claude.md=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ CLAUDE.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index c2065bc..72c96fc 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### Local only ### +gradle.properties diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6e9cc72 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md - alist API 프로젝트 + +## 프로젝트 개요 +- **프로젝트명**: alist API +- **그룹**: com.alist +- **포트**: 8106 +- **빌드 결과물**: `api.jar` + +## 기술 스택 +- **Java**: 21 +- **Framework**: Spring Boot 3.5.10 +- **빌드 도구**: Gradle +- **DB**: MariaDB +- **ORM**: MyBatis (mapper XML: `classpath:mapper/**/*.xml`) +- **인증**: JWT (jjwt 0.11.5) + Spring Security +- **API 문서**: Swagger (springdoc-openapi 2.6.0) +- **기타**: Lombok, Validation, Actuator, log4jdbc + +## 패키지 구조 +``` +com.alist.api +├── common +│ ├── response/ # ApiResponse, ApiResponseCode +│ └── utils/ # 공통 유틸리티 +├── config +│ ├── jwt/ # JWT 필터, 핸들러, Provider +│ ├── exception/ # GlobalExceptionHandler +│ ├── properties/ # JwtProperties 등 +│ ├── SecurityConfig.java +│ └── OpenApiConfig.java +└── modules + └── {도메인}/ # Controller, Service, Mapper, DTO +``` + +## 프로파일 +| 프로파일 | 설명 | +|---------|------| +| `local` | 로컬 개발 환경 | +| `pjt` | 프로젝트(운영) 환경 | + +## 보안 구조 +- **Swagger**: `/v3/api-docs/**`, `/swagger-ui/**` → HTTP Basic 인증 (InMemory) +- **API**: JWT Bearer 토큰 인증 (Stateless) +- **공개 경로**: `/`, `/actuator/health`, `/auth/**`, `/api/user/signup` + +## 빌드 및 실행 +```bash +# 빌드 +./gradlew bootJar + +# 로컬 실행 +./gradlew bootRun --args='--spring.profiles.active=local' + +# JAR 실행 +java -jar build/libs/api.jar --spring.profiles.active=local +``` + +## Swagger 접속 +- URL: `http://localhost:8106/swagger-ui.html` +- 인증: `swagger.login.id` / `swagger.login.password` (환경별 yaml에 설정) + +## 코드 작성 규칙 +- 응답은 `ApiResponse` 래퍼 사용 +- 응답 코드는 `ApiResponseCode` enum 사용 +- MyBatis Mapper XML은 `src/main/resources/mapper/` 하위에 작성 +- 카멜케이스 자동 변환 활성화 (`map-underscore-to-camel-case: true`) +- 새 모듈은 `modules/{도메인}/` 하위에 Controller, Service, Mapper, DTO 구성 + +## CI/CD +- **Jenkins**: `Jenkinsfile.pjt` +- **Docker**: `Dockerfile` +- **배포 스크립트**: `deploy/` 디렉토리 From 7067238dfd8fbe7874f2f093892140274742eb85 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 09:33:04 +0900 Subject: [PATCH 61/66] =?UTF-8?q?[api]=20claude=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- CLAUDE.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 72c96fc..c2065bc 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,3 @@ out/ ### VS Code ### .vscode/ - -### Local only ### -gradle.properties diff --git a/CLAUDE.md b/CLAUDE.md index 6e9cc72..9dbd1e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,3 +70,37 @@ java -jar build/libs/api.jar --spring.profiles.active=local - **Jenkins**: `Jenkinsfile.pjt` - **Docker**: `Dockerfile` - **배포 스크립트**: `deploy/` 디렉토리 + +## 개발 서버 (pjt) + +### 도메인 +- **API**: `api-alist.pjt.kr` +- **파일(업로드)**: `file-alist.pjt.kr` +- **Swagger**: `https://api-alist.pjt.kr/swagger-ui/index.html` + +### Docker +- **레지스트리**: `registry.pjt.kr` +- **이미지**: `registry.pjt.kr/alist/api` +- **컨테이너명**: `alist-api` +- **포트**: `127.0.0.1:8106->8106/tcp` + +### 서버 디렉토리 (`/srv/project/alist/`) +``` +/srv/project/alist/ +├── compose/ # docker-compose 파일 +├── data/ # 데이터 +├── env/ # 환경변수 파일 +├── logs/ # 로그 +├── scripts/ # 배포/운영 스크립트 +└── uploads/ # 업로드 파일 (file-alist.pjt.kr 루트) +``` + +### Nginx +- `file-alist.pjt.kr` → `/srv/project/alist/uploads` (정적 파일 서빙) +- HTTP(80) → HTTPS(301) 리다이렉트 +- SSL: Let's Encrypt +- 직접 접근 차단 (`allow 127.0.0.1; deny all;`) + +### 파일 업로드 +- 업로드 저장 경로: `/srv/project/alist/uploads/` +- 업로드 파일 접근 URL: `https://file-alist.pjt.kr/{파일경로}` From b59e6d54e466e584964f3fbb606a9392d7e95900 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 09:45:37 +0900 Subject: [PATCH 62/66] =?UTF-8?q?[api]=20claude.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9dbd1e0..56244a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,13 +36,35 @@ com.alist.api | 프로파일 | 설명 | |---------|------| | `local` | 로컬 개발 환경 | -| `pjt` | 프로젝트(운영) 환경 | +| `pjt` | 프로젝트(개발) 환경 | ## 보안 구조 - **Swagger**: `/v3/api-docs/**`, `/swagger-ui/**` → HTTP Basic 인증 (InMemory) - **API**: JWT Bearer 토큰 인증 (Stateless) - **공개 경로**: `/`, `/actuator/health`, `/auth/**`, `/api/user/signup` +## 응답 코드 규칙 + +`ApiResponseCode` enum으로 관리. 주요 코드: + +| 코드 | 메시지 | HTTP Status | 용도 | +|------|--------|-------------|------| +| `CODE_200` | 성공 | 200 OK | 일반 성공 | +| `CODE_400` | 잘못된 요청 | 400 Bad Request | 일반 클라이언트 오류 | +| `CODE_401` | 인증 필요 합니다. | 401 Unauthorized | 인증 없음 | +| `CODE_403` | 접근 권한 필요 합니다. | 403 Forbidden | 권한 없음 | +| `CODE_404` | 페이지를 찾을 수 없습니다. | 404 Not Found | 리소스 없음 | +| `CODE_405` | 잘못된 요청입니다. 요청 방식을 확인해 주세요. | 405 Method Not Allowed | 메서드 불일치 | +| `CODE_500` | 요청을 처리하는 중 오류가 발생했습니다. | 500 Internal Server Error | 서버 오류 | +| `CODE_2001` | {0} 정보 조회에 성공하였습니다. | 200 OK | 단건 조회 성공 | +| `CODE_2002` | {0} 등록 되었습니다. | 201 Created | 등록 성공 | +| `CODE_2003` | 조회된 정보가 없습니다. | 200 OK | 조회 결과 없음 | +| `CODE_2004` | 중복된 {0} 정보 입니다. | 409 Conflict | 중복 데이터 | +| `CODE_4001` | 입력값을 확인해주세요. | 400 Bad Request | `@Valid` / 바인딩 / 타입오류 / JSON 파싱 실패 | +| `CODE_4003` | 필수 요청 파라미터가 누락되었습니다. | 400 Bad Request | 필수 파라미터 누락 | + +- `{0}` 자리에 대상명 삽입 (예: `CODE_2001` → "회원 정보 조회에 성공하였습니다.") + ## 빌드 및 실행 ```bash # 빌드 @@ -64,7 +86,14 @@ java -jar build/libs/api.jar --spring.profiles.active=local - 응답 코드는 `ApiResponseCode` enum 사용 - MyBatis Mapper XML은 `src/main/resources/mapper/` 하위에 작성 - 카멜케이스 자동 변환 활성화 (`map-underscore-to-camel-case: true`) -- 새 모듈은 `modules/{도메인}/` 하위에 Controller, Service, Mapper, DTO 구성 +- 새 모듈 추가 시: `modules/{moduleName}/` 하위에 Controller, Service, Mapper, dto/, vo/ 구조로 생성 +- Mapper XML은 `resources/mapper/{moduleName}/` 에 위치 + +## MyBatis 규칙 +- Mapper XML 위치: `src/main/resources/mapper/**/*.xml` +- `map-underscore-to-camel-case: true` 설정 → DB 컬럼 `user_idx` → Java 필드 `userIdx` 자동 매핑 +- Mapper 인터페이스와 XML의 namespace, id 반드시 일치시킬 것 +- VO: DB 조회 결과 매핑용 / DTO: 서비스 레이어 간 데이터 전달용 / Form: 컨트롤러 입력 검증용 ## CI/CD - **Jenkins**: `Jenkinsfile.pjt` From 6e734f2b92ae1b97c7b0f39e8805857d19b066a0 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 12:02:23 +0900 Subject: [PATCH 63/66] =?UTF-8?q?[api]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../api/modules/auth/AuthController.java | 31 +++++++++++++++++++ .../api/modules/auth/form/TokenForm.java | 16 ++++++++++ .../com/alist/api/ApiApplicationTests.java | 2 ++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/alist/api/modules/auth/AuthController.java create mode 100644 src/main/java/com/alist/api/modules/auth/form/TokenForm.java diff --git a/build.gradle b/build.gradle index 6c412ac..b27da4a 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'org.mariadb.jdbc:mariadb-java-client' // swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0' // MyBatis (Boot 3 전용) implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' diff --git a/src/main/java/com/alist/api/modules/auth/AuthController.java b/src/main/java/com/alist/api/modules/auth/AuthController.java new file mode 100644 index 0000000..1fecc93 --- /dev/null +++ b/src/main/java/com/alist/api/modules/auth/AuthController.java @@ -0,0 +1,31 @@ +package com.alist.api.modules.auth; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import com.alist.api.config.jwt.JwtTokenProvider; +import com.alist.api.modules.auth.form.TokenForm; + +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/auth") +public class AuthController { + private final JwtTokenProvider jwtTokenProvider; + + public AuthController(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @PostMapping("/token") + public ResponseEntity> token(@Valid @RequestBody TokenForm tokenForm) { + String accessToken = jwtTokenProvider.createToken(tokenForm.getUserId()); + + return ApiResponse.entity(accessToken, ApiResponseCode.CODE_2001, "엑세스 토큰"); + } +} diff --git a/src/main/java/com/alist/api/modules/auth/form/TokenForm.java b/src/main/java/com/alist/api/modules/auth/form/TokenForm.java new file mode 100644 index 0000000..a5573e7 --- /dev/null +++ b/src/main/java/com/alist/api/modules/auth/form/TokenForm.java @@ -0,0 +1,16 @@ +package com.alist.api.modules.auth.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "토큰 요청 폼") +public class TokenForm { + + @NotBlank(message = "사용자명은 필수입니다.") + @Schema(description = "사용자명", example = "user123") + private String userId; +} \ No newline at end of file diff --git a/src/test/java/com/alist/api/ApiApplicationTests.java b/src/test/java/com/alist/api/ApiApplicationTests.java index 170e78d..bb6c87a 100644 --- a/src/test/java/com/alist/api/ApiApplicationTests.java +++ b/src/test/java/com/alist/api/ApiApplicationTests.java @@ -2,8 +2,10 @@ package com.alist.api; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("local") class ApiApplicationTests { @Test From f29340666111015fe390c653baf5b75685dcdb02 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 15:51:13 +0900 Subject: [PATCH 64/66] =?UTF-8?q?[api]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20use?= =?UTF-8?q?r=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alist/api/config/SecurityConfig.java | 2 +- .../modules/testUser/TestUserController.java | 39 +++++++++++ .../api/modules/testUser/dto/TestUserDto.java | 19 ++++++ .../testUser/dto/TestUserTokenDto.java | 14 ++++ .../testUser/form/TestUserSignupForm.java | 40 ++++++++++++ .../testUser/mapper/TestUserMapper.java | 14 ++++ .../testUser/service/TestUserService.java | 65 +++++++++++++++++++ .../api/modules/testUser/vo/TestUserVo.java | 8 +++ .../mapper/testUser/TestUserMapper.xml | 25 +++++++ 9 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/alist/api/modules/testUser/TestUserController.java create mode 100644 src/main/java/com/alist/api/modules/testUser/dto/TestUserDto.java create mode 100644 src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java create mode 100644 src/main/java/com/alist/api/modules/testUser/form/TestUserSignupForm.java create mode 100644 src/main/java/com/alist/api/modules/testUser/mapper/TestUserMapper.java create mode 100644 src/main/java/com/alist/api/modules/testUser/service/TestUserService.java create mode 100644 src/main/java/com/alist/api/modules/testUser/vo/TestUserVo.java create mode 100644 src/main/resources/mapper/testUser/TestUserMapper.xml diff --git a/src/main/java/com/alist/api/config/SecurityConfig.java b/src/main/java/com/alist/api/config/SecurityConfig.java index b64fd05..6f0fc9e 100644 --- a/src/main/java/com/alist/api/config/SecurityConfig.java +++ b/src/main/java/com/alist/api/config/SecurityConfig.java @@ -60,7 +60,7 @@ public class SecurityConfig { .accessDeniedHandler(new JwtAccessDeniedHandler()) ) .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/actuator/health", "/auth/**", "/api/user/signup").permitAll() + .requestMatchers("/", "/actuator/health", "/auth/**", "/api/testUser/signup").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/alist/api/modules/testUser/TestUserController.java b/src/main/java/com/alist/api/modules/testUser/TestUserController.java new file mode 100644 index 0000000..d6e8937 --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/TestUserController.java @@ -0,0 +1,39 @@ +package com.alist.api.modules.testUser; + +import com.alist.api.common.response.ApiResponse; +import com.alist.api.common.response.ApiResponseCode; +import com.alist.api.modules.testUser.dto.TestUserDto; +import com.alist.api.modules.testUser.form.TestUserSignupForm; +import com.alist.api.modules.testUser.service.TestUserService; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/api/testUser") +public class TestUserController { + + public final TestUserService testUserService; + + public TestUserController(TestUserService testUserService) { + this.testUserService = testUserService; + } + + @PostMapping("/signup") + public ResponseEntity> signup(@Valid @RequestBody TestUserSignupForm testUserSignupForm) { + + TestUserDto userDto = testUserService.insertTestUserProc(testUserSignupForm.testUserDto()); + + if (userDto.getResultCode() == 2004) { + return ApiResponse.entity(userDto, ApiResponseCode.CODE_2004, "아이디"); + } + + return ApiResponse.entity(userDto, ApiResponseCode.CODE_2002, "아이디"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/alist/api/modules/testUser/dto/TestUserDto.java b/src/main/java/com/alist/api/modules/testUser/dto/TestUserDto.java new file mode 100644 index 0000000..8e58b54 --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/dto/TestUserDto.java @@ -0,0 +1,19 @@ +package com.alist.api.modules.testUser.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TestUserDto { + private Integer userIdx; + private String id; + + @JsonIgnore + private String newPassword; + @JsonIgnore + private String password; + @JsonIgnore + private int resultCode; +} diff --git a/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java b/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java new file mode 100644 index 0000000..2e89ec7 --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/dto/TestUserTokenDto.java @@ -0,0 +1,14 @@ +package com.alist.api.modules.testUser.dto; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TestUserTokenDto { + private int userTokenIdx; + private int userIdx; + private String userApiKey; + private String userRole; + private int resultCode; +} diff --git a/src/main/java/com/alist/api/modules/testUser/form/TestUserSignupForm.java b/src/main/java/com/alist/api/modules/testUser/form/TestUserSignupForm.java new file mode 100644 index 0000000..9098cca --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/form/TestUserSignupForm.java @@ -0,0 +1,40 @@ +package com.alist.api.modules.testUser.form; + +import com.alist.api.modules.testUser.dto.TestUserDto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "회원 가입 요청 폼") +public class TestUserSignupForm { + @Schema( + description = "사용자 아이디 (공백 불가)", + example = "test" + ) + @NotBlank(message = "아이디를 입력해주세요.") + private String id; + + @Schema( + description = "비밀번호 (8~64자, 영문 + 숫자 조합, 공백 불가)", + example = "pass1234" + ) + @NotBlank(message = "비밀번호를 입력해주세요.") + @Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.") + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)\\S+$", + message = "비밀번호는 영문과 숫자를 포함하고 공백이 없어야 합니다." + ) + private String password; + + public TestUserDto testUserDto() { + TestUserDto testUserDto = new TestUserDto(); + testUserDto.setId(id.trim()); + testUserDto.setPassword(password); + return testUserDto; + } +} diff --git a/src/main/java/com/alist/api/modules/testUser/mapper/TestUserMapper.java b/src/main/java/com/alist/api/modules/testUser/mapper/TestUserMapper.java new file mode 100644 index 0000000..5ff3e0a --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/mapper/TestUserMapper.java @@ -0,0 +1,14 @@ +package com.alist.api.modules.testUser.mapper; + +import com.alist.api.modules.testUser.dto.TestUserDto; +import com.alist.api.modules.testUser.dto.TestUserTokenDto; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TestUserMapper { + int selectDuplicateTestUserCount(TestUserDto testUserDto); + + int insertTestUserSignup(TestUserDto testUserDto); + + int insertTestUserTokenSignup(TestUserTokenDto testUserTokenDto); +} diff --git a/src/main/java/com/alist/api/modules/testUser/service/TestUserService.java b/src/main/java/com/alist/api/modules/testUser/service/TestUserService.java new file mode 100644 index 0000000..6b4d4f6 --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/service/TestUserService.java @@ -0,0 +1,65 @@ +package com.alist.api.modules.testUser.service; + +import com.alist.api.common.utils.ApiKeyGenerator; +import com.alist.api.modules.testUser.dto.TestUserDto; +import com.alist.api.modules.testUser.dto.TestUserTokenDto; +import com.alist.api.modules.testUser.mapper.TestUserMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +public class TestUserService { + private final TestUserMapper testUserMapper; + private final PasswordEncoder passwordEncoder; + + public TestUserService(TestUserMapper testUserMapper, PasswordEncoder passwordEncoder) { + this.testUserMapper = testUserMapper; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public TestUserDto insertTestUserProc(TestUserDto testUserDto) { + // 중복체크 + if (testUserMapper.selectDuplicateTestUserCount(testUserDto) > 0) { + testUserDto.setResultCode(2004); + return testUserDto; + } + + // 비밀번호 생성 + testUserDto.setNewPassword(passwordEncoder.encode(testUserDto.getPassword())); + + // 회원정보 입력 + int result = testUserMapper.insertTestUserSignup(testUserDto); + + if (result > 0) { + // user_api_key 생성 + TestUserTokenDto testUserTokenDto = new TestUserTokenDto(); + testUserTokenDto.setUserIdx(testUserDto.getUserIdx()); + + apiKeyWithRetry(testUserTokenDto); + } + + log.info("cnt : " + testUserDto.getUserIdx()); + log.info("userIdx : " + testUserDto.getUserIdx()); + + return testUserDto; + } + + @Transactional + public int apiKeyWithRetry(TestUserTokenDto testUserTokenDto) { + for (int i = 0; i < 5; i++) { + testUserTokenDto.setUserApiKey(ApiKeyGenerator.userApiKeyProc()); + try { + return testUserMapper.insertTestUserTokenSignup(testUserTokenDto); + } catch (DuplicateKeyException e) { + // 충돌이면 다시 생성 + log.warn("Duplicate API key. retrying... userId={}", testUserTokenDto.getUserIdx()); + } + } + throw new IllegalStateException("API key generation failed after retries."); + } +} diff --git a/src/main/java/com/alist/api/modules/testUser/vo/TestUserVo.java b/src/main/java/com/alist/api/modules/testUser/vo/TestUserVo.java new file mode 100644 index 0000000..9d08a7b --- /dev/null +++ b/src/main/java/com/alist/api/modules/testUser/vo/TestUserVo.java @@ -0,0 +1,8 @@ +package com.alist.api.modules.testUser.vo; + +import lombok.Getter; + +@Getter +public class TestUserVo { + private String id; +} diff --git a/src/main/resources/mapper/testUser/TestUserMapper.xml b/src/main/resources/mapper/testUser/TestUserMapper.xml new file mode 100644 index 0000000..a0d5b56 --- /dev/null +++ b/src/main/resources/mapper/testUser/TestUserMapper.xml @@ -0,0 +1,25 @@ + + + + + + /*UserSignupMapper.insertTestUserSignup*/ + insert into test_user (id, password, del_yn, create_at, update_at) + value (#{id}, #{newPassword}, 1, now(), now()) + + + /*UserSignupMapper.insertTestUserTokenSignup*/ + insert into test_user_token (user_idx, user_api_key, user_role, created_at, updated_at) + value (#{userIdx}, #{userApiKey}, 'user', now(), now()) + + + + + From d8239f635ed646bc08ccab9a282541e41132c364 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 16:01:09 +0900 Subject: [PATCH 65/66] =?UTF-8?q?[api]=20=ED=98=B8=EC=8A=A4=ED=8A=B8=20db?= =?UTF-8?q?=20=EC=9D=B8=EC=8B=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/pjt/compose/api-compose.pjt.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deploy/pjt/compose/api-compose.pjt.yml b/deploy/pjt/compose/api-compose.pjt.yml index 12305ac..42ec4ca 100644 --- a/deploy/pjt/compose/api-compose.pjt.yml +++ b/deploy/pjt/compose/api-compose.pjt.yml @@ -4,6 +4,9 @@ services: image: registry.pjt.kr/alist/api:${IMAGE_TAG} restart: always + extra_hosts: + - "host.docker.internal:host-gateway" + ports: - "127.0.0.1:8106:8106" From 5367465027cfd9d978e6e5f19182b2f38a7ecfb5 Mon Sep 17 00:00:00 2001 From: shindw Date: Tue, 24 Feb 2026 16:54:40 +0900 Subject: [PATCH 66/66] =?UTF-8?q?[api]=20utf8mb4=5Fuca1400=5Fai=5Fci=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yaml | 2 +- src/main/resources/application-pjt.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 159f684..b9d418b 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -2,7 +2,7 @@ spring: application: name: api datasource: - url: jdbc:log4jdbc:mariadb://121.160.234.222:3000/ALISTLMS?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_unicode_ci + url: jdbc:log4jdbc:mariadb://121.160.234.222:3000/ALISTLMS?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_uca1400_ai_ci username: alist_dev password: 1qaz2wsx!@ driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index 33ef9c3..43e9bca 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -4,7 +4,7 @@ spring: main: banner-mode: log datasource: - url: jdbc:log4jdbc:mariadb://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_unicode_ci + url: jdbc:log4jdbc:mariadb://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul&connectionCollation=utf8mb4_uca1400_ai_ci username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy