diff --git "a/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" "b/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md"
new file mode 100644
index 00000000..f728915f
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md"
@@ -0,0 +1,14 @@
+---
+name: 백엔드 이슈
+about: 백엔드와 관련된 이슈
+title: "[백엔드] "
+labels: Backend
+assignees: ''
+
+---
+
+# 이슈 내용
+
+
+# 작업 목록
+- [ ]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..fb57f1ee
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+## 작업 요약
+
+## Issue Link
+
+## 문제점 및 어려움
+
+## 해결 방안
+
+## Reference
diff --git a/.github/workflows/deploy-admin-docker.yml b/.github/workflows/deploy-admin-docker.yml
new file mode 100644
index 00000000..fbcb3858
--- /dev/null
+++ b/.github/workflows/deploy-admin-docker.yml
@@ -0,0 +1,143 @@
+name: Dev Admin CI/CD With Docker
+
+on:
+ push:
+ branches:
+ - develop
+ paths:
+ - 'nowait-app-admin-api/**'
+ - 'nowait-common/**'
+ - 'nowait-domain/domain-core-rdb/**'
+ - 'nowait-domain/domain-admin-rdb/**'
+ - 'nowait-infra/**'
+ - 'build.gradle'
+ - 'settings.gradle'
+ - 'gradle/**'
+
+env:
+ DOCKERHUB_REPO: jihuneddie/nowait_dev_admin
+ DIVISION: admin
+ EC2_DEPLOY_PATH: /home/ubuntu/spring-github-action-admin-docker
+
+
+jobs:
+ build-with-gradle:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Set YML
+ run: |
+ mkdir -p nowait-app-admin-api/src/main/resources
+ echo "${{ secrets.APPLICATION_DEV_ADMIN_YML }}" \
+ | base64 --decode \
+ > nowait-app-admin-api/src/main/resources/application-admin.yml
+ find nowait-app-admin-api/src
+
+ - name: Build Admin JAR
+ run: ./gradlew clean :nowait-app-admin-api:bootJar -x test
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
+
+
+ - name: Build and Push Docker Image
+ run: |
+ TAG=${GITHUB_SHA::7}
+ docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_admin:${TAG} -t ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_admin:latest ./nowait-app-admin-api
+ docker push ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_admin:${TAG}
+ docker push ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_admin:latest
+
+
+
+ deploy:
+ name: Deploy to EC2
+ runs-on: ubuntu-latest
+ needs: build-with-gradle
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set Prometheus YML
+ run: |
+ echo "${{ secrets.PROMETHEUS_DEV_ADMIN_YML }}" \
+ | base64 --decode \
+ > prometheus-admin.yml
+
+ - name: Copy docker-compose.admin.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "docker-compose.admin.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy docker-compose.admin-monitoring.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "docker-compose.admin-monitoring.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy prometheus-admin.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "prometheus-admin.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy deploy-admin-docker.sh to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "scripts/deploy-admin-docker.sh"
+ target: "${{ env.EC2_DEPLOY_PATH }}"
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
+
+ - name: Deploy to EC2
+ uses: appleboy/ssh-action@master
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.DEVSERVER_CICD_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.DEVSERVER_CICD_SECRET_KEY }}
+ AWS_REGION: ap-northeast-2
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ envs: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
+ script: |
+ cd ${{ env.EC2_DEPLOY_PATH }}
+ chmod +x ./scripts/deploy-admin-docker.sh
+ ./scripts/deploy-admin-docker.sh
+
diff --git a/.github/workflows/deploy-admin.yml b/.github/workflows/deploy-admin.yml
new file mode 100644
index 00000000..e29c7a65
--- /dev/null
+++ b/.github/workflows/deploy-admin.yml
@@ -0,0 +1,84 @@
+name: Dev Admin CI/CD
+
+on:
+ push:
+ branches:
+ - legacy
+ paths:
+ - 'nowait-app-admin-api/**'
+ - 'nowait-common/**'
+ - 'nowait-domain/domain-core-rdb/**'
+ - 'nowait-domain/domain-admin-rdb/**'
+ - 'nowait-infra/**'
+ - 'build.gradle'
+ - 'settings.gradle'
+ - 'gradle/**'
+
+env:
+ PROJECT_NAME: NoWait
+ DIVISION: admin
+ AWS_REGION: ap-northeast-2
+ AWS_S3_BUCKET: nowait-deploy-github-actions
+ AWS_CODE_DEPLOY_APPLICATION: nowaiting
+ AWS_CODE_DEPLOY_GROUP: nowaiting-deploy-dev-admin
+
+jobs:
+ build-with-gradle:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Set YML
+ run: |
+ mkdir -p nowait-app-admin-api/src/main/resources
+ echo "${{ secrets.APPLICATION_DEV_ADMIN_YML }}" \
+ | base64 --decode \
+ > nowait-app-admin-api/src/main/resources/application-admin.yml
+ find nowait-app-admin-api/src
+
+ - name: Build Admin JAR
+ run: ./gradlew clean :nowait-app-admin-api:bootJar -x test
+
+ - name: Prepare deployment package
+ run: |
+ rm -rf deploy
+ mkdir -p deploy/scripts
+ cp scripts/start-admin.sh scripts/stop-admin.sh deploy/scripts/
+ cp appspec-admin.yml deploy/appspec.yml
+ cp nowait-app-admin-api/build/libs/nowait-app-admin-api-*.jar deploy/nowait-app-admin-api.jar
+ cd deploy
+ zip -r ../NoWait-admin-${GITHUB_SHA}.zip .
+ cd ..
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-region: ${{ env.AWS_REGION }}
+ aws-access-key-id: ${{ secrets.DEVSERVER_CICD_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.DEVSERVER_CICD_SECRET_KEY }}
+
+ - name: Upload to S3
+ run: |
+ aws deploy push \
+ --application-name "${{ env.AWS_CODE_DEPLOY_APPLICATION }}" \
+ --ignore-hidden-files \
+ --s3-location "s3://${{ env.AWS_S3_BUCKET }}/${{ env.PROJECT_NAME }}/${{ env.DIVISION }}/${{ github.sha }}.zip" \
+ --source deploy
+
+ - name: Create CodeDeploy deployment
+ run: |
+ aws deploy create-deployment \
+ --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \
+ --deployment-config-name CodeDeployDefault.AllAtOnce \
+ --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} \
+ --s3-location bucket=${{ env.AWS_S3_BUCKET }},key=${{ env.PROJECT_NAME }}/${{ env.DIVISION }}/${{ github.sha }}.zip,bundleType=zip
diff --git a/.github/workflows/deploy-user-docker.yml b/.github/workflows/deploy-user-docker.yml
new file mode 100644
index 00000000..0683ae85
--- /dev/null
+++ b/.github/workflows/deploy-user-docker.yml
@@ -0,0 +1,140 @@
+name: Dev User CI/CD With Docker
+
+on:
+ push:
+ branches:
+ - develop
+ paths:
+ - 'nowait-app-user-api/**'
+ - 'nowait-common/**'
+ - 'nowait-domain/domain-core-rdb/**'
+ - 'nowait-domain/domain-user-rdb/**'
+ - 'nowait-infra/**'
+ - 'build.gradle'
+ - 'settings.gradle'
+ - 'gradle/**'
+
+env:
+ DOCKERHUB_REPO: jihuneddie/nowait_dev_user
+ DIVISION: user
+ EC2_DEPLOY_PATH: /home/ubuntu/spring-github-action-user-docker
+
+jobs:
+ build-with-gradle:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Set YML
+ run: |
+ mkdir -p nowait-app-user-api/src/main/resources
+ echo "${{ secrets.APPLICATION_DEV_USER_YML }}" \
+ | base64 --decode \
+ > nowait-app-user-api/src/main/resources/application-user.yml
+ find nowait-app-user-api/src
+
+ - name: Build User JAR
+ run: ./gradlew clean :nowait-app-user-api:bootJar -x test
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
+
+
+ - name: Build and Push Docker Image
+ run: |
+ TAG=${GITHUB_SHA::7}
+ docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_user:${TAG} -t ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_user:latest ./nowait-app-user-api
+ docker push ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_user:${TAG}
+ docker push ${{ secrets.DOCKERHUB_USERNAME }}/nowait_dev_user:latest
+
+ deploy:
+ name: Deploy to EC2
+ runs-on: ubuntu-latest
+ needs: build-with-gradle
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set Prometheus YML
+ run: |
+ echo "${{ secrets.PROMETHEUS_DEV_USER_YML }}" \
+ | base64 --decode \
+ > prometheus-user.yml
+
+ - name: Copy docker-compose.user.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "docker-compose.user.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy docker-compose.user-monitoring.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "docker-compose.user-monitoring.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy prometheus.yml to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "prometheus-user.yml"
+ target: ${{ env.EC2_DEPLOY_PATH }}
+
+ - name: Copy deploy-user-docker to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ source: "scripts/deploy-user-docker.sh"
+ target: "${{ env.EC2_DEPLOY_PATH }}"
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
+
+ - name: Deploy to EC2
+ uses: appleboy/ssh-action@master
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.DEVSERVER_CICD_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.DEVSERVER_CICD_SECRET_KEY }}
+ AWS_REGION: ap-northeast-2
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USER }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ port: ${{ secrets.EC2_PORT }}
+ envs: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
+ script: |
+ cd ${{ env.EC2_DEPLOY_PATH }}
+ chmod +x ./scripts/deploy-user-docker.sh
+ ./scripts/deploy-user-docker.sh
+
diff --git a/.github/workflows/deploy-user.yml b/.github/workflows/deploy-user.yml
new file mode 100644
index 00000000..bf98972d
--- /dev/null
+++ b/.github/workflows/deploy-user.yml
@@ -0,0 +1,86 @@
+name: Dev User CI/CD
+
+on:
+ push:
+ branches:
+ - legacy
+ paths:
+ - 'nowait-app-user-api/**'
+ - 'nowait-common/**'
+ - 'nowait-domain/domain-core-rdb/**'
+ - 'nowait-domain/domain-user-rdb/**'
+ - 'nowait-infra/**'
+ - 'build.gradle'
+ - 'settings.gradle'
+ - 'gradle/**'
+
+env:
+ PROJECT_NAME: NoWait
+ DIVISION: user
+ AWS_REGION: ap-northeast-2
+ AWS_S3_BUCKET: nowait-deploy-github-actions
+ AWS_CODE_DEPLOY_APPLICATION: nowaiting
+ AWS_CODE_DEPLOY_GROUP: nowaiting-deploy-dev-user
+
+jobs:
+ build-with-gradle:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: JDK 17 설치
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: gradlew에 실행 권한 부여
+ run: chmod +x ./gradlew
+
+ - name: Set YML
+ run: |
+ mkdir -p nowait-app-user-api/src/main/resources
+ echo "${{ secrets.APPLICATION_DEV_USER_YML }}" \
+ | base64 --decode \
+ > nowait-app-user-api/src/main/resources/application-user.yml
+ find nowait-app-user-api/src
+
+ - name: Build User JAR
+ run: ./gradlew clean :nowait-app-user-api:bootJar -x test
+
+ - name: Prepare deployment package
+ run: |
+ rm -rf deploy
+ mkdir -p deploy/scripts
+ cp scripts/start-user.sh scripts/stop-user.sh deploy/scripts/
+ cp appspec-user.yml deploy/appspec.yml
+ cp nowait-app-user-api/build/libs/nowait-app-user-api-*.jar deploy/nowait-app-user-api.jar
+ cd deploy
+ zip -r ../NoWait-user-${GITHUB_SHA}.zip .
+ cd ..
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-region: ${{ env.AWS_REGION }}
+ aws-access-key-id: ${{ secrets.DEVSERVER_CICD_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.DEVSERVER_CICD_SECRET_KEY }}
+
+ - name: Upload to S3
+ run: |
+ aws deploy push \
+ --application-name "${{ env.AWS_CODE_DEPLOY_APPLICATION }}" \
+ --ignore-hidden-files \
+ --s3-location "s3://${{ env.AWS_S3_BUCKET }}/${{ env.PROJECT_NAME }}/${{ env.DIVISION }}/${{ github.sha }}.zip" \
+ --source deploy
+
+ - name: Create CodeDeploy deployment
+ run: |
+ aws deploy create-deployment \
+ --application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \
+ --deployment-config-name CodeDeployDefault.AllAtOnce \
+ --deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} \
+ --s3-location bucket=${{ env.AWS_S3_BUCKET }},key=${{ env.PROJECT_NAME }}/${{ env.DIVISION }}/${{ github.sha }}.zip,bundleType=zip
+
+
diff --git a/.github/workflows/pr_notification.yml b/.github/workflows/pr_notification.yml
new file mode 100644
index 00000000..74cd0f47
--- /dev/null
+++ b/.github/workflows/pr_notification.yml
@@ -0,0 +1,140 @@
+name: PR Discord Notification
+
+on:
+ pull_request:
+ types: [opened, closed, reopened]
+
+ workflow_dispatch:
+
+env:
+ USERNAME: "돌아왔다 PR 리뷰 시간"
+ AVATAR_URL: "https://i.ibb.co/GvfVG729/R1280x0-fjpg.jpg"
+
+jobs:
+ notify:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Install jq
+ run: sudo apt-get install -y jq
+
+ - name: Assign Random Reviewer
+ id: assign-reviewer
+ if: github.event.action == 'opened' || github.event.action == 'reopened'
+ uses: actions/github-script@v6
+ env:
+ REVIEWER_MAPPING: ${{ secrets.REVIEWER_MAPPING }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ script: |
+ const mappingJson = process.env.REVIEWER_MAPPING;
+ let githubToDiscordMap;
+ try {
+ githubToDiscordMap = JSON.parse(mappingJson);
+ } catch (error) {
+ throw new Error('Invalid JSON in REVIEWER_MAPPING secret.');
+ }
+
+ const allReviewers = Object.keys(githubToDiscordMap);
+ const { data: pullRequest } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.payload.pull_request.number
+ });
+
+ const prAuthor = pullRequest.user.login;
+ const reviewers = allReviewers.filter(reviewer => reviewer.toLowerCase() !== prAuthor.toLowerCase());
+
+ if (reviewers.length === 0) {
+ throw new Error('No valid reviewers available (all reviewers are PR authors).');
+ }
+
+ const randomReviewer = reviewers[Math.floor(Math.random() * reviewers.length)];
+
+ if (pullRequest.requested_reviewers.length === 0) {
+ await github.rest.pulls.requestReviewers({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.payload.pull_request.number,
+ reviewers: [randomReviewer]
+ });
+
+ const discordId = githubToDiscordMap[randomReviewer];
+ if (!discordId) {
+ throw new Error(`Discord ID not found for GitHub user: ${randomReviewer}`);
+ }
+
+ core.setOutput('reviewer', randomReviewer);
+ core.setOutput('discord_id', discordId);
+ } else {
+ const existingReviewer = pullRequest.requested_reviewers[0].login;
+ const discordId = githubToDiscordMap[existingReviewer];
+ if (!discordId) {
+ throw new Error(`Discord ID not found for GitHub user: ${existingReviewer}`);
+ }
+
+ core.setOutput('reviewer', existingReviewer);
+ core.setOutput('discord_id', discordId);
+ }
+
+ - name: Send Discord Notification
+ env:
+ DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
+
+ run: |
+ prTitle="${{ github.event.pull_request.title }}"
+ prUser="${{ github.event.pull_request.user.login }}"
+ prAction="${{ github.event.action }}"
+ prUrl="${{ github.event.pull_request.html_url }}"
+ prMerged="${{ github.event.pull_request.merged }}"
+ prActor="${{ github.actor }}"
+ prReviewer="${{ steps.assign-reviewer.outputs.reviewer }}"
+ prReviewerDiscordID="${{ steps.assign-reviewer.outputs.discord_id }}"
+
+ if [ "$prAction" == "opened" ]; then
+ MESSAGE="**📢 새로운 PR이 도착했습니다!**
+ - <@${prReviewerDiscordID}> 님 리뷰어로 할당되었습니다!! 🙏
+ - PR 제목 : ${prTitle}
+ - 등록한 사람 : ${prUser}
+ - 리뷰어 : <@${prReviewerDiscordID}>
+ - 리뷰하러 가기 >> [Click](${prUrl})"
+
+ elif [ "$prAction" == "closed" ] && [ "$prMerged" == "true" ]; then
+ MESSAGE="**🎉 PR이 머지되었습니다!**
+ - PR 제목 : ${prTitle}
+ - 작업 수행자 : ${prActor}
+ - PR 확인하기 >> [Click](${prUrl})"
+
+ elif [ "$prAction" == "closed" ]; then
+ MESSAGE="**🚨 PR이 닫혔습니다!**
+ - PR 제목 : ${prTitle}
+ - 작업 수행자 : ${prActor}
+ - PR 확인하기 >> [Click](${prUrl})"
+
+ elif [ "$prAction" == "reopened" ]; then
+ MESSAGE="**🔎 PR이 다시 열렸습니다!**
+ - <@${prReviewerDiscordID}> 님 리뷰어로 할당되었습니다!! 🙏
+ - PR 제목 : ${prTitle}
+ - 작업 수행자 : ${prActor}
+ - 리뷰어 : <@${prReviewerDiscordID}>
+ - 리뷰하러 가기 >> [Click](${prUrl})"
+
+ else
+ MESSAGE="❓ 알 수 없는 PR 이벤트: **${prTitle}** by ${prUser}. **[클릭](${prUrl})**"
+ fi
+
+ DATA=$(jq -n \
+ --arg content "$MESSAGE" \
+ --arg username "$USERNAME" \
+ --arg avatar_url "$AVATAR_URL" \
+ '{content: $content, username: $username, avatar_url: $avatar_url}')
+
+ RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H 'Content-Type: application/json' -d "$DATA" "$DISCORD_WEBHOOK_URL")
+
+ if [ "$RESPONSE" -ne 204 ]; then
+ echo "Failed to send Discord notification. HTTP Status Code: $RESPONSE"
+ exit 1
+ fi
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..34efd07b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# NoWait
+
+
+## 💡프로젝트 소개
+주점 대기 예약부터 메뉴 주문까지 원격으로 처리할 수 있는 웨이팅 서비스입니다.
+
+
+
+## 📜 프로젝트 목표
+
+### 핵심 목표
+- Redis SortedSet기반 대기열으로 급격한 트래픽에도 서비스 중단 없이 대기열 유지
+- Redis SPOF 해결
+- 멀티 모듈 프로젝트 구조로 서비스 영역을 분리하여 독립 빌드·배포 수행
+
+
+## 📌 Architecture
+### 1️⃣ System Architecture
+
+
+### 2️⃣ Multi Module Architecture
+
+
+### 3️⃣ ERD
+
+
+## 🔌 API 문서
+### 관리자
+Admin Server Swagger
+
+### 사용자
+User Server Swagger
+
+
+## 🛠️ 사용 기술
+
+
+
+## 👨👩👧👦 팀원 소개
+
+
+ | Backend |
+ Backend |
+
+
+ |
+ 김지훈
+ |
+
+ 정혜민
+ |
+
+
+
+
+ |
+
+
+ |
+
+
diff --git a/api-admin/build.gradle b/api-admin/build.gradle
deleted file mode 100644
index 31009a7f..00000000
--- a/api-admin/build.gradle
+++ /dev/null
@@ -1,57 +0,0 @@
-plugins {
- id 'org.springframework.boot'
- id 'io.spring.dependency-management'
- id 'java'
-}
-
-group = 'com.nowaiting'
-version = '0.0.1-SNAPSHOT'
-
-java {
- toolchain {
- languageVersion = JavaLanguageVersion.of(17)
- }
-}
-
-repositories {
- mavenCentral()
-}
-
-// api-admin에서 사용하는 도메인, 인프라 모듈만 추가
-dependencies {
- implementation project(':domain-menu') // 메뉴 관련 도메인
- implementation project(':domain-base') // 시간 관련 도메인
- implementation project(':domain-store') // 주점 관련 도메인
- implementation project(':domain-user')
- implementation project(':domain-token')
- implementation project(':domain-order')
- implementation project(':domain-reservation') // 예약 관련 도메인
- implementation project(':security-admin') // 인증 관련 도메인
- implementation project(':infra-aws') // aws 관련 도메인
-
- // Spring Boot Starter
- implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.springframework.boot:spring-boot-starter-validation'
-
- // SPRING SECURITY
- implementation 'org.springframework.boot:spring-boot-starter-security'
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
- implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
-
- // SWAGGER
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
-
- // DB
- implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- runtimeOnly 'com.mysql:mysql-connector-j'
-
- // 기타 필요 라이브러리 (예: Lombok)
- compileOnly 'org.projectlombok:lombok'
- annotationProcessor 'org.projectlombok:lombok'
-
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
-}
-
-test {
- useJUnitPlatform()
-}
diff --git a/api-admin/src/main/java/com/example/apiadmin/ApiAdminApplication.java b/api-admin/src/main/java/com/example/apiadmin/ApiAdminApplication.java
deleted file mode 100644
index 29363011..00000000
--- a/api-admin/src/main/java/com/example/apiadmin/ApiAdminApplication.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.example.apiadmin;
-
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-
-@EnableJpaAuditing
-@SpringBootApplication(scanBasePackages = {
- "com.example.apiadmin",
- "com.example.infrastorage",
- "com.nowait.adminsecurity"
-})
-@EntityScan(basePackages = {
- "com.example.menu.entity", // domain-menu
- "com.example.domainstore.entity", // domain-store
- "com.example.domaintoken.entity",
- "com.nowaiting.user.entity",
- "com.nowait.domainreservation.entity",
- "com.nowait.domainorder.entity",
- "com.nowait.domainorder.entity"
-
-})
-@EnableJpaRepositories(basePackages = {
- "com.example.menu.repository",
- "com.nowaiting.user.repository",
- "com.example.domainstore.repository",
- "com.example.domaintoken.repository",
- "com.nowait.domainorder.repository",
- "com.nowait.domainreservation.repository"
-})
-public class ApiAdminApplication {
- public static void main(String[] args) {
- org.springframework.boot.SpringApplication.run(ApiAdminApplication.class, args);
- }
-}
diff --git a/api-admin/src/main/java/com/example/apiadmin/exception/GlobalExceptionHandler.java b/api-admin/src/main/java/com/example/apiadmin/exception/GlobalExceptionHandler.java
deleted file mode 100644
index 5c5c5c1d..00000000
--- a/api-admin/src/main/java/com/example/apiadmin/exception/GlobalExceptionHandler.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.example.apiadmin.exception;
-
-import static com.nowaiting.common.exception.ErrorMessage.*;
-import static org.springframework.http.HttpStatus.UNAUTHORIZED;
-import static org.springframework.http.HttpStatus.*;
-
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.validation.FieldError;
-import org.springframework.validation.ObjectError;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.MissingRequestValueException;
-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.multipart.MultipartException;
-
-import com.example.apiadmin.reservation.exception.ReservationNotFoundException;
-import com.example.domaintoken.exception.BusinessException;
-import com.nowait.adminsecurity.exception.ResourceNotFoundException;
-import com.nowait.adminsecurity.exception.UnauthorizedException;
-import com.nowait.domainorder.exception.DuplicateOrderException;
-import com.nowait.domainorder.exception.OrderItemsEmptyException;
-import com.nowait.domainorder.exception.OrderParameterEmptyException;
-import com.nowaiting.common.exception.ErrorMessage;
-import com.nowaiting.common.exception.ErrorResponse;
-import com.nowaiting.user.exception.UserNotFoundException;
-
-import io.swagger.v3.oas.annotations.Hidden;
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-@Hidden
-@RestControllerAdvice
-public class GlobalExceptionHandler {
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(BusinessException.class)
- public ErrorResponse handleBusinessException(BusinessException e) {
- log.error("handleBusinessException", e);
- return new ErrorResponse(e.getMessage(), e.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
- log.error("handleMethodArgumentNotValidException", e);
- Map errors = getErrors(e);
- return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode(), errors);
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(HttpMessageNotReadableException.class)
- public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
- log.error("handleHttpMessageNotReadableException", e);
- return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(IllegalArgumentException.class)
- public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
- log.error("handleIllegalArgumentException", e);
- return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(MissingRequestValueException.class)
- public ErrorResponse handleMissingRequestValueException(MissingRequestValueException e) {
- log.error("handleMissingRequestValueExceptionException", e);
- return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode());
- }
-
- @ResponseStatus(value = UNAUTHORIZED)
- @ExceptionHandler(UnauthorizedException.class)
- public ErrorResponse handleUnauthorizedException(UnauthorizedException e) {
- log.error("handleUnauthorizedExceptionException", e);
- return new ErrorResponse(e.getMessage(), e.getCode());
- }
-
- @ResponseStatus(value = NOT_FOUND)
- @ExceptionHandler(ResourceNotFoundException.class)
- public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException e) {
- log.error("handleResourceNotFoundExceptionException", e);
- return new ErrorResponse(e.getMessage(), e.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(MultipartException.class)
- public ErrorResponse handleMultipartException(MultipartException e) {
- log.error("handleMultipartException", e);
- return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode());
- }
-
- @ResponseStatus(value = NOT_FOUND)
- @ExceptionHandler(UserNotFoundException.class)
- public ErrorResponse userNotFoundException(UserNotFoundException e) {
- log.error("userNotFoundException", e);
- return new ErrorResponse(e.getMessage(), NOTFOUND_USER.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(OrderParameterEmptyException.class)
- public ErrorResponse orderParameterEmptyException(OrderParameterEmptyException e) {
- log.error("orderParameterEmptyException", e);
- return new ErrorResponse(e.getMessage(), ORDER_PARAMETER_EMPTY.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(OrderItemsEmptyException.class)
- public ErrorResponse orderItemsEmptyException(OrderItemsEmptyException e) {
- log.error("orderItemsEmptyException", e);
- return new ErrorResponse(e.getMessage(), ORDER_ITEMS_EMPTY.getCode());
- }
-
- @ResponseStatus(value = BAD_REQUEST)
- @ExceptionHandler(DuplicateOrderException.class)
- public ErrorResponse duplicateOrderException(DuplicateOrderException e) {
- log.error("duplicateOrderException", e);
- return new ErrorResponse(e.getMessage(), ErrorMessage.DUPLICATE_ORDER.getCode());
- }
-
- @ResponseStatus(value = NOT_FOUND)
- @ExceptionHandler(ReservationNotFoundException.class)
- public ErrorResponse reservationNotFoundException(ReservationNotFoundException e) {
- log.error("reservationNotFoundException", e);
- return new ErrorResponse(e.getMessage(), NOTFOUND_RESERVATION.getCode());
- }
-
-
- private static Map getErrors(MethodArgumentNotValidException e) {
- return e.getBindingResult()
- .getAllErrors()
- .stream()
- .filter(ObjectError.class::isInstance)
- .collect(Collectors.toMap(
- error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(),
- ObjectError::getDefaultMessage,
- (msg1, msg2) -> msg1 + ";" + msg2
- ));
- }
-
-}
diff --git a/api-admin/src/main/java/com/example/apiadmin/menu/controller/MenuController.java b/api-admin/src/main/java/com/example/apiadmin/menu/controller/MenuController.java
deleted file mode 100644
index eb7523b7..00000000
--- a/api-admin/src/main/java/com/example/apiadmin/menu/controller/MenuController.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.example.apiadmin.menu.controller;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import com.example.apiadmin.menu.dto.MenuCreateRequest;
-import com.example.apiadmin.menu.dto.MenuCreateResponse;
-import com.example.apiadmin.menu.service.MenuService;
-import com.nowaiting.common.api.ApiUtils;
-
-import jakarta.validation.Valid;
-import lombok.RequiredArgsConstructor;
-
-@RestController
-@RequestMapping("/admin/menus")
-@RequiredArgsConstructor
-public class MenuController {
-
- private final MenuService menuService;
-
- @PostMapping("/create")
- public ResponseEntity> createMenu(@Valid @RequestBody MenuCreateRequest request) {
- MenuCreateResponse response = menuService.createMenu(request);
-
- return ResponseEntity
- .status(HttpStatus.CREATED)
- .body(
- ApiUtils.success(
- response
- )
- );
- }
-
- @GetMapping("/all-menus/stores/{storeId}")
- public ResponseEntity> getMenusByStoreId(@PathVariable Long storeId) {
- return ResponseEntity
- .status(HttpStatus.OK)
- .body(
- ApiUtils.success(
- menuService.getMenusByStoreId(storeId)
- )
- );
- }
-}
diff --git a/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuImageService.java b/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuImageService.java
deleted file mode 100644
index 5aec2e7a..00000000
--- a/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuImageService.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.example.apiadmin.menu.service;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.multipart.MultipartFile;
-
-import com.example.apiadmin.menu.dto.MenuImageUploadResponse;
-import com.example.infrastorage.s3.S3Service;
-import com.example.menu.entity.Menu;
-import com.example.menu.entity.MenuImage;
-import com.example.menu.repository.MenuImageRepository;
-import com.example.menu.repository.MenuRepository;
-
-import lombok.RequiredArgsConstructor;
-
-@Service
-@RequiredArgsConstructor
-public class MenuImageService {
-
- private final MenuRepository menuRepository;
- private final MenuImageRepository menuImageRepository;
- private final S3Service s3Service;
-
- @Transactional
- public MenuImageUploadResponse save(Long menuId, MultipartFile file) {
-
- String type = "menu";
- Menu menu = menuRepository.findById(menuId)
- .orElseThrow(() -> new IllegalArgumentException("Menu not found with id: " + menuId));
-
- S3Service.S3UploadResult uploadResult = s3Service.upload(type, menuId, file).join();
-
- // MenuImage 엔티티 생성 및 저장
- MenuImage menuImage = MenuImage.builder()
- .menu(menu)
- .imageUrl(uploadResult.url())
- .fileKey(uploadResult.key())
- .build();
-
- menuImageRepository.save(menuImage);
-
- // 응답 생성
- return MenuImageUploadResponse.fromEntity(menuImage);
- }
-
- @Transactional
- public void delete(Long id) {
- MenuImage menuImage = menuImageRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("MenuImage not found with id: " + id));
-
- s3Service.delete(menuImage.getFileKey());
- menuImageRepository.delete(menuImage);
- }
-}
diff --git a/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuService.java b/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuService.java
deleted file mode 100644
index eedb55a8..00000000
--- a/api-admin/src/main/java/com/example/apiadmin/menu/service/MenuService.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.example.apiadmin.menu.service;
-
-import java.util.List;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import com.example.apiadmin.menu.dto.MenuCreateRequest;
-import com.example.apiadmin.menu.dto.MenuCreateResponse;
-import com.example.apiadmin.menu.dto.MenuImageUploadResponse;
-import com.example.apiadmin.menu.dto.MenuReadDto;
-import com.example.apiadmin.menu.dto.MenuReadResponse;
-import com.example.menu.entity.Menu;
-import com.example.menu.entity.MenuImage;
-import com.example.menu.repository.MenuImageRepository;
-import com.example.menu.repository.MenuRepository;
-
-import lombok.RequiredArgsConstructor;
-
-@Service
-@RequiredArgsConstructor
-public class MenuService {
-
- private final MenuRepository menuRepository;
- private final MenuImageRepository menuImageRepository;
-
- @Transactional
- public MenuCreateResponse createMenu(MenuCreateRequest request) {
- Menu toSave = request.toEntity();
-
- Menu saved = menuRepository.save(toSave);
-
- return MenuCreateResponse.fromEntity(saved);
- }
-
- @Transactional(readOnly = true)
- public MenuReadResponse getMenusByStoreId(Long storeId) {
- List