diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..20c6f542
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,19 @@
+
+
+## ✨ 이슈 내용
+
+-
+
+## ✅ 체크리스트
+
+- [ ] Assignees / Labels / Milestone 선택
+- [ ]
+- [ ]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..627d852f
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,20 @@
+
+
+## 📌 관련 이슈
+
+
+## ✨ 어떤 이유로 변경된 내용인지
+
+
+
+## 🙏 검토 혹은 리뷰어에게 남기고 싶은 말
diff --git a/.github/workflows/api-CD-dev.yml b/.github/workflows/api-CD-dev.yml
new file mode 100644
index 00000000..1d6c9183
--- /dev/null
+++ b/.github/workflows/api-CD-dev.yml
@@ -0,0 +1,105 @@
+# 워크플로우의 이름 지정
+name: Umbba API Server CD (Develop)
+
+# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
+on:
+ push:
+ branches: [ "develop" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ - .github/workflows/**
+
+env:
+ S3_BUCKET_NAME: umbba-develop-storage
+
+jobs:
+ build:
+ name: Code deployment
+
+ # 실행 환경
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 4) FCM secret key 파일 생성
+ - name: FCM secret key 파일 생성
+ run: |
+ cd ./umbba-api/src/main/resources
+
+ mkdir ./firebase
+ cd ./firebase
+
+ aws s3 cp --region ap-northeast-2 s3://${{ secrets.S3_DEVELOP_BUCKET_NAME }}/json/umbba-fcm-firebase-adminsdk.json .
+
+ shell: bash
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-api:bootJar -x test
+
+ # 디렉토리 생성
+ - name: Make Directory
+ run: mkdir -p deploy
+
+ # Jar 파일 복사
+ - name: Copy Jar
+ run: cp ./umbba-api/build/libs/*.jar ./deploy
+ # run: cp -r ./umbba-api/src/main/* ./deploy
+
+ # appspec.yml, script files 파일 복사
+ - name: Copy files
+ run: cp ./scripts/umbba-api-dev/* ./deploy
+
+ - name: Make zip file
+ run: zip -r ./umbba-api.zip ./deploy
+ shell: bash
+
+ - name: Upload to S3
+ run: aws s3 cp --region ap-northeast-2 ./umbba-api.zip s3://$S3_BUCKET_NAME/
+
+ # Deploy
+ - name: Deploy
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ run:
+ aws deploy create-deployment
+ --application-name umbba-develop-server-codedeploy
+ --deployment-group-name umbba-api-server-codedeploy-group
+ --file-exists-behavior OVERWRITE
+ --s3-location bucket=umbba-develop-storage,bundleType=zip,key=umbba-api.zip
+ --region ap-northeast-2
\ No newline at end of file
diff --git a/.github/workflows/api-CD-prod.yml b/.github/workflows/api-CD-prod.yml
new file mode 100644
index 00000000..abc3ea10
--- /dev/null
+++ b/.github/workflows/api-CD-prod.yml
@@ -0,0 +1,105 @@
+# 워크플로우의 이름 지정
+name: Umbba API Server CD (Production)
+
+# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ - .github/workflows/**
+
+env:
+ S3_BUCKET_NAME: umbba-storage
+
+jobs:
+ build:
+ name: Code deployment
+
+ # 실행 환경
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 4) FCM secret key 파일 생성
+ - name: FCM secret key 파일 생성
+ run: |
+ cd ./umbba-api/src/main/resources
+
+ mkdir ./firebase
+ cd ./firebase
+
+ aws s3 cp --region ap-northeast-2 s3://${{ secrets.S3_BUCKET_NAME }}/json/umbba-fcm-firebase-adminsdk.json .
+
+ shell: bash
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-api:bootJar -x test
+
+ # 디렉토리 생성
+ - name: Make Directory
+ run: mkdir -p deploy
+
+ # Jar 파일 복사
+ - name: Copy Jar
+ run: cp ./umbba-api/build/libs/*.jar ./deploy
+ # run: cp -r ./umbba-api/src/main/* ./deploy
+
+ # appspec.yml, script files 파일 복사
+ - name: Copy files
+ run: cp ./scripts/umbba-api-prod/* ./deploy
+
+ - name: Make zip file
+ run: zip -r ./umbba-api.zip ./deploy
+ shell: bash
+
+ - name: Upload to S3
+ run: aws s3 cp --region ap-northeast-2 ./umbba-api.zip s3://$S3_BUCKET_NAME/
+
+ # Deploy
+ - name: Deploy
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
+ run:
+ aws deploy create-deployment
+ --application-name umbba-server-codedeploy
+ --deployment-group-name umbba-api-server-codedeploy-group
+ --file-exists-behavior OVERWRITE
+ --s3-location bucket=umbba-storage,bundleType=zip,key=umbba-api.zip
+ --region ap-northeast-2
\ No newline at end of file
diff --git a/.github/workflows/api-CI-dev.yml b/.github/workflows/api-CI-dev.yml
new file mode 100644
index 00000000..e5636bff
--- /dev/null
+++ b/.github/workflows/api-CI-dev.yml
@@ -0,0 +1,55 @@
+name: Umbba API Server CI (Develop)
+
+on:
+ push:
+ branches: [ "develop" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ pull_request:
+ branches: [ "develop" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-api:bootJar -x test
diff --git a/.github/workflows/api-CI-prod.yml b/.github/workflows/api-CI-prod.yml
new file mode 100644
index 00000000..b4d51d32
--- /dev/null
+++ b/.github/workflows/api-CI-prod.yml
@@ -0,0 +1,55 @@
+name: Umbba API Server CI (Production)
+
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ pull_request:
+ branches: [ "main" ]
+ paths:
+ - umbba-api/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-api:bootJar -x test
\ No newline at end of file
diff --git a/.github/workflows/notification-CD-dev.yml b/.github/workflows/notification-CD-dev.yml
new file mode 100644
index 00000000..6fe22226
--- /dev/null
+++ b/.github/workflows/notification-CD-dev.yml
@@ -0,0 +1,105 @@
+# 워크플로우의 이름 지정
+name: Umbba Notification Server CD (Develop)
+
+# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
+on:
+ push:
+ branches: [ "develop" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ - .github/workflows/**
+
+env:
+ S3_BUCKET_NAME: umbba-develop-storage
+
+jobs:
+ build:
+ name: Code deployment
+
+ # 실행 환경
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 4) FCM secret key 파일 생성
+ - name: FCM secret key 파일 생성
+ run: |
+ cd ./umbba-notification/src/main/resources
+
+ mkdir ./firebase
+ cd ./firebase
+
+ aws s3 cp --region ap-northeast-2 s3://${{ secrets.S3_DEVELOP_BUCKET_NAME }}/json/umbba-fcm-firebase-adminsdk.json .
+
+ shell: bash
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-notification:bootJar -x test
+
+ # 디렉토리 생성
+ - name: Make Directory
+ run: mkdir -p deploy
+
+ # Jar 파일 복사
+ - name: Copy Jar
+ run: cp ./umbba-notification/build/libs/*.jar ./deploy
+ # run: cp -r src/main/* ./deploy
+
+ # appspec.yml, script files 파일 복사
+ - name: Copy files
+ run: cp ./scripts/umbba-notification-dev/* ./deploy
+
+ - name: Make zip file
+ run: zip -r ./umbba-notification.zip ./deploy
+ shell: bash
+
+ - name: Upload to S3
+ run: aws s3 cp --region ap-northeast-2 ./umbba-notification.zip s3://$S3_BUCKET_NAME/
+
+ # Deploy
+ - name: Deploy
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ run:
+ aws deploy create-deployment
+ --application-name umbba-develop-server-codedeploy
+ --deployment-group-name umbba-notification-server-codedeploy-group
+ --file-exists-behavior OVERWRITE
+ --s3-location bucket=umbba-develop-storage,bundleType=zip,key=umbba-notification.zip
+ --region ap-northeast-2
diff --git a/.github/workflows/notification-CD-prod.yml b/.github/workflows/notification-CD-prod.yml
new file mode 100644
index 00000000..cd9fb8c7
--- /dev/null
+++ b/.github/workflows/notification-CD-prod.yml
@@ -0,0 +1,105 @@
+# 워크플로우의 이름 지정
+name: Umbba Notification Server CD (Production)
+
+# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ - .github/workflows/**
+
+env:
+ S3_BUCKET_NAME: umbba-storage
+
+jobs:
+ build:
+ name: Code deployment
+
+ # 실행 환경
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 4) FCM secret key 파일 생성
+ - name: FCM secret key 파일 생성
+ run: |
+ cd ./umbba-notification/src/main/resources
+
+ mkdir ./firebase
+ cd ./firebase
+
+ aws s3 cp --region ap-northeast-2 s3://${{ secrets.S3_BUCKET_NAME }}/json/umbba-fcm-firebase-adminsdk.json .
+
+ shell: bash
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-notification:bootJar -x test
+
+ # 디렉토리 생성
+ - name: Make Directory
+ run: mkdir -p deploy
+
+ # Jar 파일 복사
+ - name: Copy Jar
+ run: cp ./umbba-notification/build/libs/*.jar ./deploy
+ # run: cp -r src/main/* ./deploy
+
+ # appspec.yml, script files 파일 복사
+ - name: Copy files
+ run: cp ./scripts/umbba-notification-prod/* ./deploy
+
+ - name: Make zip file
+ run: zip -r ./umbba-notification.zip ./deploy
+ shell: bash
+
+ - name: Upload to S3
+ run: aws s3 cp --region ap-northeast-2 ./umbba-notification.zip s3://$S3_BUCKET_NAME/
+
+ # Deploy
+ - name: Deploy
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
+ run:
+ aws deploy create-deployment
+ --application-name umbba-server-codedeploy
+ --deployment-group-name umbba-notification-server-codedeploy-group
+ --file-exists-behavior OVERWRITE
+ --s3-location bucket=umbba-storage,bundleType=zip,key=umbba-notification.zip
+ --region ap-northeast-2
diff --git a/.github/workflows/notification-CI-dev.yml b/.github/workflows/notification-CI-dev.yml
new file mode 100644
index 00000000..cb09fdcd
--- /dev/null
+++ b/.github/workflows/notification-CI-dev.yml
@@ -0,0 +1,55 @@
+name: Umbba Notification Server CI (Develop)
+
+on:
+ push:
+ branches: [ "develop" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ pull_request:
+ branches: [ "develop" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_DEVELOP_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_DEVELOP_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-notification:bootJar -x test
diff --git a/.github/workflows/notification-CI-prod.yml b/.github/workflows/notification-CI-prod.yml
new file mode 100644
index 00000000..1b01c8c7
--- /dev/null
+++ b/.github/workflows/notification-CI-prod.yml
@@ -0,0 +1,55 @@
+name: Umbba Notification Server CI (Production)
+
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+ pull_request:
+ branches: [ "main" ]
+ paths:
+ - umbba-notification/**
+ - umbba-domain/**
+ - umbba-common/**
+ - umbba-external/**
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요
+ - name: checkout
+ uses: actions/checkout@v3
+
+ # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ # 3) AWS Secrets Manger 환경변수 사용
+ - name: Read secrets from AWS Secrets Manager into environment variables
+ uses: abhilash1in/aws-secrets-manager-action@v1.1.0
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+ secrets: /secret/umbba-secret
+ parse-json: false
+
+ # 이 워크플로우는 gradle build
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
+ run: ./gradlew umbba-notification:bootJar -x test
diff --git a/.gitignore b/.gitignore
index f024bee5..8ad08fda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-application.yml
+firebase/
HELP.md
.gradle
@@ -7,6 +7,11 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/
+umbba-test.http
+
+## Querydsl
+umbba-domain/src/main/generated
+
### STS ###
.apt_generated
.classpath
@@ -37,4 +42,4 @@ out/
### VS Code ###
.vscode/
-.DS_Store
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
index 9261ecd5..37813823 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,353 @@
-# Umbba-Server
-최최최최최최최최최최최종본 main 브랜치 (진지)
-commit 날리는자, 벌받을지어다...
+# 👨👩👧👦 엄빠도 어렸다
+> 부모의 추억을 자식과 공유하며 공감대를 찾는 문답형 아카이빙 서비스, 엄빠도 어렸다
+
+
+
+
+
+## 🌸 금쪽이들
+| 이동섭 | 박예준 |
+| :----------------------------------------------------------: |:----------------------------------------------------------------------------------------------------------------------------------:|
+| | |
+| [ddongseop](https://github.com/ddongseop) | [jun02160](https://github.com/jun02160) |
+
+## 👻 Role
+
+| 담당 역할 | Role |
+|:-------------------|:--------:|
+| Nginx 배포, CI/CD 구축 | 이동섭 |
+| DB 구축 (RDS) | 이동섭 |
+| ERD 작성 | 이동섭, 박예준 |
+| API 개발 | 이동섭, 박예준 |
+| 소셜로그인 기능 구현 | 이동섭 |
+| 푸시알림 기능 구현 | 박예준 |
+
+
+
+
+## 🛠️ 개발 환경
+
+| | |
+| --- | --- |
+| 통합 개발 환경 | IntelliJ |
+| Spring 버전 | 2.7.13 |
+| 데이터베이스 | AWS RDS(MySQL), Redis |
+| 배포 | AWS EC2(Ubuntu), S3 |
+| Project 빌드 관리 도구 | Gradle |
+| CI/CD 툴 | Github Actions, CodeDeploy |
+| ERD 다이어그램 툴 | ERDCloud |
+| Java version | Java 11 |
+| 패키지 구조 | 도메인 패키지 구조 |
+| API 테스트 | PostMan, Swagger |
+| 외부 연동 | Slack, FCM |
+
+
+## 🔧 시스템 아키텍처
+
+
+
+
+
+## 📜 API Docs
+
+### 🔗 [API Docs](https://harsh-step-7dd.notion.site/API-887ec56c3fdd48e19fec44820b63a83d?pvs=4)
+
+
+
+## ☁️ ERD
+
+
+
+
+## 📂 Project Structure
+```
+📂 umbba-api
+├── build.gradle
+└── src
+ ├── main
+ │ ├── 📂 java/sopt/org/umbba/api
+ │ │ ├── ApiApplication.java
+ │ │ ├── 🗂 config
+ │ │ │ ├── SecurityConfig.java
+ │ │ │ ├── 🗂 auth
+ │ │ │ │ ├── CustomJwtAuthenticationEntryPoint.java
+ │ │ │ │ ├── JwtAuthenticationFilter.java
+ │ │ │ │ └── UserAuthentication.java
+ │ │ │ ├── 🗂 jwt
+ │ │ │ │ ├── JwtProvider.java
+ │ │ │ │ ├── JwtValidationType.java
+ │ │ │ │ ├── TokenDto.java
+ │ │ │ │ └── TokenRepository.java
+ │ │ │ └── 🗂 sqs
+ │ │ │ ├── SqsConfig.java
+ │ │ │ └── 🗂 producer
+ │ │ │ └── SqsProducer.java
+ │ │ ├── 🗂 controller
+ │ │ │ ├── 🗂 advice
+ │ │ │ │ └── ControllerExceptionAdvice.java
+ │ │ │ ├── 🗂 health
+ │ │ │ │ ├── DemoController.java
+ │ │ │ │ ├── ServerProfileController.java
+ │ │ │ │ └── SlackTestController.java
+ │ │ │ ├── 🗂 parentchild
+ │ │ │ │ ├── ParentchildController.java
+ │ │ │ │ └── 🗂 dto
+ │ │ │ │ ├── 🗂 request
+ │ │ │ │ │ ├── InviteCodeRequestDto.java
+ │ │ │ │ │ ├── OnboardingInviteRequestDto.java
+ │ │ │ │ │ └── OnboardingReceiveRequestDto.java
+ │ │ │ │ └── 🗂 response
+ │ │ │ │ ├── InviteResultResponseDto.java
+ │ │ │ │ ├── OnboardingInviteResponseDto.java
+ │ │ │ │ └── OnboardingReceiveResponseDto.java
+ │ │ │ ├── 🗂 qna
+ │ │ │ │ ├── QnAController.java
+ │ │ │ │ └── 🗂 dto
+ │ │ │ │ ├── 🗂 request
+ │ │ │ │ │ └── TodayAnswerRequestDto.java
+ │ │ │ │ └── 🗂 response
+ │ │ │ │ ├── GetInvitationResponseDto.java
+ │ │ │ │ ├── GetMainViewResponseDto.java
+ │ │ │ │ ├── QnAListResponseDto.java
+ │ │ │ │ ├── SingleQnAResponseDto.java
+ │ │ │ │ └── TodayQnAResponseDto.java
+ │ │ │ └── 🗂 user
+ │ │ │ ├── AuthController.java
+ │ │ │ └── 🗂 dto
+ │ │ │ ├── 🗂 request
+ │ │ │ │ ├── RefreshRequestDto.java
+ │ │ │ │ ├── SocialLoginRequestDto.java
+ │ │ │ │ └── UserInfoDto.java
+ │ │ │ └── 🗂 response
+ │ │ │ └── UserLoginResponseDto.java
+ │ │ └── 🗂 service
+ │ │ ├── 🗂 notification
+ │ │ │ └── NotificationService.java
+ │ │ ├── 🗂 parentchild
+ │ │ │ └── ParentchildService.java
+ │ │ ├── 🗂 qna
+ │ │ │ └── QnAService.java
+ │ │ └── 🗂 user
+ │ │ ├── AuthService.java
+ │ │ └── 🗂 social
+ │ │ ├── 🗂 apple
+ │ │ │ ├── AppleLoginService.java
+ │ │ │ └── 🗂 verify
+ │ │ │ ├── AppleClaimsValidator.java
+ │ │ │ ├── AppleJwtParser.java
+ │ │ │ ├── EncryptUtils.java
+ │ │ │ └── PublicKeyGenerator.java
+ │ │ └── 🗂 kakao
+ │ │ └── KakaoLoginService.java
+ │ └── 📂 resources
+ └── application.yaml
+
+📂 umbba-notification
+├── build.gradle
+└── src
+ └── main
+ ├── 📂 java/sopt/org/umbba/notification
+ │ ├── NotificationApplication.java
+ │ ├── 🗂 config
+ │ │ ├── 🗂 fcm
+ │ │ │ └── FCMConfig.java
+ │ │ ├── 🗂 scheduler
+ │ │ │ └── ScheduleConfig.java
+ │ │ └── 🗂 sqs
+ │ │ ├── SqsConfig.java
+ │ │ └── 🗂 consumer
+ │ │ └── SqsConsumer.java
+ │ └── 🗂 service
+ │ ├── 🗂 fcm
+ │ │ ├── FCMController.java
+ │ │ ├── FCMService.java
+ │ │ └── 🗂 dto
+ │ │ └── FCMMessage.java
+ │ ├── 🗂 scheduler
+ │ │ └── FCMScheduler.java
+ │ └── 🗂 slack
+ │ └── SlackApi.java
+ └── 📂 resources
+ ├── application.yaml
+ └── 🗂 firebase
+ └── umbba-fcm-firebase-adminsdk.json
+
+📂 umbba-domain
+├── build.gradle
+└── src
+ └── main
+ └── 📂 java/sopt/org/umbba/domain
+ ├── UmbbaDomainRoot.java
+ ├── 🗂 config
+ │ └── 🗂 jpa
+ │ └── JpaConfig.java
+ └── 🗂 domain
+ ├── 🗂 common
+ │ └── AuditingTimeEntity.java
+ ├── 🗂 parentchild
+ │ ├── Parentchild.java
+ │ ├── ParentchildRelation.java
+ │ ├── 🗂 dao
+ │ │ └── ParentchildDao.java
+ │ └── 🗂 repository
+ │ └── ParentchildRepository.java
+ ├── 🗂 qna
+ │ ├── OnboardingAnswer.java
+ │ ├── QnA.java
+ │ ├── Question.java
+ │ ├── QuestionSection.java
+ │ ├── QuestionType.java
+ │ ├── 🗂 dao
+ │ │ └── QnADao.java
+ │ └── 🗂 repository
+ │ ├── QnARepository.java
+ │ └── QuestionRepository.java
+ ├── 🗂 redis
+ │ └── RefreshToken.java
+ └── 🗂 user
+ ├── SocialPlatform.java
+ ├── User.java
+ └── 🗂 repository
+ └── UserRepository.java
+
+📂 umbba-common
+├── build.gradle
+└── src
+ └── main
+ └── 📂 java/sopt/org/umbba/common
+ ├── UmbbaCommonRoot.java
+ ├── 🗂 exception
+ │ ├── ErrorType.java
+ │ ├── SuccessType.java
+ │ ├── 🗂 dto
+ │ │ └── ApiResponse.java
+ │ └── 🗂 model
+ │ └── CustomException.java
+ └── 🗂 sqs
+ ├── MessageType.java
+ ├── MessageUtils.java
+ └── 🗂 dto
+ ├── FCMPushRequestDto.java
+ ├── FirebaseDto.java
+ ├── MessageDto.java
+ ├── PushMessage.java
+ └── SlackDto.java
+
+📂 umbba-external
+├── build.gradle
+└── src
+ └── main
+ └── 📂 java/sopt/org/umbba/external
+ ├── UmbbaExternalRoot.java
+ └── 🗂 client
+ └── 🗂 auth
+ ├── 🗂 apple
+ │ ├── AppleApiClient.java
+ │ └── 🗂 response
+ │ ├── ApplePublicKey.java
+ │ └── ApplePublicKeys.java
+ └── 🗂 kakao
+ ├── KakaoApiClient.java
+ ├── KakaoAuthApiClient.java
+ └── 🗂 response
+ ├── KakaoAccessTokenResponse.java
+ ├── KakaoAccount.java
+ ├── KakaoUserProfile.java
+ └── KakaoUserResponse.java
+```
+
+
+
+# 🌱 Branch
+
+
+
+
+# 🙏 Commit Convention
+```
+- [CHORE] : 동작에 영향 없는 코드 or 변경 없는 변경사항(주석 추가 등) or 파일명, 폴더명 수정 or 파일, 폴더 삭제 or 디렉토리 구조 변경
+- [RENAME] : 파일 이름 변경시
+- [FEAT] : 새로운 기능 구현
+- [FIX] : 버그, 오류 해결
+- [REFACTOR] : 전면 수정, 코드 리팩토링
+- [ADD] : Feat 이외의 부수적인 코드 추가, 라이브러리 추가, 새로운 파일 생성
+- [DEL] : 쓸모없는 코드 삭제
+- [CORRECT] : 주로 문법의 오류나 타입의 변경, 이름 변경시
+- [DOCS] : README나 WIKI 등의 문서 수정
+- [MERGE]: 다른 브랜치와 병합
+- [TEST] : 테스트 코드 추가/작성
+```
+- 커밋은 세부 기능 기준
+- 이슈번호 붙이는 단위 : **FEAT, FIX, REFACTOR**
+
+ ex. `git commit -m “[FEAT] 로그인 기능 구현 #2”`
+
+
+# 🙏 Code Convention
+
+> 💡 **동료들과 말투를 통일하기 위해 컨벤션을 지정합니다.**
+>
+> 오합지졸의 코드가 아닌, **한 사람이 짠 것같은 코드**를 작성하는 것이 추후 유지보수나 협업에서 도움이 됩니다. 내가 코드를 생각하면서 짤 수 있도록 해주는 룰이라고 생각해도 좋습니다!
+
+1. 기본적으로 네이밍은 **누구나 알 수 있는 쉬운 단어**를 선택한다.
+ - 우리는 외국인이 아니다. 쓸데없이 어려운 고급 어휘를 피한다.
+2. 변수는 CamelCase를 기본으로 한다.
+ - userEmail, userCellPhone ...
+3. URL, 파일명 등은 kebab-case를 사용한다.
+ - /user-email-page ...
+4. 패키지명은 단어가 달라지더라도 무조건 소문자를 사용한다.
+ - frontend, useremail ...
+5. ENUM이나 상수는 대문자로 네이밍한다.
+ - NORMAL_STATUS ...
+6. 함수명은 소문자로 시작하고**동사**로 네이밍한다.
+ - getUserId(), isNormal() ...
+7. 클래스명은**명사**로 작성하고 UpperCamelCase를 사용한다.
+ - UserEmail, Address ...
+8. 객체 이름을 함수 이름에 중복해서 넣지 않는다. (= 상위 이름을 하위 이름에 중복시키지 않는다.)
+ - line.getLength() (O) / line.getLineLength() (X)
+9. 컬렉션은 복수형을 사용하거나 컬렉션을 명시해준다.
+ - List ids, Map userToIdMap ...
+10. 이중적인 의미를 가지는 단어는 지양한다.
+ - event, design ...
+11. 의도가 드러난다면 되도록 짧은 이름을 선택한다.
+ - retreiveUser() (X) / getUser() (O)
+ - 단, 축약형을 선택하는 경우는 개발자의 의도가 명백히 전달되는 경우이다. 명백히 전달이 안된다면 축약형보다 서술형이 더 좋다.
+12. 함수의 부수효과를 설명한다.
+ - 함수는 한가지 동작만 수행하는 것이 좋지만, 때에 따라 부수 효과를 일으킬 수도 있다.
+ ```
+ fun getOrder() {
+ if (order == null) {
+ order = Order()
+ }
+ return order
+ }
+ ```
+ - 위 함수는 단순히 order만 가져오는 것이 아니라, 없으면 생성해서 리턴한다.
+ - 그러므로 getOrder() (X) / getOrCreateOrder() (O)
+13. LocalDateTime -> xxxAt, LocalDate -> xxxDt로 네이밍
+14. 객체를 조회하는 함수는 JPA Repository에서 findXxx 형식의 네이밍 쿼리메소드를 사용하므로 개발자가 작성하는 Service단에서는 되도록이면 getXxx를 사용하자.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..d7a7395c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,57 @@
+buildscript {
+ ext {
+ springBootVersion = "2.7.14"
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+}
+
+subprojects {
+ group = "sopt.org.umbba"
+ version = '0.0.1-SNAPSHOT'
+ apply plugin: "java-library"
+ apply plugin: "org.springframework.boot"
+ apply plugin: "io.spring.dependency-management"
+
+
+ sourceCompatibility = "11"
+
+ repositories {
+ mavenCentral()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+
+ configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+ }
+
+ dependencies {
+ implementation "org.springframework.boot:spring-boot-starter-validation"
+ testImplementation "org.springframework.boot:spring-boot-starter-test"
+
+ // lombok
+ compileOnly "org.projectlombok:lombok"
+ annotationProcessor "org.projectlombok:lombok"
+
+ // Health Check
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+
+ // AWS Secrets Manager
+ implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap:3.1.3'
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws-secrets-manager-config:2.2.6.RELEASE'
+ }
+
+ test {
+ useJUnitPlatform()
+ }
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..249e5832
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..774fae87
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..a69d9cb6
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,240 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..53a6b238
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,91 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/scripts/umbba-api-dev/appspec.yml b/scripts/umbba-api-dev/appspec.yml
new file mode 100644
index 00000000..0386a216
--- /dev/null
+++ b/scripts/umbba-api-dev/appspec.yml
@@ -0,0 +1,22 @@
+version: 0.0
+os: linux
+
+files:
+ - source: /
+ destination: /home/ubuntu/api-server
+ overwrite: yes
+
+permissions:
+ - object: /
+ pattern: "**"
+ owner: ubuntu
+ group: ubuntu
+
+hooks:
+ AfterInstall:
+ - location: deploy.sh
+ timeout: 180
+ runas: ubuntu
+ - location: switch.sh
+ timeout: 180
+ runas: ubuntu
\ No newline at end of file
diff --git a/scripts/umbba-api-dev/deploy.sh b/scripts/umbba-api-dev/deploy.sh
new file mode 100644
index 00000000..6905ed35
--- /dev/null
+++ b/scripts/umbba-api-dev/deploy.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+BUILD_PATH=$(ls /home/ubuntu/api-server/umbba-api-0.0.1-SNAPSHOT.jar)
+JAR_NAME=$(basename $BUILD_PATH)
+echo "[$NOW_TIME] build 파일명: $JAR_NAME"
+
+echo "[$NOW_TIME] build 파일 복사"
+DEPLOY_PATH=/home/ubuntu/api-server/nonstop/jar/
+cp $BUILD_PATH $DEPLOY_PATH
+
+echo "[$NOW_TIME] 현재 구동중인 Prod 확인"
+CURRENT_PROFILE=$(curl -s http://localhost/profile)
+echo "[$NOW_TIME] $CURRENT_PROFILE"
+
+# 쉬고 있는 prod 찾기: dev1이 사용중이면 dev2가 쉬고 있고, 반대면 dev1이 쉬고 있음
+if [ $CURRENT_PROFILE == dev1 ]
+then
+ IDLE_PROFILE=dev2
+ IDLE_PORT=8082
+elif [ $CURRENT_PROFILE == dev2 ]
+then
+ IDLE_PROFILE=dev1
+ IDLE_PORT=8081
+else
+ echo "[$NOW_TIME] 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
+ echo "[$NOW_TIME] dev1을 할당합니다. IDLE_PROFILE: dev1"
+ IDLE_PROFILE=dev1
+ IDLE_PORT=8081
+fi
+
+echo "[$NOW_TIME] application.jar 교체"
+IDLE_APPLICATION=$IDLE_PROFILE-Umbba-API.jar
+IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION
+
+ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH
+
+echo "[$NOW_TIME] $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
+IDLE_PID=$(pgrep -f $IDLE_APPLICATION)
+
+if [ -z $IDLE_PID ]
+then
+ echo "[$NOW_TIME] 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
+else
+ echo "[$NOW_TIME] kill -15 $IDLE_PID"
+ kill -15 $IDLE_PID
+
+ while ps -p $IDLE_PID > /dev/null; do
+ sleep 1
+ done
+ echo "[$NOW_TIME] 애플리케이션이 정상 종료되었습니다."
+fi
+
+echo "[$NOW_TIME] $IDLE_PROFILE 배포"
+nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH >> /home/ubuntu/api-server/deploy.log 2>/home/ubuntu/api-server/deploy_err.log &
+
+##################################################################
+
+echo "[$NOW_TIME] $IDLE_PROFILE 10초 후 Health check 시작"
+echo "[$NOW_TIME] curl -s http://localhost:$IDLE_PORT/health "
+sleep 10
+
+for retry_count in {1..10}
+do
+ response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
+ up_count=$(echo $response | grep 'UP' | wc -l)
+
+ if [ $up_count -ge 1 ]
+ then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
+ echo "[$NOW_TIME] Health check 성공"
+ break
+ else
+ echo "[$NOW_TIME] Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
+ echo "[$NOW_TIME] Health check: ${response}"
+ fi
+
+ if [ $retry_count -eq 10 ]
+ then
+ echo "[$NOW_TIME] Health check 실패. "
+ echo "[$NOW_TIME] Nginx에 연결하지 않고 배포를 종료합니다."
+ exit 1
+ fi
+
+ echo "[$NOW_TIME] Health check 연결 실패. 재시도..."
+ sleep 10
+done
\ No newline at end of file
diff --git a/scripts/umbba-api-dev/switch.sh b/scripts/umbba-api-dev/switch.sh
new file mode 100644
index 00000000..06387f45
--- /dev/null
+++ b/scripts/umbba-api-dev/switch.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+echo "[$NOW_TIME] 스위칭"
+sleep 10
+echo "[$NOW_TIME] 현재 구동중인 Port 확인"
+CURRENT_PROFILE=$(curl -s http://localhost/profile)
+
+# 쉬고 있는 prod 찾기: dev1이 사용중이면 dev2가 쉬고 있고, 반대면 dev1이 쉬고 있음
+if [ $CURRENT_PROFILE == dev1 ]
+then
+ IDLE_PORT=8082
+elif [ $CURRENT_PROFILE == dev2 ]
+then
+ IDLE_PORT=8081
+else
+ echo "[$NOW_TIME] 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
+ echo "[$NOW_TIME] 8081을 할당합니다."
+ IDLE_PORT=8081
+fi
+
+echo "[$NOW_TIME] 전환할 Port: $IDLE_PORT"
+echo "[$NOW_TIME] Port 전환"
+echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
+
+PROXY_PORT=$(curl -s http://localhost/profile)
+echo "[$NOW_TIME] Nginx Current Proxy Port: $PROXY_PORT"
+
+echo "[$NOW_TIME] Nginx Reload"
+sudo service nginx reload
\ No newline at end of file
diff --git a/scripts/umbba-api-prod/appspec.yml b/scripts/umbba-api-prod/appspec.yml
new file mode 100644
index 00000000..0386a216
--- /dev/null
+++ b/scripts/umbba-api-prod/appspec.yml
@@ -0,0 +1,22 @@
+version: 0.0
+os: linux
+
+files:
+ - source: /
+ destination: /home/ubuntu/api-server
+ overwrite: yes
+
+permissions:
+ - object: /
+ pattern: "**"
+ owner: ubuntu
+ group: ubuntu
+
+hooks:
+ AfterInstall:
+ - location: deploy.sh
+ timeout: 180
+ runas: ubuntu
+ - location: switch.sh
+ timeout: 180
+ runas: ubuntu
\ No newline at end of file
diff --git a/scripts/umbba-api-prod/deploy.sh b/scripts/umbba-api-prod/deploy.sh
new file mode 100644
index 00000000..d8680401
--- /dev/null
+++ b/scripts/umbba-api-prod/deploy.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+BUILD_PATH=$(ls /home/ubuntu/api-server/umbba-api-0.0.1-SNAPSHOT.jar)
+JAR_NAME=$(basename $BUILD_PATH)
+echo "[$NOW_TIME] build 파일명: $JAR_NAME"
+
+echo "[$NOW_TIME] build 파일 복사"
+DEPLOY_PATH=/home/ubuntu/api-server/nonstop/jar/
+cp $BUILD_PATH $DEPLOY_PATH
+
+echo "[$NOW_TIME] 현재 구동중인 Prod 확인"
+CURRENT_PROFILE=$(curl -s http://localhost/profile)
+echo "[$NOW_TIME] $CURRENT_PROFILE"
+
+# 쉬고 있는 prod 찾기: prod1이 사용중이면 prod2가 쉬고 있고, 반대면 prod1이 쉬고 있음
+if [ $CURRENT_PROFILE == prod1 ]
+then
+ IDLE_PROFILE=prod2
+ IDLE_PORT=8082
+elif [ $CURRENT_PROFILE == prod2 ]
+then
+ IDLE_PROFILE=prod1
+ IDLE_PORT=8081
+else
+ echo "[$NOW_TIME] 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
+ echo "[$NOW_TIME] prod1을 할당합니다. IDLE_PROFILE: prod1"
+ IDLE_PROFILE=prod1
+ IDLE_PORT=8081
+fi
+
+echo "[$NOW_TIME] application.jar 교체"
+IDLE_APPLICATION=$IDLE_PROFILE-Umbba-API.jar
+IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION
+
+ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH
+
+echo "[$NOW_TIME] $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
+IDLE_PID=$(pgrep -f $IDLE_APPLICATION)
+
+if [ -z $IDLE_PID ]
+then
+ echo "[$NOW_TIME] 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
+else
+ echo "[$NOW_TIME] kill -15 $IDLE_PID"
+ kill -15 $IDLE_PID
+
+ while ps -p $IDLE_PID > /dev/null; do
+ sleep 1
+ done
+ echo "[$NOW_TIME] 애플리케이션이 정상 종료되었습니다."
+fi
+
+echo "[$NOW_TIME] $IDLE_PROFILE 배포"
+nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH >> /home/ubuntu/api-server/deploy.log 2>/home/ubuntu/api-server/deploy_err.log &
+
+##################################################################
+
+echo "[$NOW_TIME] $IDLE_PROFILE 10초 후 Health check 시작"
+echo "[$NOW_TIME] curl -s http://localhost:$IDLE_PORT/health "
+sleep 10
+
+for retry_count in {1..10}
+do
+ response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
+ up_count=$(echo $response | grep 'UP' | wc -l)
+
+ if [ $up_count -ge 1 ]
+ then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
+ echo "[$NOW_TIME] Health check 성공"
+ break
+ else
+ echo "[$NOW_TIME] Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
+ echo "[$NOW_TIME] Health check: ${response}"
+ fi
+
+ if [ $retry_count -eq 10 ]
+ then
+ echo "[$NOW_TIME] Health check 실패. "
+ echo "[$NOW_TIME] Nginx에 연결하지 않고 배포를 종료합니다."
+ exit 1
+ fi
+
+ echo "[$NOW_TIME] Health check 연결 실패. 재시도..."
+ sleep 10
+done
\ No newline at end of file
diff --git a/scripts/umbba-api-prod/switch.sh b/scripts/umbba-api-prod/switch.sh
new file mode 100644
index 00000000..4efce738
--- /dev/null
+++ b/scripts/umbba-api-prod/switch.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+echo "[$NOW_TIME] 스위칭"
+sleep 10
+echo "[$NOW_TIME] 현재 구동중인 Port 확인"
+CURRENT_PROFILE=$(curl -s http://localhost/profile)
+
+# 쉬고 있는 prod 찾기: prod1이 사용중이면 prod2가 쉬고 있고, 반대면 prod1이 쉬고 있음
+if [ $CURRENT_PROFILE == prod1 ]
+then
+ IDLE_PORT=8082
+elif [ $CURRENT_PROFILE == prod2 ]
+then
+ IDLE_PORT=8081
+else
+ echo "[$NOW_TIME] 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
+ echo "[$NOW_TIME] 8081을 할당합니다."
+ IDLE_PORT=8081
+fi
+
+echo "[$NOW_TIME] 전환할 Port: $IDLE_PORT"
+echo "[$NOW_TIME] Port 전환"
+echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
+
+PROXY_PORT=$(curl -s http://localhost/profile)
+echo "[$NOW_TIME] Nginx Current Proxy Port: $PROXY_PORT"
+
+echo "[$NOW_TIME] Nginx Reload"
+sudo service nginx reload
\ No newline at end of file
diff --git a/scripts/umbba-notification-dev/appspec.yml b/scripts/umbba-notification-dev/appspec.yml
new file mode 100644
index 00000000..4a0bf554
--- /dev/null
+++ b/scripts/umbba-notification-dev/appspec.yml
@@ -0,0 +1,19 @@
+version: 0.0
+os: linux
+
+files:
+ - source: /
+ destination: /home/ubuntu/notification-server
+ overwrite: yes
+
+permissions:
+ - object: /
+ pattern: "**"
+ owner: ubuntu
+ group: ubuntu
+
+hooks:
+ AfterInstall:
+ - location: deploy.sh
+ timeout: 180
+ runas: ubuntu
\ No newline at end of file
diff --git a/scripts/umbba-notification-dev/deploy.sh b/scripts/umbba-notification-dev/deploy.sh
new file mode 100644
index 00000000..8024a227
--- /dev/null
+++ b/scripts/umbba-notification-dev/deploy.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+BUILD_PATH=/home/ubuntu/notification-server/umbba-notification-0.0.1-SNAPSHOT.jar
+TARGET_PORT=8083
+TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')
+
+if [ ! -z ${TARGET_PID} ]; then
+ echo "[$NOW_TIME] Kill WAS running at ${TARGET_PORT}." >> /home/ubuntu/notification-server/deploy.log
+ sudo kill -15 ${TARGET_PID}
+ while ps -p $TARGET_PID > /dev/null; do
+ sleep 1
+ done
+ echo "[$NOW_TIME] 애플리케이션이 정상 종료되었습니다."
+fi
+
+nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=dev $BUILD_PATH >> /home/ubuntu/notification-server/deploy.log 2>/home/ubuntu/notification-server/deploy_err.log &
+echo "[$NOW_TIME] Now new WAS runs at ${TARGET_PORT}." >> /home/ubuntu/notification-server/deploy.log
+exit 0
\ No newline at end of file
diff --git a/scripts/umbba-notification-prod/appspec.yml b/scripts/umbba-notification-prod/appspec.yml
new file mode 100644
index 00000000..4a0bf554
--- /dev/null
+++ b/scripts/umbba-notification-prod/appspec.yml
@@ -0,0 +1,19 @@
+version: 0.0
+os: linux
+
+files:
+ - source: /
+ destination: /home/ubuntu/notification-server
+ overwrite: yes
+
+permissions:
+ - object: /
+ pattern: "**"
+ owner: ubuntu
+ group: ubuntu
+
+hooks:
+ AfterInstall:
+ - location: deploy.sh
+ timeout: 180
+ runas: ubuntu
\ No newline at end of file
diff --git a/scripts/umbba-notification-prod/deploy.sh b/scripts/umbba-notification-prod/deploy.sh
new file mode 100644
index 00000000..1d95de1b
--- /dev/null
+++ b/scripts/umbba-notification-prod/deploy.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+NOW_TIME="$(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)"
+
+BUILD_PATH=/home/ubuntu/notification-server/umbba-notification-0.0.1-SNAPSHOT.jar
+TARGET_PORT=8083
+TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')
+
+if [ ! -z ${TARGET_PID} ]; then
+ echo "[$NOW_TIME] Kill WAS running at ${TARGET_PORT}." >> /home/ubuntu/notification-server/deploy.log
+ sudo kill -15 ${TARGET_PID}
+ while ps -p $TARGET_PID > /dev/null; do
+ sleep 1
+ done
+ echo "[$NOW_TIME] 애플리케이션이 정상 종료되었습니다."
+fi
+
+nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=prod $BUILD_PATH >> /home/ubuntu/notification-server/deploy.log 2>/home/ubuntu/notification-server/deploy_err.log &
+echo "[$NOW_TIME] Now new WAS runs at ${TARGET_PORT}." >> /home/ubuntu/notification-server/deploy.log
+exit 0
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..12ede70b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,8 @@
+rootProject.name = 'umbbaServer'
+
+include 'umbba-common'
+include 'umbba-domain'
+include 'umbba-external'
+include 'umbba-api'
+include 'umbba-notification'
+
diff --git a/umbba-api/build.gradle b/umbba-api/build.gradle
new file mode 100644
index 00000000..42904e80
--- /dev/null
+++ b/umbba-api/build.gradle
@@ -0,0 +1,43 @@
+jar { enabled = false }
+
+dependencies {
+ implementation project(':umbba-common')
+ implementation project(':umbba-domain')
+ implementation project(':umbba-external')
+
+ // spring mvc
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+
+ // swagger
+// implementation 'org.springdoc:springdoc-openapi-ui:1.5.4'
+
+ // SQS
+ implementation "org.springframework.cloud:spring-cloud-aws-messaging:2.2.6.RELEASE"
+
+ // Spring Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.security:spring-security-test'
+
+ // jwt
+ implementation group: "io.jsonwebtoken", name: "jjwt-api", version: "0.11.2"
+ implementation group: "io.jsonwebtoken", name: "jjwt-impl", version: "0.11.2"
+ implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2"
+
+ // random String
+ implementation 'org.apache.commons:commons-lang3'
+
+ // for FeignException
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.7'
+
+ // redis
+ implementation "org.springframework.boot:spring-boot-starter-data-redis"
+ implementation "org.springframework.session:spring-session-data-redis"
+
+ // s3
+// implementation "org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE"
+
+ // AWS Secrets Manager
+ implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap:3.1.3'
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws-secrets-manager-config:2.2.6.RELEASE'
+}
+
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/ApiApplication.java b/umbba-api/src/main/java/sopt/org/umbba/api/ApiApplication.java
new file mode 100644
index 00000000..97943392
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/ApiApplication.java
@@ -0,0 +1,25 @@
+package sopt.org.umbba.api;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import sopt.org.umbba.common.UmbbaCommonRoot;
+import sopt.org.umbba.domain.UmbbaDomainRoot;
+import sopt.org.umbba.external.UmbbaExternalRoot;
+
+@SpringBootApplication(scanBasePackageClasses = {
+ UmbbaCommonRoot.class,
+ UmbbaDomainRoot.class,
+ UmbbaExternalRoot.class,
+ ApiApplication.class
+}, exclude = { UserDetailsServiceAutoConfiguration.class })
+@EnableFeignClients(basePackageClasses = UmbbaExternalRoot.class)
+public class ApiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ApiApplication.class, args);
+ }
+
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java
new file mode 100644
index 00000000..9f3c1bed
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/SecurityConfig.java
@@ -0,0 +1,51 @@
+package sopt.org.umbba.api.config;
+
+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.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import sopt.org.umbba.api.config.auth.CustomJwtAuthenticationEntryPoint;
+import sopt.org.umbba.api.config.auth.JwtAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+ private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint;
+
+ private static final String[] AUTH_WHITELIST = {
+ "/kakao/**", "/login", "/reissue",
+ "/qna/**", "onboard/**", "/home", "/dummy", "/user/me",
+// "/log-out",
+ "/test", "/profile", "/health", "/actuator/health",
+ "/alarm/qna", "/alarm/drink",
+ "/demo/**",
+ "/album/image"
+ };
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http
+ .formLogin().disable() // Form Login 사용 X
+ .httpBasic().disable() // HTTP Basic 사용 X
+ .csrf().disable() // 쿠키 기반이 아닌 JWT 기반이므로 사용 X
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Spring Security 세션 정책 : 세션을 생성 및 사용하지 않음
+ .and()
+ .exceptionHandling()
+ .authenticationEntryPoint(customJwtAuthenticationEntryPoint) // 에러 핸들링
+ .and()
+ .authorizeHttpRequests()
+ .antMatchers(AUTH_WHITELIST).permitAll()
+ .anyRequest().authenticated()
+ .and()
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // JWT 인증 필터 적용
+ .build();
+ }
+
+}
\ No newline at end of file
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/SqsConfig.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/SqsConfig.java
new file mode 100644
index 00000000..9ff1380b
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/SqsConfig.java
@@ -0,0 +1,33 @@
+package sopt.org.umbba.api.config;
+
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sqs.AmazonSQSAsync;
+import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+@Configuration
+public class SqsConfig {
+
+ @Value("${cloud.aws.credentials.accessKey}")
+ private String AWS_ACCESS_KEY;
+
+ @Value("${cloud.aws.credentials.secretKey}")
+ private String AWS_SECRET_KEY;
+
+ @Value("${cloud.aws.region.static}")
+ private String AWS_REGION;
+
+ @Primary
+ @Bean
+ public AmazonSQSAsync amazonSQSAsync() {
+ BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(AWS_ACCESS_KEY, AWS_SECRET_KEY);
+ return AmazonSQSAsyncClientBuilder.standard()
+ .withRegion(AWS_REGION)
+ .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
+ .build();
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/CustomJwtAuthenticationEntryPoint.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/CustomJwtAuthenticationEntryPoint.java
new file mode 100644
index 00000000..39a23c4c
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/CustomJwtAuthenticationEntryPoint.java
@@ -0,0 +1,30 @@
+package sopt.org.umbba.api.config.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+import sopt.org.umbba.common.exception.ErrorType;
+import sopt.org.umbba.common.exception.dto.ApiResponse;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
+ setResponse(response);
+ }
+
+ private void setResponse(HttpServletResponse response) throws IOException {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.getWriter().println(objectMapper.writeValueAsString(ApiResponse.error(ErrorType.INVALID_ACCESS_TOKEN)));
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/JwtAuthenticationFilter.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/JwtAuthenticationFilter.java
new file mode 100644
index 00000000..99951680
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/JwtAuthenticationFilter.java
@@ -0,0 +1,57 @@
+package sopt.org.umbba.api.config.auth;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+import sopt.org.umbba.api.config.jwt.JwtProvider;
+import sopt.org.umbba.api.config.jwt.JwtValidationType;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * JWT가 유효성을 검증하는 Filter
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtProvider jwtProvider;
+
+ @Override
+ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws IOException, ServletException {
+ try {
+
+ // Request의 Header에서 JWT 토큰을 String으로 가져옴
+ final String token = getJwtFromRequest(request);
+
+ if (StringUtils.hasText(token) && jwtProvider.validateAccessToken(token) == JwtValidationType.VALID_JWT) {
+ Long userId = jwtProvider.getUserFromJwt(token);
+ UserAuthentication authentication = new UserAuthentication(userId, null, null);
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ } catch (Exception exception) {
+ log.error("error : ", exception);
+ }
+
+ filterChain.doFilter(request, response);
+ }
+
+ private String getJwtFromRequest(HttpServletRequest request) {
+ String bearerToken = request.getHeader("Authorization");
+ if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
+ return bearerToken.substring("Bearer ".length());
+ }
+ return null;
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/UserAuthentication.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/UserAuthentication.java
new file mode 100644
index 00000000..f0f7083c
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/auth/UserAuthentication.java
@@ -0,0 +1,13 @@
+package sopt.org.umbba.api.config.auth;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+public class UserAuthentication extends UsernamePasswordAuthenticationToken {
+
+ public UserAuthentication(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
+ super(principal, credentials, authorities);
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtProvider.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtProvider.java
new file mode 100644
index 00000000..77482664
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtProvider.java
@@ -0,0 +1,159 @@
+package sopt.org.umbba.api.config.jwt;
+
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import sopt.org.umbba.domain.domain.redis.RefreshToken;
+import sopt.org.umbba.common.exception.ErrorType;
+import sopt.org.umbba.common.exception.model.CustomException;
+
+import javax.annotation.PostConstruct;
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Base64;
+import java.util.Date;
+
+import static java.util.Objects.isNull;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtProvider {
+
+
+ private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 60 * 1000L * 60 * 24 * 365; // 액세스 토큰 만료 시간: 1일로 지정
+ private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 1000L * 180 * 10; // 리프레시 토큰 만료 시간: 30시간으로 지정
+
+ @Value("${jwt.secret}")
+ private String JWT_SECRET;
+ private final TokenRepository tokenRepository;
+
+ @PostConstruct
+ protected void init() {
+ JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
+ }
+
+
+ public TokenDto issueToken(Authentication authentication) {
+ return TokenDto.of(
+ generateAccessToken(authentication),
+ generateRefreshToken(authentication));
+ }
+
+ // Access 토큰 생성
+ private String generateAccessToken(Authentication authentication) {
+ final Date now = new Date();
+
+ final Claims claims = Jwts.claims()
+ .setIssuedAt(now)
+ .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION_TIME));
+
+ claims.put("userId", authentication.getPrincipal());
+
+ return Jwts.builder()
+ .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
+ .setClaims(claims)
+ .signWith(getSigningKey())
+ .compact();
+ }
+
+ // Refresh 토큰 생성
+ /**
+ * Redis 내부에는
+ * refreshToken:userId : tokenValue 형태로 저장한다.
+ * accessToken과 다르게 UUID로 생성한다.
+ */
+ private String generateRefreshToken(Authentication authentication) {
+ final Date now = new Date();
+
+ final Claims claims = Jwts.claims()
+ .setIssuedAt(now)
+ .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME));
+
+ // TODO UUID or JWT 토큰으로 암호화하여 생성
+ String refreshToken = Jwts.builder()
+ .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
+ .setClaims(claims)
+ .signWith(getSigningKey())
+ .compact();
+
+ tokenRepository.save(
+ RefreshToken.builder()
+ .id(Long.parseLong(authentication.getPrincipal().toString()))
+ .refreshToken(refreshToken)
+ .expiration(REFRESH_TOKEN_EXPIRATION_TIME.intValue() / 1000)
+ .build()
+ );
+
+ return refreshToken;
+ }
+
+ // Access 토큰 검증
+ public JwtValidationType validateAccessToken(String accessToken) {
+ try {
+ final Claims claims = getBody(accessToken);
+ log.info(JwtValidationType.VALID_JWT.getValue());
+ return JwtValidationType.VALID_JWT;
+ } catch (MalformedJwtException ex) {
+ log.error(JwtValidationType.INVALID_JWT_TOKEN.getValue());
+ return JwtValidationType.INVALID_JWT_TOKEN;
+ } catch (ExpiredJwtException ex) {
+ log.error(JwtValidationType.EXPIRED_JWT_TOKEN.getValue());
+ return JwtValidationType.EXPIRED_JWT_TOKEN;
+ } catch (UnsupportedJwtException ex) {
+ log.error(JwtValidationType.UNSUPPORTED_JWT_TOKEN.getValue());
+ return JwtValidationType.UNSUPPORTED_JWT_TOKEN;
+ } catch (IllegalArgumentException ex) {
+ log.error(JwtValidationType.EMPTY_JWT.getValue());
+ return JwtValidationType.EMPTY_JWT;
+ }
+ }
+
+ // Refresh 토큰 검증
+ public boolean validateRefreshToken(Long userId, String refreshToken) throws Exception {
+ // 해당유저의 Refresh 토큰 만료 : Redis에 해당 유저의 토큰이 존재하지 않음
+ RefreshToken token = tokenRepository.findById(userId).orElseThrow(() -> new CustomException(ErrorType.INVALID_REFRESH_TOKEN));
+
+ if (token.getRefreshToken() == null) {
+ return false;
+ }
+ else return token.getRefreshToken().equals(refreshToken);
+ }
+
+ public void deleteRefreshToken(Long userId) {
+ tokenRepository.deleteById(userId);
+ }
+
+ // 토큰에 담겨있는 userId 획득
+ public Long getUserFromJwt(String token) {
+ Claims claims = getBody(token);
+ return Long.parseLong(claims.get("userId").toString());
+ }
+
+ private Claims getBody(final String token) {
+ // 만료된 토큰에 대해 parseClaimsJws를 수행하면 io.jsonwebtoken.ExpiredJwtException이 발생한다.
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ private SecretKey getSigningKey() {
+ String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes());
+ return Keys.hmacShaKeyFor(encodedKey.getBytes());
+ }
+
+ public static Long getUserFromPrincial(Principal principal) {
+ if (isNull(principal)) {
+ throw new CustomException(ErrorType.EMPTY_PRINCIPLE_EXCEPTION);
+ }
+ return Long.valueOf(principal.getName());
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtValidationType.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtValidationType.java
new file mode 100644
index 00000000..dd7cc444
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/JwtValidationType.java
@@ -0,0 +1,19 @@
+package sopt.org.umbba.api.config.jwt;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public enum JwtValidationType {
+
+ VALID_JWT("유효한 JWT 토큰입니다."),
+ INVALID_JWT_SIGNATURE("잘못된 JWT 서명입니다."),
+ INVALID_JWT_TOKEN("유효하지 않는 JWT 토큰입니다."),
+ EXPIRED_JWT_TOKEN("만료된 JWT 토큰입니다."),
+ UNSUPPORTED_JWT_TOKEN("지원하지 않는 JWT 토큰입니다."),
+ EMPTY_JWT("JWT 토큰이 존재하지 않습니다.");
+
+ private final String value;
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenDto.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenDto.java
new file mode 100644
index 00000000..4c456b26
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenDto.java
@@ -0,0 +1,21 @@
+package sopt.org.umbba.api.config.jwt;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class TokenDto {
+
+ private String accessToken;
+ private String refreshToken;
+
+ public static TokenDto of(String accessToken, String refreshToken) {
+ return new TokenDto(accessToken, refreshToken);
+ }
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenRepository.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenRepository.java
new file mode 100644
index 00000000..4de694a1
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/jwt/TokenRepository.java
@@ -0,0 +1,8 @@
+package sopt.org.umbba.api.config.jwt;
+
+import org.springframework.data.repository.CrudRepository;
+import sopt.org.umbba.domain.domain.redis.RefreshToken;
+
+//Redis에 저장해주는 역할
+public interface TokenRepository extends CrudRepository {
+}
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/config/sqs/producer/SqsProducer.java b/umbba-api/src/main/java/sopt/org/umbba/api/config/sqs/producer/SqsProducer.java
new file mode 100644
index 00000000..d3595d78
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/config/sqs/producer/SqsProducer.java
@@ -0,0 +1,86 @@
+package sopt.org.umbba.api.config.sqs.producer;
+
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.model.MessageAttributeValue;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import sopt.org.umbba.common.sqs.MessageType;
+import sopt.org.umbba.common.sqs.MessageUtils;
+import sopt.org.umbba.common.sqs.dto.MessageDto;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * 큐에 메시지를 보내는 역할: API 서버에서 이벤트가 발생할 떄 푸시알림 전송
+ * -> 처음 SQS 대기열 생성에서 설정해둔 사항이 여기서 적용 (지연시간, 메시지 수신 대기 등)
+ *
+ * 1. 처리할 작업 메시지를 SQS에 등록
+ * 2. 큐에서 메시지를 소비(consume)하는 것을 실패한 경우, DLQ로 전송
+ *
+ * TODO 기존에 푸시알림을 파이어베이스로 보내기 위해 호출했던 함수를 SQS Producer로 대체
+ */
+@Slf4j
+@Component
+public class SqsProducer {
+
+ @Value("${cloud.aws.sqs.notification.url}")
+ private String NOTIFICATION_URL;
+
+ private static final String GROUP_ID = "sqs";
+ private final ObjectMapper objectMapper;
+ private final AmazonSQS amazonSqs;
+ private static final String SQS_QUEUE_REQUEST_LOG_MESSAGE = "====> [SQS Queue Request] : %s ";
+
+ public SqsProducer(ObjectMapper objectMapper, AmazonSQS amazonSqs) {
+ this.objectMapper = objectMapper;
+ this.amazonSqs = amazonSqs;
+ }
+
+
+ public void produce(MessageDto message) {
+ try {
+ SendMessageRequest request = new SendMessageRequest(NOTIFICATION_URL,
+ objectMapper.writeValueAsString(message))
+// .withMessageGroupId(GROUP_ID)
+// .withMessageDeduplicationId(UUID.randomUUID().toString()) // TODO UUID Random String으로 변경
+ .withMessageAttributes(createMessageAttributes(message.getType()));
+
+ amazonSqs.sendMessage(request);
+ if (Objects.equals(message.getType(), MessageType.SLACK)) {
+ log.info(MessageUtils.generate(SQS_QUEUE_REQUEST_LOG_MESSAGE, "Slack 500 Error 내용"));
+ } else {
+ log.info(MessageUtils.generate(SQS_QUEUE_REQUEST_LOG_MESSAGE, request));
+ }
+
+
+ } catch (JsonProcessingException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ private Map createMessageAttributes(String type) {
+
+ return Map.of(MessageType.MESSAGE_TYPE_HEADER, new MessageAttributeValue()
+ .withDataType("String")
+ .withStringValue(type));
+ }
+
+
+ /* Queue에 단일 메시지를 보내는 함수 -> SQS 실습에서 사용한 함수
+ public SendResult sendMessage(String groupId, String message) {
+// Message newMessage = MessageBuilder.withPayload(message).build();
+ System.out.println("Sender: " + message);
+ return queueMessagingTemplate.send(to -> to
+ .queue(QUEUE_NAME)
+ .messageGroupId(groupId)
+ .messageDeduplicationId(groupId)
+ .payload(message));
+ }
+ */
+}
\ No newline at end of file
diff --git a/umbba-api/src/main/java/sopt/org/umbba/api/controller/advice/ControllerExceptionAdvice.java b/umbba-api/src/main/java/sopt/org/umbba/api/controller/advice/ControllerExceptionAdvice.java
new file mode 100644
index 00000000..21fff53d
--- /dev/null
+++ b/umbba-api/src/main/java/sopt/org/umbba/api/controller/advice/ControllerExceptionAdvice.java
@@ -0,0 +1,205 @@
+package sopt.org.umbba.api.controller.advice;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.Errors;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingRequestHeaderException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.util.NestedServletException;
+import sopt.org.umbba.api.service.notification.NotificationService;
+import sopt.org.umbba.common.exception.ErrorType;
+import sopt.org.umbba.common.exception.dto.ApiResponse;
+import sopt.org.umbba.common.exception.model.CustomException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.UnexpectedTypeException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+@Slf4j
+@RestControllerAdvice
+@Component
+@RequiredArgsConstructor
+public class ControllerExceptionAdvice {
+
+ private final NotificationService notificationService;
+
+ /**
+ * 400 BAD_REQUEST
+ */
+
+ // FeignException은 @ControllerAdvice에서 처리하는 것이 권장되지 않음
+
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ protected ApiResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
+
+ Errors errors = e.getBindingResult();
+ Map validateDetails = new HashMap<>();
+
+ for (FieldError error : errors.getFieldErrors()) {
+ String validKeyName = String.format("valid_%s", error.getField());
+ validateDetails.put(validKeyName, error.getDefaultMessage());
+ }
+ return ApiResponse.error(ErrorType.REQUEST_VALIDATION_EXCEPTION, validateDetails);
+ }
+
+ // 잘못된 타입으로 요청을 보낸 경우
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(UnexpectedTypeException.class)
+ protected ApiResponse handleUnexpectedTypeException(final UnexpectedTypeException e) {
+ return ApiResponse.error(ErrorType.VALIDATION_WRONG_TYPE_EXCEPTION);
+ }
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ApiResponse handlerMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) {
+ return ApiResponse.error(ErrorType.VALIDATION_WRONG_TYPE_EXCEPTION);
+ }
+
+ // Header에 원하는 Key가 없는 경우
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(MissingRequestHeaderException.class)
+ protected ApiResponse