From 958bdd5f88af11058f557432ea3b615e52e9dca5 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 13 Nov 2025 17:19:20 +0900 Subject: [PATCH 01/43] =?UTF-8?q?kubernetes=20yaml=ED=8C=8C=EC=9D=BC=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 --- app.yaml | 32 ++++++++++++++++++++++++++------ mysql.yaml | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app.yaml b/app.yaml index a39088e..e690823 100644 --- a/app.yaml +++ b/app.yaml @@ -19,22 +19,42 @@ spec: spec: containers: - name: springboot-app - image: my-spring-app:latest + image: springboot-app:latest ports: - containerPort: 8080 envFrom: - secretRef: name: db-secret - --- apiVersion: v1 kind: Service metadata: - name: springboot-app -spec: - selector: + labels: app: springboot-app +spec: ports: - - port: 8080 + - port: 80 + protocol: TCP targetPort: 8080 + selector: + app: springboot-app + sessionAffinity: None type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: springboot-ingress + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: springboot-app + port: + number: 80 diff --git a/mysql.yaml b/mysql.yaml index 8e9f918..b9ed1ff 100644 --- a/mysql.yaml +++ b/mysql.yaml @@ -31,7 +31,7 @@ spec: accessModes: ["ReadWriteOnce"] resources: requests: - storage: 5Gi + storage: 1Gi --- apiVersion: v1 kind: Service From 23121bbea7465dcf5b402dffedfc601788e9bc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:08:35 +0900 Subject: [PATCH 02/43] Add CI workflow for develop branch --- .github/workflows/develop.yml | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/develop.yml diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000..1ccb143 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,53 @@ +name: develop CI + +on: + pull_request: + branches: + - develop + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + # 코드 체크아웃 + - name: Checkout code + uses: actions/checkout@v3 + + # JDK 21 설정 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + + # Gradle wrapper 실행 권한 부여 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Gradle 캐시 + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper/ + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # application.yml + - name: Overwrite application.yml with secret + run: | + mkdir -p src/main/resources + cat < src/main/resources/application.yml + ${{ secrets.APPLICATION_YML }} + EOF + + # 빌드 & 테스트 + - name: Build and Test + run: ./gradlew clean build + + # 테스트 결과 출력 + - name: Show build artifacts + run: ls -l build/libs From 6f0a5477a8add2dc5a5cb709128f9dc81bc0585a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:16:42 +0900 Subject: [PATCH 03/43] Rename develop CI to pull request CI --- .github/workflows/develop.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 1ccb143..93a21fa 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -1,8 +1,9 @@ -name: develop CI +name: pull request CI on: pull_request: branches: + - main - develop jobs: From 3daa34510458801aab31958115ccca7b4f095d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:23:52 +0900 Subject: [PATCH 04/43] Modify CI workflow configuration file name --- .github/workflows/{develop.yml => CI.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{develop.yml => CI.yml} (100%) diff --git a/.github/workflows/develop.yml b/.github/workflows/CI.yml similarity index 100% rename from .github/workflows/develop.yml rename to .github/workflows/CI.yml From 83956195d02929e25144af46e3e7d7fc01b68dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:51:39 +0900 Subject: [PATCH 05/43] Add CD workflow for Docker and K3s deployment --- .github/workflows/CD.yml | 73 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/CD.yml diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml new file mode 100644 index 0000000..43f4aab --- /dev/null +++ b/.github/workflows/CD.yml @@ -0,0 +1,73 @@ +name: CD + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + # 코드 체크아웃 + - name: Checkout + uses: actions/checkout@v3 + + # DockerHub 로그인 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + # Docker 이미지 빌드 & 푸시 + - name: Build and Push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . + docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} + + # EC2 SSH → k3s에 Secret 생성 & Deployment 업데이트 + - name: Deploy to K3s via SSH + uses: appleboy/ssh-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd /home/ubuntu/skill-boost # k8s YAML 저장 경로 + + # -- db-secret 생성 -- + cat < Date: Thu, 20 Nov 2025 19:48:45 +0900 Subject: [PATCH 06/43] Add Dockerfile for Gradle build and deployment --- .github/workflows/Dockerfile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/Dockerfile diff --git a/.github/workflows/Dockerfile b/.github/workflows/Dockerfile new file mode 100644 index 0000000..556342b --- /dev/null +++ b/.github/workflows/Dockerfile @@ -0,0 +1,25 @@ +FROM gradle:8.8-jdk21 AS builder + +WORKDIR /app + +# Gradle 캐시 활용 +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle +RUN ./gradlew dependencies || true + +# 소스 코드 복사 +COPY . . + +# 빌드 +RUN chmod +x gradlew +RUN ./gradlew clean build -x test + +FROM amazoncorretto:21 + +WORKDIR /app + +COPY --from=builder /app/build/libs/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] From 5f303f300b18aeb1f0c0f3d2593f0d4b2895e978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:50:51 +0900 Subject: [PATCH 07/43] Delete .github/workflows/Dockerfile --- .github/workflows/Dockerfile | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/Dockerfile diff --git a/.github/workflows/Dockerfile b/.github/workflows/Dockerfile deleted file mode 100644 index 556342b..0000000 --- a/.github/workflows/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM gradle:8.8-jdk21 AS builder - -WORKDIR /app - -# Gradle 캐시 활용 -COPY build.gradle settings.gradle gradlew ./ -COPY gradle gradle -RUN ./gradlew dependencies || true - -# 소스 코드 복사 -COPY . . - -# 빌드 -RUN chmod +x gradlew -RUN ./gradlew clean build -x test - -FROM amazoncorretto:21 - -WORKDIR /app - -COPY --from=builder /app/build/libs/*.jar app.jar - -EXPOSE 8080 - -ENTRYPOINT ["java", "-jar", "app.jar"] From 29b3aea7b109c980eb26f3151efaa385b8fadf62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:51:21 +0900 Subject: [PATCH 08/43] Add Dockerfile for Gradle build and Java application --- Dockerfile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..556342b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM gradle:8.8-jdk21 AS builder + +WORKDIR /app + +# Gradle 캐시 활용 +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle +RUN ./gradlew dependencies || true + +# 소스 코드 복사 +COPY . . + +# 빌드 +RUN chmod +x gradlew +RUN ./gradlew clean build -x test + +FROM amazoncorretto:21 + +WORKDIR /app + +COPY --from=builder /app/build/libs/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] From 0ef4fcf17aa7821b130c24f781c1c454bc6988b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:01:34 +0900 Subject: [PATCH 09/43] Rename deployment and service to skill-boost-app --- app.yaml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app.yaml b/app.yaml index e690823..646e81d 100644 --- a/app.yaml +++ b/app.yaml @@ -1,12 +1,12 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: springboot-app + name: skill-boost-app spec: replicas: 2 selector: matchLabels: - app: springboot-app + app: skill-boost-app strategy: type: RollingUpdate rollingUpdate: @@ -15,36 +15,42 @@ spec: template: metadata: labels: - app: springboot-app + app: skill-boost-app spec: containers: - - name: springboot-app - image: springboot-app:latest + - name: skill-boost-app + image: skill-boost-app:latest + imagePullPolicy: Always ports: - containerPort: 8080 + env: + - name: SPRING_PROFILES_ACTIVE + value: "prod" envFrom: - secretRef: name: db-secret + - secretRef: + name: app-secret --- apiVersion: v1 kind: Service metadata: labels: - app: springboot-app + app: skill-boost-app spec: ports: - port: 80 protocol: TCP targetPort: 8080 selector: - app: springboot-app + app: skill-boost-app sessionAffinity: None type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: springboot-ingress + name: skill-boost-app-ingress annotations: kubernetes.io/ingress.class: traefik spec: @@ -55,6 +61,6 @@ spec: pathType: Prefix backend: service: - name: springboot-app + name: skill-boost-app port: number: 80 From f5283dcf66740ce07b171b9df089697bc9bace11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:03:33 +0900 Subject: [PATCH 10/43] Fix Docker password secret and enhance deployment process Updated Docker login password secret and improved deployment steps. --- .github/workflows/CD.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 43f4aab..c568560 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -19,7 +19,7 @@ jobs: uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + password: ${{ secrets.DOCKER_PASSWORD }} # Docker 이미지 빌드 & 푸시 - name: Build and Push Docker image @@ -35,7 +35,7 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | - cd /home/ubuntu/skill-boost # k8s YAML 저장 경로 + cd /home/ubuntu/skill-boost # -- db-secret 생성 -- cat < Date: Thu, 20 Nov 2025 21:11:22 +0900 Subject: [PATCH 11/43] Implement dynamic IP management for GitHub Actions Add steps to manage GitHub Actions IP in AWS security group --- .github/workflows/CD.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index c568560..012799f 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -27,6 +27,20 @@ jobs: docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} + # Github Actions IP 가져오기 + - name: Get Github Actions IP + id: ip + uses: haythem/public-ip@v1.2 + + # AWS 보안 그룹에 동적으로 IP 추가 + - name: Add Github Actions IP to Security group + run: | + aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ap-northeast-2 + # EC2 SSH → k3s에 Secret 생성 & Deployment 업데이트 - name: Deploy to K3s via SSH uses: appleboy/ssh-action@v0.1.7 @@ -75,3 +89,12 @@ jobs: # -- 배포 결과 확인 -- kubectl get pods -l app=skill-boost-app echo "✅ Deployment successful!" + + # AWS 보안 그룹에서 IP 제거 + - name: Remove Github Actions IP from security group + run: | + aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ap-northeast-2 From d1cdc54ea2f9f551f94fe762ffefda4d57b9388f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:46:51 +0900 Subject: [PATCH 12/43] Add conditional execution for IP removal step --- .github/workflows/CD.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 012799f..96a0fd8 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -92,6 +92,7 @@ jobs: # AWS 보안 그룹에서 IP 제거 - name: Remove Github Actions IP from security group + if: always() run: | aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 env: From f2e5278681c5c2c0b9186d0cce4862c30c8d1f63 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 20 Nov 2025 21:56:24 +0900 Subject: [PATCH 13/43] add application yml file --- src/main/resources/application-local.yml | 47 +++++++++++++++++++++++ src/main/resources/application-prod.yml | 28 ++++++++++++++ src/main/resources/application.properties | 1 - src/main/resources/application.yml | 3 ++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..dada28b --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,47 @@ +# 서버 포트 설정 +server: + port: 8080 + +# Spring Boot 애플리케이션 기본 설정 +spring: + application: + name: skill-boost + + # JPA 설정 (테이블 자동 생성을 위해 ddl-auto: update 추가) + jpa: + hibernate: + ddl-auto: update + # (선택사항) 실행되는 SQL을 로그로 보려면 주석 해제 + # show-sql: true + + # GitHub OAuth2 로그인 설정 + security: + oauth2: + client: + registration: + github: + client-id: Ov23liXAPa0etQe0EisI + client-secret: ${GITHUB_CLIENT_SECRET} # .env 파일에서 읽어옴 + scope: + - read:user + - user:email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id + +# SpringDoc (Swagger) 설정 +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + path: /swagger-ui.html + +# JWT 토큰 설정 +jwt: + secret-key: ${JWT_SECRET_KEY} # .env 파일에서 읽어옴 + expiration-ms: 86400000 # 토큰 만료 시간 (24시간) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..2da9ab5 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,28 @@ +application: + name: skill-boost + +jpa: + hibernate: + ddl-auto: none + +security: + oauth2: + client: + registration: + github: + client-id: Ov23liXAPa0etQe0EisI + client-secret: ${GITHUB_CLIENT_SECRET} + scope: + - read:user + - user:email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id + +jwt: + secret-key: ${JWT_SECRET_KEY} + expiration-ms: 86400000 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 666da9c..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=skill-boost diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ef46c2a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: local \ No newline at end of file From 1dff82935d8c0c8d8fbd6c3dc0c228500de6c3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:02:15 +0900 Subject: [PATCH 14/43] remove cd command Removed unnecessary directory change in deployment script. --- .github/workflows/CD.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 96a0fd8..bf29332 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -49,8 +49,6 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | - cd /home/ubuntu/skill-boost - # -- db-secret 생성 -- cat < Date: Thu, 20 Nov 2025 22:09:27 +0900 Subject: [PATCH 15/43] CD comment out --- .github/workflows/CD.yml | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index bf29332..1c40f18 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -27,19 +27,19 @@ jobs: docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} - # Github Actions IP 가져오기 - - name: Get Github Actions IP - id: ip - uses: haythem/public-ip@v1.2 - - # AWS 보안 그룹에 동적으로 IP 추가 - - name: Add Github Actions IP to Security group - run: | - aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ap-northeast-2 +# # Github Actions IP 가져오기 +# - name: Get Github Actions IP +# id: ip +# uses: haythem/public-ip@v1.2 +# +# # AWS 보안 그룹에 동적으로 IP 추가 +# - name: Add Github Actions IP to Security group +# run: | +# aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 +# env: +# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} +# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +# AWS_DEFAULT_REGION: ap-northeast-2 # EC2 SSH → k3s에 Secret 생성 & Deployment 업데이트 - name: Deploy to K3s via SSH @@ -89,11 +89,11 @@ jobs: echo "✅ Deployment successful!" # AWS 보안 그룹에서 IP 제거 - - name: Remove Github Actions IP from security group - if: always() - run: | - aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ap-northeast-2 +# - name: Remove Github Actions IP from security group +# if: always() +# run: | +# aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SECRET_GROUP_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32 +# env: +# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} +# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +# AWS_DEFAULT_REGION: ap-northeast-2 From 747cceea9c0145a90d7ccfcf3bd77011b02b358e Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 20 Nov 2025 22:15:56 +0900 Subject: [PATCH 16/43] add test controller --- .../java/com/example/skillboost/TestController.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/example/skillboost/TestController.java diff --git a/src/main/java/com/example/skillboost/TestController.java b/src/main/java/com/example/skillboost/TestController.java new file mode 100644 index 0000000..d8e061c --- /dev/null +++ b/src/main/java/com/example/skillboost/TestController.java @@ -0,0 +1,12 @@ +package com.example.skillboost; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + @GetMapping("/") + public String hello() { + return "Hello, World"; + } +} From 082b9d6373a4e6647d0de54f794aaa75a83a85b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:21:31 +0900 Subject: [PATCH 17/43] Add workflow_dispatch trigger to CD workflow --- .github/workflows/CD.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 1c40f18..6778f51 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,6 +1,7 @@ name: CD on: + workflow_dispatch: push: branches: - main From 20803c67bdd107efc8c3713213ee97c259266e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B0=EC=98=81=EB=B9=88?= <90145556+byb0823@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:31:05 +0900 Subject: [PATCH 18/43] Update Docker image name in app.yaml --- .github/workflows/CD.yml | 1 + app.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 6778f51..1881660 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -78,6 +78,7 @@ jobs: EOF # -- Deployment 이미지 업데이트 (자동으로 롤링 업데이트 시작) -- + kubectl apply -f app.yaml kubectl set image deployment/skill-boost-app \ skill-boost-app=${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} \ --record diff --git a/app.yaml b/app.yaml index 646e81d..137516a 100644 --- a/app.yaml +++ b/app.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: skill-boost-app - image: skill-boost-app:latest + image: skill-boost:latest imagePullPolicy: Always ports: - containerPort: 8080 From ce16e0e790f4d738ca5feb3878f18463edba2e0a Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 20 Nov 2025 23:35:48 +0900 Subject: [PATCH 19/43] modify k3s yaml file --- .github/workflows/CD.yml | 18 +++++++++++++----- app.yaml => k8s/app.yaml | 0 mysql.yaml => k8s/mysql.yaml | 0 3 files changed, 13 insertions(+), 5 deletions(-) rename app.yaml => k8s/app.yaml (100%) rename mysql.yaml => k8s/mysql.yaml (100%) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 1881660..291f775 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -28,6 +28,16 @@ jobs: docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} + # EC2로 yaml 파일 복사 + - name: Copy k8s manifests to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + source: "k8s/*.yaml" + target: "/home/${{ secrets.EC2_USER }}/k8s-manifests" + # # Github Actions IP 가져오기 # - name: Get Github Actions IP # id: ip @@ -77,11 +87,9 @@ jobs: GITHUB_CLIENT_SECRET: "${{ secrets.GITHUB_CLIENT_SECRET }}" EOF - # -- Deployment 이미지 업데이트 (자동으로 롤링 업데이트 시작) -- - kubectl apply -f app.yaml - kubectl set image deployment/skill-boost-app \ - skill-boost-app=${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} \ - --record + # -- 매니페스트 파일 적용 -- + cd /home/${{ secrets.EC2_USER }}/k8s-manifests/k8s + kubectl apply -f . # -- 롤링 업데이트 완료 대기 -- kubectl rollout status deployment/skill-boost-app --timeout=5m diff --git a/app.yaml b/k8s/app.yaml similarity index 100% rename from app.yaml rename to k8s/app.yaml diff --git a/mysql.yaml b/k8s/mysql.yaml similarity index 100% rename from mysql.yaml rename to k8s/mysql.yaml From 4fd2ddb658a87a576af18c403975b738a26e109b Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 20 Nov 2025 23:46:24 +0900 Subject: [PATCH 20/43] modify app.yaml --- k8s/app.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/k8s/app.yaml b/k8s/app.yaml index 137516a..cd75421 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: skill-boost-app + namespace: default spec: replicas: 2 selector: @@ -35,6 +36,8 @@ spec: apiVersion: v1 kind: Service metadata: + name: skill-boost-service + namespace: default labels: app: skill-boost-app spec: @@ -51,9 +54,9 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: skill-boost-app-ingress - annotations: - kubernetes.io/ingress.class: traefik + namespace: default spec: + ingressClassName: traefik rules: - http: paths: @@ -61,6 +64,6 @@ spec: pathType: Prefix backend: service: - name: skill-boost-app + name: skill-boost-service port: number: 80 From ee5fa2aaa63e32ad0728012350b06f07d88707bc Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 00:17:25 +0900 Subject: [PATCH 21/43] modify app.yaml --- .github/workflows/CD.yml | 16 ++++++++++++++++ k8s/app.yaml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 291f775..55033a4 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -38,6 +38,22 @@ jobs: source: "k8s/*.yaml" target: "/home/${{ secrets.EC2_USER }}/k8s-manifests" + # SSH 접속 후 app.yaml의 username 치환 + - name: Replace in app.yaml on EC2 + uses: appleboy/ssh-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + # k8s 매니페스트 디렉토리 + MANIFEST_DIR="/home/${{ secrets.EC2_USER }}/k8s-manifests/k8s" + + # USERNAME을 GitHub Secret 값으로 치환 + sed -i "s|USERNAME|${{ secrets.DOCKER_USERNAME }}|g" "$MANIFEST_DIR/app.yaml" + + echo "✅ app.yaml의 USERNAME 치환 완료" + # # Github Actions IP 가져오기 # - name: Get Github Actions IP # id: ip diff --git a/k8s/app.yaml b/k8s/app.yaml index cd75421..b01e66c 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -20,7 +20,7 @@ spec: spec: containers: - name: skill-boost-app - image: skill-boost:latest + image: USERNAME/skill-boost:latest imagePullPolicy: Always ports: - containerPort: 8080 From e8c92b6f8f49c98b8536219613e968444e0f720e Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 00:56:37 +0900 Subject: [PATCH 22/43] modify mysql.yaml --- .github/workflows/CD.yml | 8 +++++--- k8s/mysql.yaml | 1 + src/main/resources/application-prod.yml | 8 +++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 55033a4..4c68aa1 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -85,9 +85,10 @@ jobs: namespace: default type: Opaque stringData: - DB_URL: "${{ secrets.DB_URL }}" - DB_USERNAME: "${{ secrets.DB_USERNAME }}" - DB_PASSWORD: "${{ secrets.DB_PASSWORD }}" + MYSQL_ROOT_PASSWORD: "${{ secrets.DB_PASSWORD }}" + MYSQL_DATABASE: "${{ secrets.DB }}" + MYSQL_USER: "${{ secrets.DB_USERNAME }}" + MYSQL_PASSWORD: "${{ secrets.DB_PASSWORD }}" EOF # -- app-secret 생성 -- @@ -99,6 +100,7 @@ jobs: namespace: default type: Opaque stringData: + MYSQL_URL: "${{ secrets.MYSQL_URL }}" JWT_SECRET_KEY: "${{ secrets.JWT_SECRET_KEY }}" GITHUB_CLIENT_SECRET: "${{ secrets.GITHUB_CLIENT_SECRET }}" EOF diff --git a/k8s/mysql.yaml b/k8s/mysql.yaml index b9ed1ff..6bd13ec 100644 --- a/k8s/mysql.yaml +++ b/k8s/mysql.yaml @@ -37,6 +37,7 @@ apiVersion: v1 kind: Service metadata: name: mysql + namespace: default spec: selector: app: mysql diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2da9ab5..7a0f91a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,6 +1,12 @@ application: name: skill-boost +spring: + datasource: + url: ${MYSQL_URL} + username: ${MYSQL_USER} + password: ${MYSQL_PASSWORD} + jpa: hibernate: ddl-auto: none @@ -10,7 +16,7 @@ security: client: registration: github: - client-id: Ov23liXAPa0etQe0EisI + client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET} scope: - read:user From 73b44a5aa07c714608c96d89beaa51a7dca258c6 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 01:05:48 +0900 Subject: [PATCH 23/43] modify cd --- .github/workflows/CD.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 4c68aa1..c6638b6 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -76,6 +76,9 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | + # 기존 db-secret 삭제 + kubectl delete secret db-secret + # -- db-secret 생성 -- cat < Date: Fri, 21 Nov 2025 01:19:32 +0900 Subject: [PATCH 24/43] modify CD.yaml --- .github/workflows/CD.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index c6638b6..5e7c202 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -25,8 +25,8 @@ jobs: # Docker 이미지 빌드 & 푸시 - name: Build and Push Docker image run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . - docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} + docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:latest # EC2로 yaml 파일 복사 - name: Copy k8s manifests to EC2 From 0071494659274e1f2ec3c9896975d6099c5048a9 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 02:02:28 +0900 Subject: [PATCH 25/43] modify CD.yaml --- .github/workflows/CD.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 5e7c202..0d6340b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -121,6 +121,9 @@ jobs: # -- 배포 결과 확인 -- kubectl get pods -l app=skill-boost-app echo "✅ Deployment successful!" + + # -- 사용하지 않는 이미지 삭제 -- + sudo k3s ctr images prune -f # AWS 보안 그룹에서 IP 제거 # - name: Remove Github Actions IP from security group From 85d8b0d48262a1faa19203cb7b154f57cfdc3b87 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 02:13:53 +0900 Subject: [PATCH 26/43] modify hello controller --- src/main/java/com/example/skillboost/TestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/skillboost/TestController.java b/src/main/java/com/example/skillboost/TestController.java index d8e061c..cf0db2b 100644 --- a/src/main/java/com/example/skillboost/TestController.java +++ b/src/main/java/com/example/skillboost/TestController.java @@ -7,6 +7,6 @@ public class TestController { @GetMapping("/") public String hello() { - return "Hello, World"; + return "Hello"; } } From 8067a1879d6e40c80f80e035863a665c84cde10a Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 02:20:05 +0900 Subject: [PATCH 27/43] modify CD.yml --- .github/workflows/CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 0d6340b..952526f 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -123,7 +123,7 @@ jobs: echo "✅ Deployment successful!" # -- 사용하지 않는 이미지 삭제 -- - sudo k3s ctr images prune -f + sudo k3s ctr images prune --all # AWS 보안 그룹에서 IP 제거 # - name: Remove Github Actions IP from security group From d9dda047410bbcd965d59d564d3de4b9b166656e Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 02:29:51 +0900 Subject: [PATCH 28/43] modify image name --- .github/workflows/CD.yml | 12 ++++++------ k8s/app.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 952526f..811403b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -25,8 +25,8 @@ jobs: # Docker 이미지 빌드 & 푸시 - name: Build and Push Docker image run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:latest + docker build -t ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} . + docker push ${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }} # EC2로 yaml 파일 복사 - name: Copy k8s manifests to EC2 @@ -38,8 +38,8 @@ jobs: source: "k8s/*.yaml" target: "/home/${{ secrets.EC2_USER }}/k8s-manifests" - # SSH 접속 후 app.yaml의 username 치환 - - name: Replace in app.yaml on EC2 + # SSH 접속 후 app.yaml의 IMAGE 치환 + - name: Replace IMAGE in app.yaml on EC2 uses: appleboy/ssh-action@v0.1.7 with: host: ${{ secrets.EC2_HOST }} @@ -49,8 +49,8 @@ jobs: # k8s 매니페스트 디렉토리 MANIFEST_DIR="/home/${{ secrets.EC2_USER }}/k8s-manifests/k8s" - # USERNAME을 GitHub Secret 값으로 치환 - sed -i "s|USERNAME|${{ secrets.DOCKER_USERNAME }}|g" "$MANIFEST_DIR/app.yaml" + # IMAGE를 GitHub Secret 값으로 치환 + sed -i "s|IMAGE|${{ secrets.DOCKER_USERNAME }}/skill-boost:${{ github.sha }}|g" "$MANIFEST_DIR/app.yaml" echo "✅ app.yaml의 USERNAME 치환 완료" diff --git a/k8s/app.yaml b/k8s/app.yaml index b01e66c..f28a1aa 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -20,7 +20,7 @@ spec: spec: containers: - name: skill-boost-app - image: USERNAME/skill-boost:latest + image: IMAGE imagePullPolicy: Always ports: - containerPort: 8080 From c9633ce78e3593ebe110e900f9f8bf948bf991d1 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 02:34:02 +0900 Subject: [PATCH 29/43] change test controller return value --- .github/workflows/CD.yml | 1 - src/main/java/com/example/skillboost/TestController.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 811403b..2e5c737 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,7 +1,6 @@ name: CD on: - workflow_dispatch: push: branches: - main diff --git a/src/main/java/com/example/skillboost/TestController.java b/src/main/java/com/example/skillboost/TestController.java index cf0db2b..7f8f67b 100644 --- a/src/main/java/com/example/skillboost/TestController.java +++ b/src/main/java/com/example/skillboost/TestController.java @@ -7,6 +7,6 @@ public class TestController { @GetMapping("/") public String hello() { - return "Hello"; + return "Hello World!"; } } From 4f7eebd6bf43fb0969732135541dbf912385cd43 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 15:08:38 +0900 Subject: [PATCH 30/43] =?UTF-8?q?application=20yml=ED=8C=8C=EC=9D=BC=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 | 1 + src/main/resources/application-prod.yml | 46 ++++++++++++------------- src/main/resources/application-test.yml | 32 +++++++++++++++++ src/main/resources/application.yml | 4 +-- 4 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/application-test.yml diff --git a/build.gradle b/build.gradle index 74c055c..710349c 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7a0f91a..f7044b2 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,33 +1,33 @@ -application: - name: skill-boost - spring: + application: + name: skill-boost + datasource: url: ${MYSQL_URL} username: ${MYSQL_USER} password: ${MYSQL_PASSWORD} -jpa: - hibernate: - ddl-auto: none + jpa: + hibernate: + ddl-auto: none -security: - oauth2: - client: - registration: - github: - client-id: ${GITHUB_CLIENT_ID} - client-secret: ${GITHUB_CLIENT_SECRET} - scope: - - read:user - - user:email - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - provider: - github: - authorization-uri: https://github.com/login/oauth/authorize - token-uri: https://github.com/login/oauth/access_token - user-info-uri: https://api.github.com/user - user-name-attribute: id + security: + oauth2: + client: + registration: + github: + client-id: ${GITHUB_CLIENT_ID} + client-secret: ${GITHUB_CLIENT_SECRET} + scope: + - read:user + - user:email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id jwt: secret-key: ${JWT_SECRET_KEY} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..105917c --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,32 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL + driver-class-name: + username: sa + password: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create-drop + database-platform: org.hibernate.dialect.H2Dialect + + security: + oauth2: + client: + registration: + github: + client-id: test + client-secret: test + scope: + - read:user + - user:email + provider: + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id + +jwt: + secret-key: test-secret + expiration-ms: 100000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ef46c2a..c22d980 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: - profiles: - active: local \ No newline at end of file + application: + name: skill-boost \ No newline at end of file From 188a6866c64a8fac888710de47a0d931d7d079b7 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Fri, 21 Nov 2025 15:09:08 +0900 Subject: [PATCH 31/43] =?UTF-8?q?CI/CD=20yml=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CD.yml | 2 +- .github/workflows/CI.yml | 57 ++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 2e5c737..64ac456 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -12,7 +12,7 @@ jobs: steps: # 코드 체크아웃 - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # DockerHub 로그인 - name: Login to DockerHub diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 93a21fa..6670844 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,42 +13,47 @@ jobs: steps: # 코드 체크아웃 - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # JDK 21 설정 - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: temurin - java-version: 21 + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + # 도커 컴포즈 + - name: Start docker-compose + run: | + docker-compose up -d + + # MySQL이 ready 될 때까지 대기 (최대 30초) + timeout 30 bash -c 'until docker-compose exec -T mysql mysqladmin ping -h localhost --silent; do sleep 1; done' + + echo "MySQL is ready!" # Gradle wrapper 실행 권한 부여 - name: Grant execute permission for gradlew run: chmod +x gradlew - # Gradle 캐시 - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper/ - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - # application.yml - - name: Overwrite application.yml with secret - run: | - mkdir -p src/main/resources - cat < src/main/resources/application.yml - ${{ secrets.APPLICATION_YML }} - EOF - # 빌드 & 테스트 - name: Build and Test run: ./gradlew clean build + env: + SPRING_PROFILES_ACTIVE: test + + # 테스트 결과 업로드 + - name: Upload Test Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + build/reports/tests/test/ + build/test-results/test/ - # 테스트 결과 출력 - - name: Show build artifacts - run: ls -l build/libs + # 도커 컴포즈 종료 + - name: Docker Compose Down + if: always() + run: docker-compose down -v \ No newline at end of file From c97e5c8dc2635055fc6133356def3209ebbeb683 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 16:53:43 +0900 Subject: [PATCH 32/43] =?UTF-8?q?build:=20Gradle=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80(Security,=20OAuth2,=20JWT,=20Swa?= =?UTF-8?q?gger)(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 710349c..9394c53 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,14 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + } tasks.named('test') { From 8304e91bb473cfaee9f257f05659bc13aa301043 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 16:55:44 +0900 Subject: [PATCH 33/43] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B5=AC=ED=98=84=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/skillboost/domain/User.java | 25 +++++++++++++++++++ .../skillboost/repository/UserRepository.java | 9 +++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/com/example/skillboost/domain/User.java create mode 100644 src/main/java/com/example/skillboost/repository/UserRepository.java diff --git a/src/main/java/com/example/skillboost/domain/User.java b/src/main/java/com/example/skillboost/domain/User.java new file mode 100644 index 0000000..fd4aac4 --- /dev/null +++ b/src/main/java/com/example/skillboost/domain/User.java @@ -0,0 +1,25 @@ +package com.example.skillboost.domain; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + private String username; + private String githubId; + private String provider; // github, local +} diff --git a/src/main/java/com/example/skillboost/repository/UserRepository.java b/src/main/java/com/example/skillboost/repository/UserRepository.java new file mode 100644 index 0000000..7bb196b --- /dev/null +++ b/src/main/java/com/example/skillboost/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.skillboost.repository; + +import com.example.skillboost.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} \ No newline at end of file From 0e0b11291806be49bc9de983f4d0ba7d61fb91fe Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 16:57:00 +0900 Subject: [PATCH 34/43] =?UTF-8?q?feat:=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=ED=98=84(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/skillboost/auth/JwtFilter.java | 59 +++++++++++ .../example/skillboost/auth/JwtProvider.java | 98 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/main/java/com/example/skillboost/auth/JwtFilter.java create mode 100644 src/main/java/com/example/skillboost/auth/JwtProvider.java diff --git a/src/main/java/com/example/skillboost/auth/JwtFilter.java b/src/main/java/com/example/skillboost/auth/JwtFilter.java new file mode 100644 index 0000000..0f81fbb --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/JwtFilter.java @@ -0,0 +1,59 @@ +package com.example.skillboost.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BEARER_PREFIX = "Bearer "; + + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + // Request Header에서 JWT 토큰 추출 + String jwt = resolveToken(request); + + // JWT 토큰 유효성 검증 + if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) { + // 유효한 토큰이면 Authentication 객체를 생성하여 SecurityContext에 저장 + Authentication authentication = jwtProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("JWT 인증 성공: {}", authentication.getName()); + } else if (StringUtils.hasText(jwt)) { + log.warn("유효하지 않은 JWT 토큰"); + } + + // 다음 필터로 진행 + filterChain.doFilter(request, response); + } + + /** + * Request Header에서 JWT 토큰 추출 + */ + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(BEARER_PREFIX.length()).trim(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/skillboost/auth/JwtProvider.java b/src/main/java/com/example/skillboost/auth/JwtProvider.java new file mode 100644 index 0000000..bfd4237 --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/JwtProvider.java @@ -0,0 +1,98 @@ +package com.example.skillboost.auth; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; + +@Slf4j +@Component +public class JwtProvider { + + private Key key; + + @Value("${jwt.secret-key}") + private String secretKeyBase64; + + @Value("${jwt.expiration-ms}") + private long expirationMs; + + @PostConstruct // ✅ 이거 꼭 있어야 합니다! + protected void init() { + // Base64로 인코딩된 secret key를 디코딩하여 Key 객체 생성 + byte[] keyBytes = Base64.getDecoder().decode(this.secretKeyBase64); + this.key = Keys.hmacShaKeyFor(keyBytes); + log.info("JWT Provider 초기화 완료"); + } + + /** + * JWT 토큰 생성 + */ + public String createToken(String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + this.expirationMs); + + return Jwts.builder() + .setSubject(email) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * JWT 토큰 유효성 검증 + */ + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return true; + } catch (ExpiredJwtException e) { + log.error("JWT 토큰이 만료되었습니다: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + log.error("지원되지 않는 JWT 토큰입니다: {}", e.getMessage()); + } catch (MalformedJwtException e) { + log.error("잘못된 형식의 JWT 토큰입니다: {}", e.getMessage()); + } catch (SecurityException e) { + log.error("JWT 서명이 올바르지 않습니다: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + log.error("JWT 토큰이 비어있습니다: {}", e.getMessage()); + } + return false; + } + + /** + * JWT 토큰에서 Authentication 객체 생성 + */ + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + + String email = claims.getSubject(); + + User principal = new User( + email, + "", + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")) + ); + + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } +} \ No newline at end of file From 04c5c872fa2e74450bf10d1e3c3336a4b0c4f1cd Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 16:58:27 +0900 Subject: [PATCH 35/43] =?UTF-8?q?config:=20Security=20=EB=B0=8F=20Swagger?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/SecurityConfig.java | 60 +++++++++++++++++++ .../skillboost/auth/config/SwaggerConfig.java | 37 ++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/main/java/com/example/skillboost/auth/config/SecurityConfig.java create mode 100644 src/main/java/com/example/skillboost/auth/config/SwaggerConfig.java diff --git a/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java new file mode 100644 index 0000000..aa6126a --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java @@ -0,0 +1,60 @@ +package com.example.skillboost.auth.config; + +import com.example.skillboost.auth.JwtFilter; +import com.example.skillboost.auth.JwtProvider; +import com.example.skillboost.auth.handler.OAuth2SuccessHandler; +import com.example.skillboost.auth.service.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final CustomOAuth2UserService customOAuth2UserService; + private final JwtProvider jwtProvider; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // ✅ 파라미터 제거! + http + // 요청 권한 설정 + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/api/auth/**", + "/oauth2/**", + "/login/oauth2/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**", + "/favicon.ico" + ).permitAll() + .anyRequest().authenticated() + ) + // CSRF 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // JWT 필터 적용 + .addFilterBefore(new JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) + // 기본 폼 로그인 및 HTTP Basic 인증 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + // OAuth2 로그인 설정 + .oauth2Login(oauth2 -> oauth2 + .successHandler(oAuth2SuccessHandler) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/skillboost/auth/config/SwaggerConfig.java b/src/main/java/com/example/skillboost/auth/config/SwaggerConfig.java new file mode 100644 index 0000000..89870db --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.example.skillboost.auth.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI openAPI() { + Server localServer = new Server() + .url("http://localhost:8080") + .description("Local Server"); + + + return new OpenAPI() + .servers(List.of(localServer)) + .components(new Components() + .addSecuritySchemes("bearer-token", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))) + .addSecurityItem(new SecurityRequirement().addList("bearer-token")) + .info(new Info() + .title("My Application API") + .description("API Documentation") + .version("1.0.0")); + } +} \ No newline at end of file From daaa550ca8d47ec8239d5039c37dd3f2f870a495 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 17:00:02 +0900 Subject: [PATCH 36/43] =?UTF-8?q?feat:=20AuthController=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B9=83=ED=97=88=EB=B8=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20URL=20API=20=EC=B6=94=EA=B0=80(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/example/skillboost/auth/controller/AuthController.java diff --git a/src/main/java/com/example/skillboost/auth/controller/AuthController.java b/src/main/java/com/example/skillboost/auth/controller/AuthController.java new file mode 100644 index 0000000..82dc33f --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/controller/AuthController.java @@ -0,0 +1,23 @@ +package com.example.skillboost.auth.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Map; + +@Tag(name = "깃허브 인증 (Authentication)", description = "소셜 로그인 API") +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Operation(summary = "GitHub 로그인 URL 반환", + description = "프론트엔드에서 이 주소로 GET 요청을 보내면, 사용자가 접속해야 할 GitHub 로그인 페이지 URL을 반환합니다.") + @GetMapping("/github-login-url") + public Map getGithubLoginUrl() { + String loginUrl = "/oauth2/authorization/github"; + + return Map.of("url", loginUrl); + } +} \ No newline at end of file From 8ef3a91de393e8a3edf240bd9adfc5eb2696aa8f Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 17:07:16 +0900 Subject: [PATCH 37/43] =?UTF-8?q?feat:=20OAuth2=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=B5=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/handler/OAuth2SuccessHandler.java | 75 +++++++++++++++++++ .../auth/service/CustomOAuth2UserService.java | 75 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/main/java/com/example/skillboost/auth/handler/OAuth2SuccessHandler.java create mode 100644 src/main/java/com/example/skillboost/auth/service/CustomOAuth2UserService.java diff --git a/src/main/java/com/example/skillboost/auth/handler/OAuth2SuccessHandler.java b/src/main/java/com/example/skillboost/auth/handler/OAuth2SuccessHandler.java new file mode 100644 index 0000000..b484cc8 --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/handler/OAuth2SuccessHandler.java @@ -0,0 +1,75 @@ +package com.example.skillboost.auth.handler; + +import com.example.skillboost.auth.JwtProvider; +import com.example.skillboost.domain.User; +import com.example.skillboost.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + private final UserRepository userRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException { + + log.info("OAuth2 인증 성공!"); + + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + String email = (String) oAuth2User.getAttributes().get("email"); + + // GitHub에서 이메일을 비공개로 설정한 경우 처리 + if (email == null || email.isEmpty()) { + String githubId = String.valueOf(oAuth2User.getAttributes().get("id")); + email = githubId + "@github.temp"; + log.warn("이메일 비공개 사용자 - 임시 이메일 사용: {}", email); + } + + // Lambda에서 사용하기 위한 final 변수 + final String finalEmail = email; + + // 사용자 조회 + User user = userRepository.findByEmail(finalEmail) + .orElseThrow(() -> { + log.error("사용자를 찾을 수 없습니다: {}", finalEmail); + return new RuntimeException("User not found: " + finalEmail); + }); + + // JWT 토큰 생성 + String token = jwtProvider.createToken(user.getEmail()); + log.info("JWT 토큰 생성 완료: {}", user.getEmail()); + + // JSON 응답 생성 + Map responseData = new HashMap<>(); + responseData.put("success", true); + responseData.put("token", token); + responseData.put("email", user.getEmail()); + responseData.put("username", user.getUsername()); + + // 클라이언트에 JWT 응답 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(objectMapper.writeValueAsString(responseData)); + + // 프론트엔드로 리다이렉트하려면 아래 주석 해제 + // response.sendRedirect("http://localhost:3000/oauth2/redirect?token=" + token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/skillboost/auth/service/CustomOAuth2UserService.java b/src/main/java/com/example/skillboost/auth/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..d4cbf0f --- /dev/null +++ b/src/main/java/com/example/skillboost/auth/service/CustomOAuth2UserService.java @@ -0,0 +1,75 @@ +package com.example.skillboost.auth.service; + +import com.example.skillboost.domain.User; +import com.example.skillboost.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + + @Override + @Transactional + public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException { + // GitHub에서 사용자 정보 가져오기 + OAuth2User oAuth2User = super.loadUser(request); + Map attributes = oAuth2User.getAttributes(); + + log.info("GitHub OAuth2 사용자 정보: {}", attributes); + + // GitHub 사용자 정보 추출 + String email = (String) attributes.get("email"); + String githubId = String.valueOf(attributes.get("id")); + String username = (String) attributes.get("login"); + + // 이메일이 비공개인 경우 임시 이메일 생성 + if (email == null || email.isEmpty()) { + email = githubId + "@github.temp"; + log.warn("이메일 비공개 사용자 - 임시 이메일 생성: {}", email); + } + + // 사용자 저장 또는 업데이트 + final String finalEmail = email; + User user = userRepository.findByEmail(email) + .map(existing -> { + log.info("기존 사용자 업데이트: {}", finalEmail); + existing.setGithubId(githubId); + existing.setUsername(username); + return existing; + }) + .orElseGet(() -> { + log.info("새로운 사용자 생성: {}", finalEmail); + return User.builder() + .email(finalEmail) + .username(username) + .githubId(githubId) + .provider("github") + .build(); + }); + + userRepository.save(user); + log.info("사용자 정보 저장 완료: {} (GitHub ID: {})", user.getEmail(), user.getGithubId()); + + // OAuth2User 객체 반환 + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), + attributes, + "id" + ); + } +} \ No newline at end of file From 5222c3004f93527be6caa4409e515d153caedbd5 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 17:17:52 +0900 Subject: [PATCH 38/43] =?UTF-8?q?chore:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/skillboost/auth/JwtProvider.java | 2 +- .../java/com/example/skillboost/auth/config/SecurityConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/skillboost/auth/JwtProvider.java b/src/main/java/com/example/skillboost/auth/JwtProvider.java index bfd4237..9833ae9 100644 --- a/src/main/java/com/example/skillboost/auth/JwtProvider.java +++ b/src/main/java/com/example/skillboost/auth/JwtProvider.java @@ -28,7 +28,7 @@ public class JwtProvider { @Value("${jwt.expiration-ms}") private long expirationMs; - @PostConstruct // ✅ 이거 꼭 있어야 합니다! + @PostConstruct protected void init() { // Base64로 인코딩된 secret key를 디코딩하여 Key 객체 생성 byte[] keyBytes = Base64.getDecoder().decode(this.secretKeyBase64); diff --git a/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java index aa6126a..f5eff35 100644 --- a/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java +++ b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java @@ -23,7 +23,7 @@ public class SecurityConfig { private final JwtProvider jwtProvider; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // ✅ 파라미터 제거! + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 요청 권한 설정 .authorizeHttpRequests(auth -> auth From 3064898f67fe99912b18ca4e322a4fbd1264433d Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Mon, 24 Nov 2025 17:17:52 +0900 Subject: [PATCH 39/43] =?UTF-8?q?chore:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/skillboost/auth/JwtProvider.java | 2 +- .../java/com/example/skillboost/auth/config/SecurityConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/skillboost/auth/JwtProvider.java b/src/main/java/com/example/skillboost/auth/JwtProvider.java index bfd4237..9833ae9 100644 --- a/src/main/java/com/example/skillboost/auth/JwtProvider.java +++ b/src/main/java/com/example/skillboost/auth/JwtProvider.java @@ -28,7 +28,7 @@ public class JwtProvider { @Value("${jwt.expiration-ms}") private long expirationMs; - @PostConstruct // ✅ 이거 꼭 있어야 합니다! + @PostConstruct protected void init() { // Base64로 인코딩된 secret key를 디코딩하여 Key 객체 생성 byte[] keyBytes = Base64.getDecoder().decode(this.secretKeyBase64); diff --git a/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java index aa6126a..f5eff35 100644 --- a/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java +++ b/src/main/java/com/example/skillboost/auth/config/SecurityConfig.java @@ -23,7 +23,7 @@ public class SecurityConfig { private final JwtProvider jwtProvider; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // ✅ 파라미터 제거! + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 요청 권한 설정 .authorizeHttpRequests(auth -> auth From 3ed823f498240ff5ca68431b2aace40c724548f9 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Tue, 25 Nov 2025 19:22:57 +0900 Subject: [PATCH 40/43] =?UTF-8?q?refactor:=20JwtProvider=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EA=B0=95=ED=99=94(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/skillboost/auth/JwtProvider.java | 20 ++++++++++++--- src/main/resources/application-local.yml | 25 +++++++------------ src/main/resources/application.yml | 4 ++- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/skillboost/auth/JwtProvider.java b/src/main/java/com/example/skillboost/auth/JwtProvider.java index 9833ae9..274ae7a 100644 --- a/src/main/java/com/example/skillboost/auth/JwtProvider.java +++ b/src/main/java/com/example/skillboost/auth/JwtProvider.java @@ -30,10 +30,22 @@ public class JwtProvider { @PostConstruct protected void init() { - // Base64로 인코딩된 secret key를 디코딩하여 Key 객체 생성 - byte[] keyBytes = Base64.getDecoder().decode(this.secretKeyBase64); - this.key = Keys.hmacShaKeyFor(keyBytes); - log.info("JWT Provider 초기화 완료"); + log.info("========== JWT 설정 값 확인 =========="); + log.info("입력된 Secret Key: [{}]", this.secretKeyBase64); + + if (this.secretKeyBase64 == null || this.secretKeyBase64.startsWith("${")) { + throw new RuntimeException("환경변수 [JWT_SECRET_KEY]가 설정되지 않았습니다! IntelliJ 설정을 확인해주세요."); + } + String safeKey = this.secretKeyBase64.replaceAll("\\s+", ""); + + try { + byte[] keyBytes = Base64.getDecoder().decode(safeKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + log.info("JWT Provider 정상 초기화 완료"); + } catch (IllegalArgumentException e) { + log.error("Base64 디코딩 실패! 키 값을 확인해주세요. (현재 값: {})", safeKey); + throw e; + } } /** diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index dada28b..ac03546 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,31 +1,26 @@ -# 서버 포트 설정 server: port: 8080 -# Spring Boot 애플리케이션 기본 설정 spring: application: name: skill-boost - - # JPA 설정 (테이블 자동 생성을 위해 ddl-auto: update 추가) + jpa: hibernate: ddl-auto: update - # (선택사항) 실행되는 SQL을 로그로 보려면 주석 해제 - # show-sql: true + show-sql: true - # GitHub OAuth2 로그인 설정 security: oauth2: client: registration: github: client-id: Ov23liXAPa0etQe0EisI - client-secret: ${GITHUB_CLIENT_SECRET} # .env 파일에서 읽어옴 + client-secret: a5dc74aff160176ad62591fbe3d2c0a839eb1ef6 scope: - read:user - user:email - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + redirect-uri: http://localhost:8080/login/oauth2/code/github provider: github: authorization-uri: https://github.com/login/oauth/authorize @@ -33,15 +28,13 @@ spring: user-info-uri: https://api.github.com/user user-name-attribute: id -# SpringDoc (Swagger) 설정 +jwt: + secret-key: TXlTdXBlclNlY3JldEtleUZvclNraWxsQm9vc3RQcm9qZWN0MjAyNUNoYWxsZW5nZSE= + expiration-ms: 86400000 + springdoc: api-docs: enabled: true swagger-ui: enabled: true - path: /swagger-ui.html - -# JWT 토큰 설정 -jwt: - secret-key: ${JWT_SECRET_KEY} # .env 파일에서 읽어옴 - expiration-ms: 86400000 # 토큰 만료 시간 (24시간) + path: /swagger-ui.html \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c22d980..82026c5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,5 @@ spring: application: - name: skill-boost \ No newline at end of file + name: skill-boost + profiles: + active: local \ No newline at end of file From 3008bbf765000b5479a97e083b86a8725d328fb5 Mon Sep 17 00:00:00 2001 From: JONGTAE02 Date: Tue, 25 Nov 2025 19:22:57 +0900 Subject: [PATCH 41/43] =?UTF-8?q?refactor:=20JwtProvider=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EA=B0=95=ED=99=94(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/skillboost/auth/JwtProvider.java | 20 ++++++++++++--- src/main/resources/application-local.yml | 25 +++++++------------ src/main/resources/application.yml | 4 ++- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/skillboost/auth/JwtProvider.java b/src/main/java/com/example/skillboost/auth/JwtProvider.java index 9833ae9..274ae7a 100644 --- a/src/main/java/com/example/skillboost/auth/JwtProvider.java +++ b/src/main/java/com/example/skillboost/auth/JwtProvider.java @@ -30,10 +30,22 @@ public class JwtProvider { @PostConstruct protected void init() { - // Base64로 인코딩된 secret key를 디코딩하여 Key 객체 생성 - byte[] keyBytes = Base64.getDecoder().decode(this.secretKeyBase64); - this.key = Keys.hmacShaKeyFor(keyBytes); - log.info("JWT Provider 초기화 완료"); + log.info("========== JWT 설정 값 확인 =========="); + log.info("입력된 Secret Key: [{}]", this.secretKeyBase64); + + if (this.secretKeyBase64 == null || this.secretKeyBase64.startsWith("${")) { + throw new RuntimeException("환경변수 [JWT_SECRET_KEY]가 설정되지 않았습니다! IntelliJ 설정을 확인해주세요."); + } + String safeKey = this.secretKeyBase64.replaceAll("\\s+", ""); + + try { + byte[] keyBytes = Base64.getDecoder().decode(safeKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + log.info("JWT Provider 정상 초기화 완료"); + } catch (IllegalArgumentException e) { + log.error("Base64 디코딩 실패! 키 값을 확인해주세요. (현재 값: {})", safeKey); + throw e; + } } /** diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index dada28b..ac03546 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,31 +1,26 @@ -# 서버 포트 설정 server: port: 8080 -# Spring Boot 애플리케이션 기본 설정 spring: application: name: skill-boost - - # JPA 설정 (테이블 자동 생성을 위해 ddl-auto: update 추가) + jpa: hibernate: ddl-auto: update - # (선택사항) 실행되는 SQL을 로그로 보려면 주석 해제 - # show-sql: true + show-sql: true - # GitHub OAuth2 로그인 설정 security: oauth2: client: registration: github: client-id: Ov23liXAPa0etQe0EisI - client-secret: ${GITHUB_CLIENT_SECRET} # .env 파일에서 읽어옴 + client-secret: a5dc74aff160176ad62591fbe3d2c0a839eb1ef6 scope: - read:user - user:email - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + redirect-uri: http://localhost:8080/login/oauth2/code/github provider: github: authorization-uri: https://github.com/login/oauth/authorize @@ -33,15 +28,13 @@ spring: user-info-uri: https://api.github.com/user user-name-attribute: id -# SpringDoc (Swagger) 설정 +jwt: + secret-key: TXlTdXBlclNlY3JldEtleUZvclNraWxsQm9vc3RQcm9qZWN0MjAyNUNoYWxsZW5nZSE= + expiration-ms: 86400000 + springdoc: api-docs: enabled: true swagger-ui: enabled: true - path: /swagger-ui.html - -# JWT 토큰 설정 -jwt: - secret-key: ${JWT_SECRET_KEY} # .env 파일에서 읽어옴 - expiration-ms: 86400000 # 토큰 만료 시간 (24시간) + path: /swagger-ui.html \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c22d980..82026c5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,5 @@ spring: application: - name: skill-boost \ No newline at end of file + name: skill-boost + profiles: + active: local \ No newline at end of file From 111096b45e2d0e9f6847f8f26526279639094104 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 27 Nov 2025 23:10:49 +0900 Subject: [PATCH 42/43] =?UTF-8?q?application-test.yml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 105917c..66363b4 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,9 +1,9 @@ spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL - driver-class-name: + driver-class-name: org.h2.Driver username: sa - password: org.h2.Driver + password: jpa: hibernate: From b25a4c843316e63396ef360a301004b1632598c2 Mon Sep 17 00:00:00 2001 From: yeongbinbae Date: Thu, 27 Nov 2025 23:10:59 +0900 Subject: [PATCH 43/43] =?UTF-8?q?CI.yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6670844..fd1d4e0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,6 +1,7 @@ name: pull request CI on: + workflow_dispatch: pull_request: branches: - main @@ -23,16 +24,6 @@ jobs: java-version: '21' cache: 'gradle' - # 도커 컴포즈 - - name: Start docker-compose - run: | - docker-compose up -d - - # MySQL이 ready 될 때까지 대기 (최대 30초) - timeout 30 bash -c 'until docker-compose exec -T mysql mysqladmin ping -h localhost --silent; do sleep 1; done' - - echo "MySQL is ready!" - # Gradle wrapper 실행 권한 부여 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -52,8 +43,3 @@ jobs: path: | build/reports/tests/test/ build/test-results/test/ - - # 도커 컴포즈 종료 - - name: Docker Compose Down - if: always() - run: docker-compose down -v \ No newline at end of file