diff --git a/.deploy/deploy_dev.sh b/.deploy/deploy_dev.sh
new file mode 100644
index 00000000..740b82ee
--- /dev/null
+++ b/.deploy/deploy_dev.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+DOCKER_APP_NAME=meetup
+DOCKER_USERNAME=modagbul
+
+# 최신 이미지 가져오기
+docker pull ${DOCKER_USERNAME}/moing_dev:blue
+docker pull ${DOCKER_USERNAME}/moing_dev:green
+
+# 로그 디렉토리 설정
+LOG_DIR=$(pwd)/logs/logback
+mkdir -p $LOG_DIR
+
+# 현재 실행 중인 컨테이너를 확인 (blue 또는 green)
+EXIST_BLUE=$(docker ps --filter name=${DOCKER_APP_NAME}-blue --filter status=running -q)
+EXIST_GREEN=$(docker ps --filter name=${DOCKER_APP_NAME}-green --filter status=running -q)
+
+# 둘 다 실행 중이지 않을 경우 blue 실행
+if [ -z "$EXIST_BLUE" ] && [ -z "$EXIST_GREEN" ]; then
+ echo "No containers running. Starting blue up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-blue
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_dev:blue
+ BEFORE_COMPOSE_COLOR="green"
+ AFTER_COMPOSE_COLOR="blue"
+elif [ -z "$EXIST_BLUE" ]; then
+ echo "blue up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-blue
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_dev:blue
+ BEFORE_COMPOSE_COLOR="green"
+ AFTER_COMPOSE_COLOR="blue"
+else
+ echo "green up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-green -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-green
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-green -p 8082:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_dev:green
+ BEFORE_COMPOSE_COLOR="blue"
+ AFTER_COMPOSE_COLOR="green"
+fi
+
+sleep 40
+
+# 새로운 컨테이너가 제대로 실행되었는지 확인
+EXIST_AFTER=$(docker ps --filter name=${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} --filter status=running -q)
+if [ -n "$EXIST_AFTER" ]; then
+ # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
+ cp ./nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/nginx.conf
+ sudo nginx -s reload
+
+ # 이전 컨테이너 종료
+ docker stop ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
+ docker rm ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
+ echo "$BEFORE_COMPOSE_COLOR down"
+else
+ # Docker logs
+ LOGS=$(docker logs ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} 2>&1)
+ echo "Error: ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} failed to start."
+ echo "$LOGS"
+fi
diff --git a/.deploy/deploy_prod.sh b/.deploy/deploy_prod.sh
new file mode 100644
index 00000000..d5ae4e4b
--- /dev/null
+++ b/.deploy/deploy_prod.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+DOCKER_APP_NAME=meetup
+DOCKER_USERNAME=modagbul
+
+# 최신 이미지 가져오기
+docker pull ${DOCKER_USERNAME}/moing_prod:blue
+docker pull ${DOCKER_USERNAME}/moing_prod:green
+
+# 로그 디렉토리 설정
+LOG_DIR=$(pwd)/logs/logback
+mkdir -p $LOG_DIR
+
+# 현재 실행 중인 컨테이너를 확인 (blue 또는 green)
+EXIST_BLUE=$(docker ps --filter name=${DOCKER_APP_NAME}-blue --filter status=running -q)
+EXIST_GREEN=$(docker ps --filter name=${DOCKER_APP_NAME}-green --filter status=running -q)
+
+# 둘 다 실행 중이지 않을 경우 blue 실행
+if [ -z "$EXIST_BLUE" ] && [ -z "$EXIST_GREEN" ]; then
+ echo "No containers running. Starting blue up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-blue
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_prod:blue
+ BEFORE_COMPOSE_COLOR="green"
+ AFTER_COMPOSE_COLOR="blue"
+elif [ -z "$EXIST_BLUE" ]; then
+ echo "blue up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-blue
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_prod:blue
+ BEFORE_COMPOSE_COLOR="green"
+ AFTER_COMPOSE_COLOR="blue"
+else
+ echo "green up"
+
+ # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
+ if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-green -q)" ]; then
+ docker rm ${DOCKER_APP_NAME}-green
+ fi
+
+ docker run -d --name ${DOCKER_APP_NAME}-green -p 8082:8080 -e TZ=Asia/Seoul \
+ -v $LOG_DIR:/logs/logback ${DOCKER_USERNAME}/moing_prod:green
+ BEFORE_COMPOSE_COLOR="blue"
+ AFTER_COMPOSE_COLOR="green"
+fi
+
+sleep 40
+
+# 새로운 컨테이너가 제대로 실행되었는지 확인
+EXIST_AFTER=$(docker ps --filter name=${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} --filter status=running -q)
+if [ -n "$EXIST_AFTER" ]; then
+ # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
+ sudo cp ./nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/nginx.conf
+ sudo nginx -s reload
+
+ # 이전 컨테이너 종료
+ docker stop ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
+ docker rm ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
+ echo "$BEFORE_COMPOSE_COLOR down"
+else
+ # Docker logs
+ LOGS=$(docker logs ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} 2>&1)
+ echo "Error: ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} failed to start."
+ echo "$LOGS"
+fi
diff --git a/.deploy/dev_dockerfile b/.deploy/dev_dockerfile
new file mode 100644
index 00000000..3abad45c
--- /dev/null
+++ b/.deploy/dev_dockerfile
@@ -0,0 +1,16 @@
+FROM openjdk:11-jdk
+
+# 타임존 설정
+ENV TZ=Asia/Seoul
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+ARG CACHEBREAKER=1
+ARG JAR_FILE=./build/libs/backend-0.0.1-SNAPSHOT.jar
+COPY ${JAR_FILE} app.jar
+
+# 문서를 이미지의 /static/docs 디렉토리에 복사
+COPY ./build/docs/asciidoc/*.html /static/docs/
+
+# 애플리케이션 실행 시 -cp 옵션을 사용하여 /static/docs 디렉토리를 클래스패스에 추가
+ENTRYPOINT ["java","-cp",".:/static/docs","-Dspring.profiles.active=dev","-jar","/app.jar"]
+
diff --git a/.deploy/prod_dockerfile b/.deploy/prod_dockerfile
new file mode 100644
index 00000000..bff7338e
--- /dev/null
+++ b/.deploy/prod_dockerfile
@@ -0,0 +1,16 @@
+FROM openjdk:11-jdk
+
+# 타임존 설정
+ENV TZ=Asia/Seoul
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+ARG CACHEBREAKER=1
+ARG JAR_FILE=./build/libs/backend-0.0.1-SNAPSHOT.jar
+COPY ${JAR_FILE} app.jar
+
+# 문서를 이미지의 /static/docs 디렉토리에 복사
+COPY ./build/docs/asciidoc/*.html /static/docs/
+
+# 애플리케이션 실행 시 -cp 옵션을 사용하여 /static/docs 디렉토리를 클래스패스에 추가
+ENTRYPOINT ["java","-cp",".:/static/docs","-Dspring.profiles.active=prod","-jar","/app.jar"]
+
diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md
new file mode 100644
index 00000000..cae79156
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature.md
@@ -0,0 +1,17 @@
+---
+name: feature
+about: feat
+title: 'feat/ :'
+labels: ''
+assignees: ''
+
+---
+
+## 📢 description
+
+## ✅ to do
+- [ ]
+- [ ]
+- [ ]
+
+## 🔗 etc
diff --git a/.github/ISSUE_TEMPLATE/fix.md b/.github/ISSUE_TEMPLATE/fix.md
new file mode 100644
index 00000000..418a400f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/fix.md
@@ -0,0 +1,17 @@
+---
+name: fix
+about: fix
+title: 'fix/ :'
+labels: ''
+assignees: ''
+
+---
+
+## description
+
+## to do
+[ ]
+[ ]
+[ ]
+
+## etc
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..9c36590f
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,13 @@
+## PR 타입
+- [ ] 기능 추가
+- [ ] 버그 수정
+- [ ] 의존성, 환경 변수, 빌드 관련 업데이트
+- [ ] 기타 사소한 수정
+
+## 개요
+
+## 변경 사항
+
+## 코드 리뷰 시 참고 사항
+
+## 테스트 결과
diff --git a/.github/workflows/CD-dev.yml b/.github/workflows/CD-dev.yml
new file mode 100644
index 00000000..e2e47780
--- /dev/null
+++ b/.github/workflows/CD-dev.yml
@@ -0,0 +1,122 @@
+name: CD-dev
+
+on:
+ push:
+ branches: [ "main" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ working-directory: ./
+ APPLICATION: ${{ secrets.APPLICATION_DEV }}
+ GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_DEV }}
+ APPLE_KEY: ${{ secrets.APPLE_KEY_DEV }}
+
+ steps:
+ # 소스 코드 체크아웃
+ - uses: actions/checkout@v4
+
+ # JDK 11 설정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+
+ # Gradle 패키지 캐시
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ # 설정 파일 생성
+ - run: |
+ cd ./src/main/resources
+ touch ./application.yml
+ echo "${{env.APPLICATION}}" > ./application.yml
+ touch ./firebase-key.json
+ echo "${{env.GOOGLE_APPLICATION_CREDENTIALS}}" | base64 --decode > ./firebase-key.json
+ touch ./apple-key.p8
+ echo "${{env.APPLE_KEY}}" > ./apple-key.p8
+
+ # 설정 파일을 작업공간에 저장
+ - uses: actions/upload-artifact@v4
+ with:
+ name: application.yml
+ path: ./src/main/resources/application.yml
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: firebase-key.json
+ path: ./src/main/resources/firebase-key.json
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: apple-key.p8
+ path: ./src/main/resources/apple-key.p8
+
+ # gradlew 권한 설정
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle로 빌드
+ - name: Build with Gradle
+ run: ./gradlew build
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle 캐시 정리
+ - name: Cleanup Gradle Cache
+ if: ${{ always() }}
+ run: |
+ rm -f ~/.gradle/caches/modules-2/modules-2.lock
+ rm -f ~/.gradle/caches/modules-2/gc.properties
+
+ # Docker 이미지 빌드 및 푸시
+ - name: Docker build
+ run: |
+ docker build --no-cache -f ./.deploy/dev_dockerfile -t ${{ secrets.DOCKER_USERNAME_DEV }}/moing_dev:green .
+ docker build --no-cache -f ./.deploy/dev_dockerfile -t ${{ secrets.DOCKER_USERNAME_DEV }}/moing_dev:blue .
+
+
+ - name: Docker Hub Login
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME_DEV }}
+ password: ${{ secrets.DOCKER_PASSWORD_DEV }}
+
+ - name: Docker push
+ run: |
+ docker push ${{ secrets.DOCKER_USERNAME_DEV }}/moing_dev:green
+ docker push ${{ secrets.DOCKER_USERNAME_DEV }}/moing_dev:blue
+
+ # EC2로 deploy.sh 전송
+ - name: Deploy deploy.sh to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_SERVER_HOST_DEV }}
+ username: ec2-user
+ key: ${{ secrets.PRIVATE_KEY_DEV }}
+ source: "./.deploy/deploy_dev.sh"
+ target: "/home/ec2-user/"
+
+ # 배포 스크립트 실행
+ - name: Deploy on EC2
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.EC2_SERVER_HOST_DEV }}
+ username: ec2-user
+ key: ${{ secrets.PRIVATE_KEY_DEV }}
+ envs: GITHUB_SHA
+ script: |
+ chmod +x /home/ec2-user/deploy_dev.sh
+ /home/ec2-user/deploy_dev.sh
\ No newline at end of file
diff --git a/.github/workflows/CD-prod.yml b/.github/workflows/CD-prod.yml
new file mode 100644
index 00000000..c14325ab
--- /dev/null
+++ b/.github/workflows/CD-prod.yml
@@ -0,0 +1,122 @@
+name: CD-prod
+
+on:
+ push:
+ branches: [ "release" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ working-directory: ./
+ APPLICATION: ${{ secrets.APPLICATION_PROD }}
+ GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_PROD }}
+ APPLE_KEY: ${{ secrets.APPLE_KEY_PROD }}
+
+ steps:
+ # 소스 코드 체크아웃
+ - uses: actions/checkout@v4
+
+ # JDK 11 설정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+
+ # Gradle 패키지 캐시
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ # 설정 파일 생성
+ - run: |
+ cd ./src/main/resources
+ touch ./application.yml
+ echo "${{env.APPLICATION}}" > ./application.yml
+ touch ./firebase-key.json
+ echo "${{env.GOOGLE_APPLICATION_CREDENTIALS}}" | base64 --decode > ./firebase-key.json
+ touch ./apple-key.p8
+ echo "${{env.APPLE_KEY}}" > ./apple-key.p8
+
+ # 설정 파일을 작업공간에 저장
+ - uses: actions/upload-artifact@v4
+ with:
+ name: application.yml
+ path: ./src/main/resources/application.yml
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: firebase-key.json
+ path: ./src/main/resources/firebase-key.json
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: apple-key.p8
+ path: ./src/main/resources/apple-key.p8
+
+ # gradlew 권한 설정
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle로 빌드
+ - name: Build with Gradle
+ run: ./gradlew build
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle 캐시 정리
+ - name: Cleanup Gradle Cache
+ if: ${{ always() }}
+ run: |
+ rm -f ~/.gradle/caches/modules-2/modules-2.lock
+ rm -f ~/.gradle/caches/modules-2/gc.properties
+
+ # Docker 이미지 빌드 및 푸시
+ - name: Docker build
+ run: |
+ docker build --no-cache -f ./.deploy/prod_dockerfile -t ${{ secrets.DOCKER_USERNAME_PROD }}/moing_prod:green .
+ docker build --no-cache -f ./.deploy/prod_dockerfile -t ${{ secrets.DOCKER_USERNAME_PROD }}/moing_prod:blue .
+
+
+ - name: Docker Hub Login
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME_PROD }}
+ password: ${{ secrets.DOCKER_PASSWORD_PROD }}
+
+ - name: Docker push
+ run: |
+ docker push ${{ secrets.DOCKER_USERNAME_PROD }}/moing_prod:green
+ docker push ${{ secrets.DOCKER_USERNAME_PROD }}/moing_prod:blue
+
+ # EC2로 deploy.sh 전송
+ - name: Deploy deploy.sh to EC2
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.EC2_SERVER_HOST_PROD }}
+ username: ec2-user
+ key: ${{ secrets.PRIVATE_KEY_PROD }}
+ source: "./.deploy/deploy_prod.sh"
+ target: "/home/ec2-user/"
+
+ # 배포 스크립트 실행
+ - name: Deploy on EC2
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.EC2_SERVER_HOST_PROD }}
+ username: ec2-user
+ key: ${{ secrets.PRIVATE_KEY_PROD }}
+ envs: GITHUB_SHA
+ script: |
+ chmod +x /home/ec2-user/deploy_prod.sh
+ /home/ec2-user/deploy_prod.sh
\ No newline at end of file
diff --git a/.github/workflows/CI-dev.yml b/.github/workflows/CI-dev.yml
new file mode 100644
index 00000000..608249cb
--- /dev/null
+++ b/.github/workflows/CI-dev.yml
@@ -0,0 +1,84 @@
+name: CI-dev
+
+on:
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ working-directory: ./
+ APPLICATION: ${{ secrets.APPLICATION_DEV }}
+ GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_DEV }}
+ APPLE_KEY: ${{ secrets.APPLE_KEY_DEV }}
+
+ steps:
+ # 소스 코드 체크아웃
+ - uses: actions/checkout@v4
+
+ # JDK 11 설정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+
+ # Gradle 패키지 캐시
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ # 설정 파일 생성
+ - run: |
+ cd ./src/main/resources
+ touch ./application.yml
+ echo "${{env.APPLICATION}}" > ./application.yml
+ touch ./firebase-key.json
+ echo "${{env.GOOGLE_APPLICATION_CREDENTIALS}}" | base64 --decode > ./firebase-key.json
+ touch ./apple-key.p8
+ echo "${{env.APPLE_KEY}}" > ./apple-key.p8
+
+ # 설정 파일을 작업공간에 저장
+ - uses: actions/upload-artifact@v4
+ with:
+ name: application.yml
+ path: ./src/main/resources/application.yml
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: firebase-key.json
+ path: ./src/main/resources/firebase-key.json
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: apple-key.p8
+ path: ./src/main/resources/apple-key.p8
+
+ # gradlew 권한 설정
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle로 빌드
+ - name: Build with Gradle
+ run: ./gradlew build
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle 캐시 정리
+ - name: Cleanup Gradle Cache
+ if: ${{ always() }}
+ run: |
+ rm -f ~/.gradle/caches/modules-2/modules-2.lock
+ rm -f ~/.gradle/caches/modules-2/gc.properties
+
diff --git a/.github/workflows/CI-prod.yml b/.github/workflows/CI-prod.yml
new file mode 100644
index 00000000..57bd93bd
--- /dev/null
+++ b/.github/workflows/CI-prod.yml
@@ -0,0 +1,83 @@
+name: CI-prod
+
+on:
+ pull_request:
+ branches:
+ - release
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ working-directory: ./
+ APPLICATION: ${{ secrets.APPLICATION_PROD }}
+ GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_PROD }}
+ APPLE_KEY: ${{ secrets.APPLE_KEY_PROD }}
+
+ steps:
+ # 소스 코드 체크아웃
+ - uses: actions/checkout@v4
+
+ # JDK 11 설정
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+
+ # Gradle 패키지 캐시
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ # 설정 파일 생성
+ - run: |
+ cd ./src/main/resources
+ touch ./application.yml
+ echo "${{env.APPLICATION}}" > ./application.yml
+ touch ./firebase-key.json
+ echo "${{env.GOOGLE_APPLICATION_CREDENTIALS}}" | base64 --decode > ./firebase-key.json
+ touch ./apple-key.p8
+ echo "${{env.APPLE_KEY}}" > ./apple-key.p8
+
+ # 설정 파일을 작업공간에 저장
+ - uses: actions/upload-artifact@v4
+ with:
+ name: application.yml
+ path: ./src/main/resources/application.yml
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: firebase-key.json
+ path: ./src/main/resources/firebase-key.json
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: apple-key.p8
+ path: ./src/main/resources/apple-key.p8
+
+ # gradlew 권한 설정
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle로 빌드
+ - name: Build with Gradle
+ run: ./gradlew build
+ working-directory: ${{ env.working-directory }}
+
+ # Gradle 캐시 정리
+ - name: Cleanup Gradle Cache
+ if: ${{ always() }}
+ run: |
+ rm -f ~/.gradle/caches/modules-2/modules-2.lock
+ rm -f ~/.gradle/caches/modules-2/gc.properties
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..850b9ca6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,56 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### yml ###
+application.yml
+application-local.yml
+application-dev.yml
+.DS_Store
+src/main/resources/firebase-key.json
+
+src/test/resources/
+.jpb
+data.sql
+
+#### docs ####
+!**/src/main/resources/static/docs/
+*.html
+
+firebase-key.json
+apple-key.p8
+logs/
\ No newline at end of file
diff --git a/README.md b/README.md
index 9685d253..5f282781 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,12 @@
-# MOING_server_release
\ No newline at end of file
+# MOING_server_release
+## System Architecture
+![image](https://github.com/Modagbul/MOING_Server_Release/assets/86006389/02032a32-da0d-429e-babe-15534476f380)
+
+## Backend CI/CD
+
+
+## Monitoring
+![image](https://github.com/Modagbul/MOING_Server_Release/assets/86006389/c0d135c3-6863-4c32-b6df-0535fcbf9953)
+
+## DB ERD
+![KakaoTalk_Image_2023-11-15-20-38-24 (1)](https://github.com/Modagbul/MOING_Server_Release/assets/86006389/e26f65bb-d717-46a6-9ef2-0b9c9dca4679)
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..a3f3499a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,157 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '2.7.14'
+ id 'io.spring.dependency-management' version '1.0.15.RELEASE'
+ id "org.asciidoctor.jvm.convert" version "3.3.2"
+ id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
+}
+
+group = 'com.moing'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '11'
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+ asciidoctorExtensions
+ querydsl.extendsFrom compileClasspath
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+
+bootJar {
+ dependsOn asciidoctor
+ from ("${asciidoctor.outputDir}/html5") {
+ into 'static/docs'
+ }
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-batch'
+ compileOnly 'org.projectlombok:lombok'
+ runtimeOnly 'com.mysql:mysql-connector-j'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ implementation 'org.springframework:spring-context:5.3.15'
+
+ implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'
+ implementation 'org.springframework:spring-context:5.3.15'
+
+ implementation 'com.google.code.gson:gson:2.8.7'
+
+ // Push Alarm
+ implementation 'com.google.firebase:firebase-admin:9.2.0'
+ implementation 'com.google.firebase:firebase-messaging:23.0.0'
+
+ // RestDocs
+ asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
+ testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
+
+ // Qeurydsl
+ implementation 'com.querydsl:querydsl-jpa:5.0.0'
+ implementation 'com.querydsl:querydsl-apt:5.0.0'
+
+ // Jwt
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
+
+ // Redis
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+
+ // WebClient
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+
+ // Feign
+ implementation 'io.github.openfeign:feign-httpclient:12.1'
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.4'
+
+ // Throttling
+ implementation 'com.bucket4j:bucket4j-core:8.1.1'
+ implementation 'com.bucket4j:bucket4j-jcache:8.1.1'
+ implementation 'javax.cache:cache-api:1.1.1'
+ implementation 'org.redisson:redisson:3.19.0'
+
+ // S3
+ implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
+ implementation platform('software.amazon.awssdk:bom:2.17.230')
+ implementation 'software.amazon.awssdk:s3'
+
+ //slack
+ implementation 'com.slack.api:slack-api-client:1.30.0'
+
+ //json
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
+
+ //log
+ implementation 'org.springframework.boot:spring-boot-starter-logging'
+ implementation 'org.springframework.boot:spring-boot-starter-aop'
+ implementation("org.springframework.boot:spring-boot-starter-actuator")
+ implementation("io.micrometer:micrometer-registry-prometheus") // 프로메테우스 마이크로미터
+
+
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
+
+ext {
+ snippetsDir = file('build/generated-snippets')
+}
+
+test {
+ outputs.dir snippetsDir
+}
+
+asciidoctor {
+ dependsOn test
+ configurations 'asciidoctorExtensions'
+ inputs.dir snippetsDir
+
+ // 특정 .adoc에 다른 adoc 파일을 가져와서(include) 사용하고 싶을 경우 경로를 baseDir로 맞춰주는 설정입니다.
+ // 개별 adoc으로 운영한다면 필요 없는 옵션입니다.
+ baseDirFollowsSourceFile()
+}
+
+asciidoctor.doFirst {
+ delete file('src/main/resources/static/docs')
+}
+
+task copyDocument(type: Copy) {
+ dependsOn asciidoctor
+ from file("build/docs/asciidoc")
+ into file("src/main/resources/static/docs")
+}
+build {
+ dependsOn copyDocument
+}
+
+
+def querydslDir = "$buildDir/generated/querydsl"
+
+querydsl {
+ jpa = true
+ querydslSourcesDir = querydslDir
+}
+sourceSets {
+ main.java.srcDir querydslDir
+}
+
+compileQuerydsl {
+ options.annotationProcessorPath = configurations.querydsl
+}
\ 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..033e24c4
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..9f4197d5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..fcb6fca1
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/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/HEAD/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
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ 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
+
+
+# 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"'
+
+# 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..6689b85b
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@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=.
+@rem This is normally unused
+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/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..0f5036dc
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'backend'
diff --git a/src/docs/asciidoc/AlarmHistory-API.adoc b/src/docs/asciidoc/AlarmHistory-API.adoc
new file mode 100644
index 00000000..c3ee6871
--- /dev/null
+++ b/src/docs/asciidoc/AlarmHistory-API.adoc
@@ -0,0 +1,37 @@
+[[AlarmHistory-API]]
+= AlarmHistory API
+
+[[AlarmHistory-알림-모아보기]]
+== AlarmHistory 알림 모아보기
+operation::alarm-history-controller-test/get_all_alarm_history[snippets='http-request,http-response,response-fields']
+
+== Type Category
+|===
+| Category | Description
+
+| `NEW_UPLOAD`
+| 신규 업로드 알림
+
+| `FIRE`
+| 불던지기 알림
+
+| `REMIND`
+| 리마인드 알림
+
+| `APPROVE_TEAM`
+| 소모임 승인
+
+| `REJECT_TEAM`
+| 소모임 반려
+
+| `COMMENT`
+| 댓글 생성 알림
+|===
+
+[[AlarmHistory-알림-단건-조회하기]]
+== AlarmHistory 알림 단건 조회하기
+operation::alarm-history-controller-test/read_alarm_history[snippets='http-request,http-response,response-fields']
+
+[[AlarmHistory-안읽은-알림개수-조회하기]]
+== AlarmHistory 안읽은 알림 개수 조회하기
+operation::alarm-history-controller-test/get_alarm_count[snippets='http-request,http-response,response-fields']
diff --git a/src/docs/asciidoc/Auth-API.adoc b/src/docs/asciidoc/Auth-API.adoc
new file mode 100644
index 00000000..bfb95656
--- /dev/null
+++ b/src/docs/asciidoc/Auth-API.adoc
@@ -0,0 +1,49 @@
+[[Auth-API]]
+= Auth API
+
+[[Auth-Kako-소셜-로그인]]
+== Auth Kakao 소셜 로그인
+operation::auth-controller-test/kakao_소셜_로그인_회원가입_전[snippets='http-request,request-fields']
+operation::auth-controller-test/kakao_소셜_로그인_회원가입_전[snippets='http-response,response-fields']
+operation::auth-controller-test/kakao_소셜_로그인_회원가입_후[snippets='http-response,response-fields']
+
+---
+
+[[Auth-Apple-소셜-로그인]]
+== Auth Apple 소셜 로그인
+operation::auth-controller-test/apple_소셜_로그인_회원가입_전[snippets='http-request,request-fields']
+operation::auth-controller-test/apple_소셜_로그인_회원가입_전[snippets='http-response,response-fields']
+operation::auth-controller-test/apple_소셜_로그인_회원가입_후[snippets='http-response,response-fields']
+
+
+[[Auth-Google-소셜-로그인]]
+== Auth Google 소셜 로그인
+operation::auth-controller-test/google_소셜_로그인_회원가입_전[snippets='http-request,request-fields']
+operation::auth-controller-test/google_소셜_로그인_회원가입_전[snippets='http-response,response-fields']
+operation::auth-controller-test/google_소셜_로그인_회원가입_후[snippets='http-response,response-fields']
+
+
+[[Auth-회원가입]]
+== Auth 회원가입
+|===
+| 성별
+| `MAN`
+| `WOMAN`
+| `NEUTRALITY`
+|===
+operation::auth-controller-test/sign_up[snippets='http-request,request-fields,http-response,response-fields']
+
+
+[[Auth-토큰-재발급]]
+== Auth 토큰 재발급
+operation::auth-controller-test/reissue_token[snippets='http-request,http-response,response-fields']
+
+[[Auth-닉네임-검사]]
+== Auth 닉네임 중복검사 요청
+operation::auth-controller-test/check_nickname_중복o[snippets='http-request']
+
+== Auth 닉네임 중복인 경우
+operation::auth-controller-test/check_nickname_중복o[snippets='http-response,response-fields']
+
+== Auth 닉네임 중복이 아닌 경우
+operation::auth-controller-test/check_nickname_중복x[snippets='http-response,response-fields']
diff --git a/src/docs/asciidoc/Block-API.adoc b/src/docs/asciidoc/Block-API.adoc
new file mode 100644
index 00000000..99baf903
--- /dev/null
+++ b/src/docs/asciidoc/Block-API.adoc
@@ -0,0 +1,24 @@
+
+
+[[Block-API]]
+= Block API
+
+[[Block-차단하기]]
+=== 차단하기
+operation::block-controller-test/차단_하기[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+=== 차단 해제 하기
+operation::block-controller-test/차단_해제_하기[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+=== 차단한 유저 조회 List
+operation::block-controller-test/차단한_유저_목록[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+=== 차단한 유저 정보 조회
+operation::block-controller-test/차단한_유저_정보_목록[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
diff --git a/src/docs/asciidoc/Board-API.adoc b/src/docs/asciidoc/Board-API.adoc
new file mode 100644
index 00000000..87e99f01
--- /dev/null
+++ b/src/docs/asciidoc/Board-API.adoc
@@ -0,0 +1,27 @@
+[[Board-API]]
+= Board API
+
+[[Board-게시글-생성]]
+== Board 게시글 생성
+operation::board-controller-test/create_board[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+---
+
+[[Board-게시글-수정]]
+== Board 게시글 수정
+operation::board-controller-test/update_board[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+---
+
+[[Board-게시글-삭제]]
+== Board 게시글 삭제
+operation::board-controller-test/delete_board[snippets='http-request,path-parameters,response-fields']
+---
+
+[[Board-게시글-전체-조회]]
+== Board 게시글 전체 조회
+operation::board-controller-test/get_board_all[snippets='http-request,path-parameters,http-response,response-fields']
+---
+
+[[Board-게시글-상제-조회]]
+== Board 게시글 상세 조회
+operation::board-controller-test/get_board_detail[snippets='http-request,path-parameters,http-response,response-fields']
+---
\ No newline at end of file
diff --git a/src/docs/asciidoc/BoardComment_API.adoc b/src/docs/asciidoc/BoardComment_API.adoc
new file mode 100644
index 00000000..f9a2ac25
--- /dev/null
+++ b/src/docs/asciidoc/BoardComment_API.adoc
@@ -0,0 +1,17 @@
+[[Board-Comment-API]]
+= Board Comment API
+
+[[Board-Comment-댓글-생성]]
+== Board Comment 댓글 생성
+operation::board-comment-controller-test/create_board_comment[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+---
+
+[[Board-Comment-댓글-삭제]]
+== Board Comment 댓글 삭제
+operation::board-comment-controller-test/delete_board_comment[snippets='http-request,path-parameters,response-fields']
+---
+
+[[Board-Comment-댓글-전체-조회]]
+== Board Comment 댓글 전체 조회
+operation::board-comment-controller-test/get_board_comment_all[snippets='http-request,path-parameters,http-response,response-fields']
+---
diff --git a/src/docs/asciidoc/Fire-API.adoc b/src/docs/asciidoc/Fire-API.adoc
new file mode 100644
index 00000000..554c28ac
--- /dev/null
+++ b/src/docs/asciidoc/Fire-API.adoc
@@ -0,0 +1,16 @@
+
+
+[[Fire-API]]
+= Fire API
+
+[[Fire-불던지기]]
+== 불 던지기
+operation::fire-controller-test/불_던지기[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+[[Fire-불던질사람조회]]
+== 불 던질 사람 조회
+operation::fire-controller-test/불_던질_사람_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
diff --git a/src/docs/asciidoc/Image-API.adoc b/src/docs/asciidoc/Image-API.adoc
new file mode 100644
index 00000000..488ce5be
--- /dev/null
+++ b/src/docs/asciidoc/Image-API.adoc
@@ -0,0 +1,17 @@
+[[Image-API]]
+= Image API
+
+[[Presigned-Url-발급]]
+== Presigned Url 발급
+operation::image-controller-test/create_presigned_url[snippets='http-request,http-response,request-fields,response-fields']
+
+=== imageFileExtension
+|===
+| input 형식
+
+| `JPG`
+
+| `JPEG`
+
+| `PNG`
+|===
\ No newline at end of file
diff --git a/src/docs/asciidoc/Mission-API.adoc b/src/docs/asciidoc/Mission-API.adoc
new file mode 100644
index 00000000..48dcbaaa
--- /dev/null
+++ b/src/docs/asciidoc/Mission-API.adoc
@@ -0,0 +1,47 @@
+
+
+[[Mission-API]]
+= Mission API
+
+[[Mission-생성]]
+== Mission 생성
+operation::mission-controller-test/미션_생성[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[Mission-조회]]
+== Mission 조회
+operation::mission-controller-test/미션_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[Mission-수정]]
+== Mission 수정
+operation::mission-controller-test/미션_수정[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[Mission-삭제]]
+== Mission 삭제
+operation::mission-controller-test/미션_삭제[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+
+[[Mission-추천]]
+== Mission 추천
+operation::mission-controller-test/미션_추천[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[Mission-종료]]
+== Mission 종료
+operation::mission-controller-test/미션_종료[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+
+[[Mission-설명-확인]]
+== Mission 설명 확인
+operation::mission-controller-test/미션_설명_확인[snippets='http-request,path-parameters,http-response,response-fields']
+
diff --git a/src/docs/asciidoc/MissionArchive-API.adoc b/src/docs/asciidoc/MissionArchive-API.adoc
new file mode 100644
index 00000000..fee04ef4
--- /dev/null
+++ b/src/docs/asciidoc/MissionArchive-API.adoc
@@ -0,0 +1,59 @@
+
+
+[[MissionArchive-API]]
+= MissionArchive API
+
+[[MissionArchive-인증하기]]
+== 미션 인증하기
+operation::mission-archive-controller-test/미션_인증하기[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[MissionArchive-재인증하기]]
+== 미션 재인증하기
+operation::mission-archive-controller-test/미션_재인증하기[snipipets='http-request,path-parameters,request-fields,http-response,response-fields']
+---
+
+[[MissionArchive-미션인증삭제]]
+== 미션 인증 삭제
+operation::mission-archive-controller-test/미션_인증_삭제[snipipets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[MissionArchive-나의미션인증조회]]
+== 나의 미션 인증 조회
+operation::mission-archive-controller-test/나의_미션_인증_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[MissionArchive-모임원미션인증조회]]
+== 모임원 미션 인증 조회
+operation::mission-archive-controller-test/모임원_미션_인증_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[MissionArchive-인증성공인원조회]]
+== 인증 성공 인원 조회 ( n/n명 )
+operation::mission-archive-controller-test/인증_성공_인원_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[MissionArchive-나의성공횟수조회]]
+== 나의 성공 횟수 조회 ( n/n번 )
+operation::mission-archive-controller-test/나의_성공_횟수_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[MissionArchive-미션인증물좋아요]]
+== 미션 인증물 좋아요
+operation::mission-archive-controller-test/미션_인증물_좋아요[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+
+[[MissionArchive-미션상태조회]]
+== 미션 상태 조회
+operation::mission-archive-controller-test/미션_상태_조회[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
diff --git a/src/docs/asciidoc/MissionArchiveComment_API.adoc b/src/docs/asciidoc/MissionArchiveComment_API.adoc
new file mode 100644
index 00000000..9ec5e7fa
--- /dev/null
+++ b/src/docs/asciidoc/MissionArchiveComment_API.adoc
@@ -0,0 +1,17 @@
+[[Mission-Comment-API]]
+= Mission Comment API
+
+[[Mission-Comment-댓글-생성]]
+== Mission Comment 댓글 생성
+operation::mission-comment-controller-test/create_mission_comment[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+---
+
+[[Mission-Comment-댓글-삭제]]
+== Mission Comment 댓글 삭제
+operation::mission-comment-controller-test/delete_mission_comment[snippets='http-request,path-parameters,response-fields']
+---
+
+[[MissionArchive-Comment-댓글-전체-조회]]
+== Mission Comment 댓글 전체 조회
+operation::mission-comment-controller-test/get_board_comment_all[snippets='http-request,path-parameters,http-response,response-fields']
+---
\ No newline at end of file
diff --git a/src/docs/asciidoc/MissionBoard-API.adoc b/src/docs/asciidoc/MissionBoard-API.adoc
new file mode 100644
index 00000000..95d17782
--- /dev/null
+++ b/src/docs/asciidoc/MissionBoard-API.adoc
@@ -0,0 +1,22 @@
+
+
+[[MissionBoard-API]]
+= MissionBoard API
+
+[[MissionBoard-단일미션인증조회]]
+== 단일 미션 인증 조회
+operation::mission-board-controller-test/단일_미션_인증_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
+
+[[MissionBoard-반복미션인증조회]]
+== 반복 미션 인증 조회
+operation::mission-board-controller-test/반복_미션_인증_조회[snipipets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
+
+[[MissionBoard-종료된인증조회]]
+== 종료된 인증 조회
+operation::mission-board-controller-test/종료된_인증_조회[snippets='http-request,path-parameters,http-response,response-fields']
+
+---
diff --git a/src/docs/asciidoc/MissionGatherBoard-API.adoc b/src/docs/asciidoc/MissionGatherBoard-API.adoc
new file mode 100644
index 00000000..8a29140e
--- /dev/null
+++ b/src/docs/asciidoc/MissionGatherBoard-API.adoc
@@ -0,0 +1,38 @@
+
+
+[[MissionGatherBoard-API]]
+= MissionGatherBoard API
+
+[[MissionBoard-미션-모아보기-단일]]
+== 미션_모아보기_단일_미션
+operation::mission-gather-controller-test/미션_모아보기_단일_미션[snippets='http-request,http-response,response-fields']
+---
+
+[[MissionBoard-미션-모아보기-반복]]
+== 미션_모아보기_반복_미션
+operation::mission-gather-controller-test/미션_모아보기_반복_미션[snippets='http-request,http-response,response-fields']
+
+---
+[[MissionBoard-팀별-미션-모아보기-단일]]
+== 팀별_미션_모아보기_단일_미션
+operation::mission-gather-controller-test/팀별_미션_모아보기_단일_미션[snippets='http-request,http-response,response-fields']
+---
+
+[[MissionBoard-팀별-미션-모아보기-반복]]
+== 팀별_미션_모아보기_반복_미션
+operation::mission-gather-controller-test/팀별_미션_모아보기_반복_미션[snippets='http-request,http-response,response-fields']
+
+---
+
+[[MissionBoard-미션-모아보기-팀별]]
+== 미션_모아보기_팀별
+operation::mission-gather-controller-test/미션_모아보기_팀별[snippets='http-request,http-response,response-fields']
+
+---
+
+
+[[MissionBoard-내가-속한-팀-조회]]
+== 내가_속한_팀_조회
+operation::mission-gather-controller-test/내가_속한_팀_조회[snippets='http-request,http-response,response-fields']
+
+---
diff --git a/src/docs/asciidoc/Mypage-API.adoc b/src/docs/asciidoc/Mypage-API.adoc
new file mode 100644
index 00000000..76317426
--- /dev/null
+++ b/src/docs/asciidoc/Mypage-API.adoc
@@ -0,0 +1,46 @@
+[[Mypage-API]]
+= Mypage API
+
+[[Mypage-로그아웃]]
+== Mypage 로그아웃
+operation::mypage-controller-test/sign_out[snippets='http-request,http-response,response-fields']
+
+---
+
+[[Mypage-회원탈퇴]]
+== Mypage 회원탈퇴
+operation::mypage-controller-test/withdraw[snippets='path-parameters,http-request,request-fields,http-response,response-fields']
+
+[[Mypage-전체-조회]]
+== Mypage 전체 조회
+operation::mypage-controller-test/get_mypage[snippets='http-request,http-response,response-fields']
+
+[[Mypage-프로필-조회]]
+== Mypage 프로필 조회
+operation::mypage-controller-test/get_profile[snippets='http-request,http-response,response-fields']
+IMPORTANT: Response Fields (profileImage, nickName, introduction)이 "undef" 인 경우는 사용자가 아직 입력을 안 한 경우임
+
+[[Mypage-프로필-수정]]
+== Mypage 프로필 수정
+operation::mypage-controller-test/update_profile[snippets='http-request,http-response,request-fields,response-fields']
+IMPORTANT: Request Fileds는 반드시 세 값이 모두 들어가야 하는 것이 아니고, 업데이트할 값만 입력해도 됨, 따라서 아래와 같은 예시 가능 +
+
+ PUT /api/mypage/profile HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer ACCESS_TOKEN
+Content-Length: 104
+Host: localhost:8080
+{
+"profileImage" : "PROFILE_IMAGE_URL"
+}
+
+
+[[Mypage-알람설정_조회]]
+== Mypage 알람설정 조회
+operation::mypage-controller-test/get_alarm[snippets='http-request,http-response,response-fields']
+
+
+[[Mypage-알람설정_수정]]
+== Mypage 알람설정 수정
+operation::mypage-controller-test/update_alarm[snippets='http-request,http-response,request-parameters,response-fields']
+
diff --git a/src/docs/asciidoc/Overview.adoc b/src/docs/asciidoc/Overview.adoc
new file mode 100644
index 00000000..f9d7db68
--- /dev/null
+++ b/src/docs/asciidoc/Overview.adoc
@@ -0,0 +1,169 @@
+[[Overview-Response]]
+== ErrorCode
+
+
+=== Error Response DTO
+|===
+| isSuccess(Boolean) | timeStamp(LocalDateTime) | errorCode(String) | message(String)
+|===
+
+=== Common ErrorCode
+|===
+| ErrorCode | Scope | Description
+
+| `400`
+| 전체
+| 요청 형식 자체가 틀리거나 권한이 없음
+
+| `403`
+| 전체
+| 접근 권한이 존재하지 않음 (로그인 안한 경우)
+
+| `405`
+| 전체
+| HTTP 메서드가 리소스에서 허용되지 않음
+
+| `500`
+| 전체
+| 서버오류
+
+| `J0001`
+| 전체
+| 예상치 못한 오류
+
+| `J0002`
+| 전체
+| 잘못된 JWT 서명
+
+|`J0003`
+| 전체
+| 만료된 토큰
+
+| `J0004`
+| 전체
+| 지원되지 않는 토큰
+
+| `J0005`
+| 전체
+| 접근이 거부됨
+
+| `J0006`
+| 전체
+| 토큰이 잘못됨
+
+| `J0007`
+| 전체
+| 추가 정보 입력 (닉네임) 안함
+
+| `J0008`
+| 재발급 시
+| 유효하지 않은 refresh Token
+|===
+
+=== User / Auth ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `U0001`
+| 전체
+| 해당 유저 존재하지 않음
+
+|`AU0001`
+| 로그인할 때
+| 이미 다른 소셜 플랫폼으로 가입함
+
+| `AU0002`
+| 로그인할 때
+| 입력 토큰이 유효하지 않음
+
+| `AU0003`
+| 로그인할 때
+| appId가 일치하지 않음 (유효하지 않음)
+
+| `AU0004`
+| 회원가입할 때
+| 닉네임이 중복됨
+|===
+
+=== Team ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `T0001`
+| API PATH에 teamId가 있을 때
+| teamId가 유효하지 않음 (존재하지 않거나, 해당 유저가 그 팀에 속해있지 않거나)
+
+| `T0002`
+| 소모임을 수정, 삭제하려고 할 때
+| 소모임장이 아님 (권한 없음)
+
+| `T0003`
+| 소모임을 가입할 때
+| 소모임을 이미 탈퇴함
+
+| `T0004`
+| 소모임을 가입할 때
+| 소모임을 이미 가입함
+
+| `T0005`
+| 소모임을 가입할 때
+| 소모임이 삭제됨
+
+|===
+
+=== MyPage ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `MP0001`
+| 알람을 수정할 때
+| 알람 입력값이 유효하지 않음
+
+| `MP0002`
+| 회원 탈퇴할 때
+| 탈퇴되지 않은 소모임이 있음
+|===
+
+=== Board ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `B0001`
+| API PATH에 boardId가 있을 때
+| boardId가 유효하지 않음 (존재하지 않음)
+
+| `B0002`
+| 게시글을 수정, 삭제하려고 할 때
+| 작성자가 아님 (권한 없음)
+|===
+
+=== BoardComment ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `BC0001`
+| API PATH에 boardId가 있을 때
+| boardId가 유효하지 않음 (존재하지 않음)
+
+| `BC0002`
+| 게시글을 수정, 삭제하려고 할 때
+| 작성자가 아님 (권한 없음)
+|===
+
+
+
+=== Mission ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `M0001`
+| 미션 생성할 때
+| 소모임장이 아님
+
+| `M0002`
+| API PATH에 missionId가 있을 때
+| missionId가 유효하지 않음
+|===
+
+=== MissionArchive ErrorCode
+|===
+| ErrorCode | Scope | Description
+| `MA0001`
+| 미션 인증물 조회할 때
+| 미션 missionId 또는 teamId가 유효하지 않음
+|===
+
diff --git a/src/docs/asciidoc/Report-API.adoc b/src/docs/asciidoc/Report-API.adoc
new file mode 100644
index 00000000..c8ea808e
--- /dev/null
+++ b/src/docs/asciidoc/Report-API.adoc
@@ -0,0 +1,10 @@
+
+
+[[Report-API]]
+= Report API
+
+[[Report-신고하기]]
+=== 신고하기
+operation::report-controller-test/신고하기[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
diff --git a/src/docs/asciidoc/Team-API.adoc b/src/docs/asciidoc/Team-API.adoc
new file mode 100644
index 00000000..a7add657
--- /dev/null
+++ b/src/docs/asciidoc/Team-API.adoc
@@ -0,0 +1,66 @@
+[[Team-API]]
+= Team API
+
+[[Team-소모임-개설]]
+== Team 소모임 개설
+operation::team-controller-test/create_team[snippets='http-request,request-fields,http-response,response-fields']
+
+== Team Category
+|===
+| Category | Description
+
+| `SPORTS`
+| 스포츠/운동
+
+| `HABIT`
+| 생활습관 개선
+
+| `TEST`
+| 시험/취업준비
+
+| `STUDY`
+| 스터디/공부
+
+| `READING`
+| 독서
+
+| `ETC`
+| 그외 자기계발
+|===
+
+
+[[Team-소모임-가입]]
+== Team 소모임 가입
+operation::team-controller-test/sign-in_team[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-조회]]
+== Team 소모임 조회
+operation::team-controller-test/get_team[snippets='http-request,http-response,response-fields']
+
+[[Team-목표보드-조회]]
+== Team 목표보드_소모임 단건_조회
+operation::team-controller-test/get_team_detail[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-수정]]
+== Team 소모임 수정
+operation::team-controller-test/update_team[snippets='http-request,path-parameters,http-response,request-fields,response-fields']
+
+[[Team-소모임-수정전-조회]]
+== Team 소모임 수정 전 조회
+operation::team-controller-test/get_current_status[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-삭제전-조회]]
+== Team 소모임 삭제 전 조회
+operation::team-controller-test/review_team[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-강제종료]]
+== Team 소모임 강제종료
+operation::team-controller-test/disband_team[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-탈퇴]]
+== Team 소모임원 강제종료
+operation::team-controller-test/withdraw_team[snippets='http-request,path-parameters,http-response,response-fields']
+
+[[Team-소모임-개수_이름-조회]]
+== Team 소모임 개수 이름 조회
+operation::team-controller-test/get_team_count[snippets='http-request,path-parameters,http-response,response-fields']
diff --git a/src/docs/asciidoc/TeamScore-API.adoc b/src/docs/asciidoc/TeamScore-API.adoc
new file mode 100644
index 00000000..12268c8e
--- /dev/null
+++ b/src/docs/asciidoc/TeamScore-API.adoc
@@ -0,0 +1,10 @@
+
+
+[[TeamScore-API]]
+= TeamScore API
+
+[[TeamScore-팀별-불-레벨-경험치-조회]]
+=== 팀별_불_레벨_경험치_조회
+operation::team-score-controller-test/팀별_불_레벨_경험치_조회[snippets='http-request,path-parameters,request-fields,http-response,response-fields']
+
+---
diff --git a/src/docs/asciidoc/api.adoc b/src/docs/asciidoc/api.adoc
new file mode 100644
index 00000000..0addb28b
--- /dev/null
+++ b/src/docs/asciidoc/api.adoc
@@ -0,0 +1,41 @@
+= Moing API Doc
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:toc: left
+:toclevels: 1
+:sectlinks:
+
+include::Overview.adoc[]
+
+include::Image-API.adoc[]
+
+include::Auth-API.adoc[]
+
+include::Mypage-API.adoc[]
+
+include::AlarmHistory-API.adoc[]
+
+include::Team-API.adoc[]
+
+include::Mission-API.adoc[]
+
+include::MissionArchive-API.adoc[]
+
+include::MissionArchiveComment_API.adoc[]
+
+include::MissionBoard-API.adoc[]
+
+include::MissionGatherBoard-API.adoc[]
+
+include::Fire-API.adoc[]
+
+include::Board-API.adoc[]
+
+include::BoardComment_API.adoc[]
+
+include::TeamScore-API.adoc[]
+
+include::Report-API.adoc[]
+
+include::Block-API.adoc[]
\ No newline at end of file
diff --git a/src/main/java/com/moing/backend/BackendApplication.java b/src/main/java/com/moing/backend/BackendApplication.java
new file mode 100644
index 00000000..d60008aa
--- /dev/null
+++ b/src/main/java/com/moing/backend/BackendApplication.java
@@ -0,0 +1,19 @@
+package com.moing.backend;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@EnableAsync
+@EnableFeignClients
+@SpringBootApplication
+@EnableJpaAuditing
+public class BackendApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BackendApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/alarm/application/MultiAlarmEventUseCase.java b/src/main/java/com/moing/backend/domain/alarm/application/MultiAlarmEventUseCase.java
new file mode 100644
index 00000000..f9eed763
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/alarm/application/MultiAlarmEventUseCase.java
@@ -0,0 +1,31 @@
+package com.moing.backend.domain.alarm.application;
+
+import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper;
+import com.moing.backend.domain.history.application.service.SaveMultiAlarmHistoryUseCase;
+import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent;
+import com.moing.backend.global.config.fcm.dto.request.MultiRequest;
+import com.moing.backend.global.config.fcm.service.MultiMessageSender;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+
+@Component
+@RequiredArgsConstructor
+public class MultiAlarmEventUseCase {
+
+ private final MultiMessageSender multiMessageSender;
+ private final SaveMultiAlarmHistoryUseCase saveMultiAlarmHistoryUseCase;
+
+ @Async
+ @EventListener
+ public void onMultiAlarmEvent(MultiFcmEvent event) {
+ if (event.getIdAndTokensByPush().isPresent() && !event.getIdAndTokensByPush().get().isEmpty()) {
+ multiMessageSender.send(new MultiRequest(event.getIdAndTokensByPush().get(), event.getTitle(), event.getBody(), event.getIdInfo(), event.getName(), event.getAlarmType(), event.getPath()));
+ }
+ if (event.getIdAndTokensBySave().isPresent() && !event.getIdAndTokensBySave().get().isEmpty()) {
+ saveMultiAlarmHistoryUseCase.saveAlarmHistories(AlarmHistoryMapper.getMemberIds(event.getIdAndTokensBySave().get()), event.getIdInfo(), event.getTitle(), event.getBody(), event.getName(), event.getAlarmType(), event.getPath());
+ }
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/alarm/application/SingleAlarmEventUseCase.java b/src/main/java/com/moing/backend/domain/alarm/application/SingleAlarmEventUseCase.java
new file mode 100644
index 00000000..d2579200
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/alarm/application/SingleAlarmEventUseCase.java
@@ -0,0 +1,29 @@
+package com.moing.backend.domain.alarm.application;
+
+import com.moing.backend.domain.history.application.service.SaveSingleAlarmHistoryUseCase;
+import com.moing.backend.global.config.fcm.dto.event.SingleFcmEvent;
+import com.moing.backend.global.config.fcm.dto.request.SingleRequest;
+import com.moing.backend.global.config.fcm.service.SingleMessageSender;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+
+@Component
+@RequiredArgsConstructor
+public class SingleAlarmEventUseCase {
+
+ private final SingleMessageSender singleMessageSender;
+ private final SaveSingleAlarmHistoryUseCase saveSingleAlarmHistoryUseCase;
+
+ @Async
+ @EventListener
+ public void onSingleAlarmEvent(SingleFcmEvent event) {
+ if (event.isAlarmPush() && !event.getMember().isSignOut()) { //알림 on, 로그아웃 안함
+ singleMessageSender.send(new SingleRequest(event.getMember().getFcmToken(), event.getTitle(), event.getBody(), event.getMember().getMemberId(), event.getIdInfo(), event.getName(), event.getAlarmType(), event.getPath()));
+ }
+ saveSingleAlarmHistoryUseCase.saveAlarmHistory(event.getMember().getMemberId(), event.getIdInfo(), event.getTitle(), event.getBody(), event.getName(), event.getAlarmType(), event.getPath());
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignInRequest.java b/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignInRequest.java
new file mode 100644
index 00000000..57ea1a77
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignInRequest.java
@@ -0,0 +1,19 @@
+package com.moing.backend.domain.auth.application.dto.request;
+
+import lombok.*;
+
+import javax.validation.constraints.NotBlank;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+public class SignInRequest {
+ @NotBlank(message="socialToken 을 입력해주세요.")
+ private String socialToken;
+
+ @NotBlank(message = "fcmToken 을 입력해주세요.")
+ private String fcmToken;
+
+}
+
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignUpRequest.java b/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignUpRequest.java
new file mode 100644
index 00000000..426b9da5
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/request/SignUpRequest.java
@@ -0,0 +1,25 @@
+package com.moing.backend.domain.auth.application.dto.request;
+
+import com.moing.backend.domain.member.domain.constant.Gender;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+public class SignUpRequest {
+
+ @NotBlank(message = "nickName 을 입력해주세요.")
+ @Size(min = 1, max = 10, message="nickName 은 최소 1개, 최대 10개의 문자만 입력 가능합니다.")
+ private String nickName;
+
+ private Gender gender;
+
+ private String birthDate;
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/request/TestRequest.java b/src/main/java/com/moing/backend/domain/auth/application/dto/request/TestRequest.java
new file mode 100644
index 00000000..372994b3
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/request/TestRequest.java
@@ -0,0 +1,16 @@
+package com.moing.backend.domain.auth.application.dto.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+public class TestRequest {
+ private String socialId;
+
+ private String fcmToken;
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/CheckNicknameResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/CheckNicknameResponse.java
new file mode 100644
index 00000000..e1a6b886
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/CheckNicknameResponse.java
@@ -0,0 +1,14 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+public class CheckNicknameResponse {
+ private Boolean isDuplicated;
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/GoogleUserResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/GoogleUserResponse.java
new file mode 100644
index 00000000..6856a0a9
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/GoogleUserResponse.java
@@ -0,0 +1,24 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class GoogleUserResponse {
+
+ private String aud;
+ private String sub;
+ private String email;
+ private String name;
+ private String picture;
+
+ public void adaptResponse() {
+ if(email.length() > 50) email = email.substring(0, 50);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoAccessTokenResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoAccessTokenResponse.java
new file mode 100644
index 00000000..99e0fb2a
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoAccessTokenResponse.java
@@ -0,0 +1,15 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.*;
+
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class KakaoAccessTokenResponse {
+ private String appId;
+}
+
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoUserResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoUserResponse.java
new file mode 100644
index 00000000..b2f2c636
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/KakaoUserResponse.java
@@ -0,0 +1,36 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.*;
+
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class KakaoUserResponse {
+ private Long id;
+ private Properties properties;
+ private KakaoAccount kakaoAccount;
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+ public static class Properties {
+ private String nickname;
+ }
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class KakaoAccount {
+ private String email;
+ }
+
+ public void adaptResponse() {
+ if(kakaoAccount.email.length() > 50) kakaoAccount.email = kakaoAccount.email.substring(0, 50);
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/ReissueTokenResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/ReissueTokenResponse.java
new file mode 100644
index 00000000..3f3cff58
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/ReissueTokenResponse.java
@@ -0,0 +1,25 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import com.moing.backend.global.response.TokenInfoResponse;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ReissueTokenResponse {
+
+ private String accessToken;
+ private String refreshToken;
+
+ public static ReissueTokenResponse from(TokenInfoResponse tokenInfoResponse) {
+ return ReissueTokenResponse.builder()
+ .accessToken(tokenInfoResponse.getAccessToken())
+ .refreshToken(tokenInfoResponse.getRefreshToken())
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/dto/response/SignInResponse.java b/src/main/java/com/moing/backend/domain/auth/application/dto/response/SignInResponse.java
new file mode 100644
index 00000000..ffb5b7ae
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/dto/response/SignInResponse.java
@@ -0,0 +1,26 @@
+package com.moing.backend.domain.auth.application.dto.response;
+
+import com.moing.backend.domain.member.domain.constant.RegistrationStatus;
+import com.moing.backend.global.response.TokenInfoResponse;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SignInResponse {
+ private String accessToken;
+ private String refreshToken;
+ private Boolean registrationStatus;
+
+ public static SignInResponse from(TokenInfoResponse tokenInfoResponse, RegistrationStatus registrationStatus) {
+ return SignInResponse.builder()
+ .accessToken(tokenInfoResponse.getAccessToken())
+ .refreshToken(tokenInfoResponse.getRefreshToken())
+ .registrationStatus(registrationStatus.equals(RegistrationStatus.COMPLETED))
+ .build();
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/CheckNicknameUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/CheckNicknameUseCase.java
new file mode 100644
index 00000000..7893e1b8
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/CheckNicknameUseCase.java
@@ -0,0 +1,20 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.auth.application.dto.response.CheckNicknameResponse;
+import com.moing.backend.domain.member.domain.service.MemberCheckService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class CheckNicknameUseCase {
+
+ private final MemberCheckService memberCheckService;
+
+ @Transactional(readOnly=true)
+ public CheckNicknameResponse checkNickname(String nickname){
+ boolean isDuplicated=memberCheckService.checkNickname(nickname);
+ return new CheckNicknameResponse(isDuplicated);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/MemberAuthUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/MemberAuthUseCase.java
new file mode 100644
index 00000000..6f5d1e2c
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/MemberAuthUseCase.java
@@ -0,0 +1,32 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.auth.exception.AccountAlreadyExistedException;
+import com.moing.backend.domain.member.domain.entity.Member;
+import com.moing.backend.domain.member.domain.service.MemberSaveService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class MemberAuthUseCase {
+
+ private final MemberSaveService memberSaveService;
+
+ public Member auth(String fcmToken, Member member, String providerInfo) {
+ member.updateFcmToken(fcmToken);
+ Member signInMember = memberSaveService.saveMember(member);
+ checkRegistration(signInMember, providerInfo);
+ return signInMember;
+ }
+
+
+ // 다른 플랫폼으로 가입했으면 에러 출력
+ private void checkRegistration(Member signInMember, String providerInfo) {
+ if(!providerInfo.contains((signInMember.getProvider().name().toLowerCase())))
+ throw new AccountAlreadyExistedException();
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/ReissueTokenUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/ReissueTokenUseCase.java
new file mode 100644
index 00000000..88714bab
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/ReissueTokenUseCase.java
@@ -0,0 +1,29 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.auth.application.dto.response.ReissueTokenResponse;
+import com.moing.backend.global.config.security.jwt.NotFoundRefreshToken;
+import com.moing.backend.global.config.security.jwt.TokenUtil;
+import com.moing.backend.global.response.TokenInfoResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ReissueTokenUseCase {
+
+ private final TokenUtil tokenUtil;
+
+ public ReissueTokenResponse reissueToken(String token) {
+ // refresh 토큰이 유효한지 확인
+ if (token != null && tokenUtil.verifyRefreshToken(token)) {
+ // 토큰 새로 받아오기
+ TokenInfoResponse newToken = tokenUtil.tokenReissue(token);
+
+ return ReissueTokenResponse.from(newToken);
+ }
+ throw new NotFoundRefreshToken();
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/SignInProvider.java b/src/main/java/com/moing/backend/domain/auth/application/service/SignInProvider.java
new file mode 100644
index 00000000..7b56a977
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/SignInProvider.java
@@ -0,0 +1,7 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.member.domain.entity.Member;
+
+public interface SignInProvider {
+ Member getUserData(String accessToken);
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/SignInUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/SignInUseCase.java
new file mode 100644
index 00000000..818395b2
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/SignInUseCase.java
@@ -0,0 +1,64 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.auth.application.dto.request.SignInRequest;
+import com.moing.backend.domain.auth.application.dto.response.SignInResponse;
+import com.moing.backend.domain.member.domain.constant.RegistrationStatus;
+import com.moing.backend.domain.member.domain.entity.Member;
+import com.moing.backend.domain.member.domain.service.MemberGetService;
+import com.moing.backend.global.config.security.jwt.TokenUtil;
+import com.moing.backend.global.config.security.util.AuthenticationUtil;
+import com.moing.backend.global.response.TokenInfoResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class SignInUseCase {
+
+ private final MemberAuthUseCase internalAuthService;
+ private final TokenUtil tokenUtil;
+ private final Map signInProviders;
+ private final MemberGetService memberGetService;
+
+ public SignInResponse signIn(SignInRequest signInRequest, String providerInfo) {
+ //1. 사용자 정보 가져오기
+ Member member = getUserDataFromPlatform(signInRequest.getSocialToken(), providerInfo);
+ //2. 로그인 및 회원가입
+ Member authenticatedMember = internalAuthService.auth(signInRequest.getFcmToken(), member, providerInfo);
+ //3. security 처리
+ AuthenticationUtil.makeAuthentication(authenticatedMember);
+ //4. token 만들기
+ TokenInfoResponse tokenResponse = tokenUtil.createToken(authenticatedMember, authenticatedMember.getRegistrationStatus().equals(RegistrationStatus.COMPLETED));
+ //5. refresh token 저장
+ tokenUtil.storeRefreshToken(authenticatedMember.getSocialId(), tokenResponse);
+
+ return SignInResponse.from(tokenResponse, authenticatedMember.getRegistrationStatus());
+ }
+
+ private Member getUserDataFromPlatform(String accessToken, String providerInfo) {
+ SignInProvider signInProvider = signInProviders.get(providerInfo+"SignIn");
+ if (signInProvider == null) {
+ throw new IllegalArgumentException("Unknown provider: " + providerInfo);
+ }
+ return signInProvider.getUserData(accessToken);
+ }
+
+ public SignInResponse testSignIn(String fcmToken, String socialId, String providerInfo) {
+ //1. 사용자 정보 가져오기
+ Member member = memberGetService.getMemberBySocialId(socialId);
+ //2. 로그인
+ Member authenticatedMember = internalAuthService.auth(fcmToken, member, providerInfo);
+ //3. security 처리
+ AuthenticationUtil.makeAuthentication(authenticatedMember);
+ //4. token 만들기
+ TokenInfoResponse tokenResponse = tokenUtil.createToken(authenticatedMember, authenticatedMember.getRegistrationStatus().equals(RegistrationStatus.COMPLETED));
+ //5. refresh token 저장
+ tokenUtil.storeRefreshToken(authenticatedMember.getSocialId(), tokenResponse);
+
+ return SignInResponse.from(tokenResponse, authenticatedMember.getRegistrationStatus());
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/SignUpUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/SignUpUseCase.java
new file mode 100644
index 00000000..f1e3d24d
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/SignUpUseCase.java
@@ -0,0 +1,46 @@
+package com.moing.backend.domain.auth.application.service;
+
+import com.moing.backend.domain.auth.application.dto.request.SignUpRequest;
+import com.moing.backend.domain.auth.application.dto.response.SignInResponse;
+import com.moing.backend.domain.auth.exception.NicknameDuplicationException;
+import com.moing.backend.domain.member.domain.constant.RegistrationStatus;
+import com.moing.backend.domain.member.domain.entity.Member;
+import com.moing.backend.domain.member.domain.service.MemberCheckService;
+import com.moing.backend.domain.member.domain.service.MemberGetService;
+import com.moing.backend.global.config.security.jwt.TokenUtil;
+import com.moing.backend.global.config.security.util.AuthenticationUtil;
+import com.moing.backend.global.response.TokenInfoResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class SignUpUseCase {
+
+ private final TokenUtil tokenUtil;
+ private final MemberGetService memberQueryService;
+ private final MemberCheckService memberCheckService;
+
+ public SignInResponse signUp(String token, SignUpRequest signUpRequest) {
+
+ //1. 유저 찾기
+ String socialId = tokenUtil.getSocialId(token);
+ Member member = memberQueryService.getMemberBySocialId(socialId);
+ //2. signUp 처리
+ String nickName=signUpRequest.getNickName();
+ if(memberCheckService.checkNickname(nickName)) throw new NicknameDuplicationException(); //닉네임 중복검사 (이중체크)
+ member.signUp(signUpRequest);
+ //3. security 처리
+ AuthenticationUtil.makeAuthentication(member);
+ //4. token 만들기
+ TokenInfoResponse tokenResponse = tokenUtil.createToken(member, member.getRegistrationStatus().equals(RegistrationStatus.COMPLETED));
+ //5. refresh token 저장
+ tokenUtil.storeRefreshToken(member.getSocialId(), tokenResponse);
+
+ return SignInResponse.from(tokenResponse, member.getRegistrationStatus());
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/WithdrawProvider.java b/src/main/java/com/moing/backend/domain/auth/application/service/WithdrawProvider.java
new file mode 100644
index 00000000..b67996b0
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/WithdrawProvider.java
@@ -0,0 +1,7 @@
+package com.moing.backend.domain.auth.application.service;
+
+import java.io.IOException;
+
+public interface WithdrawProvider {
+ void withdraw(String token) throws IOException;
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleSignInUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleSignInUseCase.java
new file mode 100644
index 00000000..323ebd51
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleSignInUseCase.java
@@ -0,0 +1,27 @@
+package com.moing.backend.domain.auth.application.service.apple;
+
+import com.moing.backend.domain.auth.application.service.SignInProvider;
+import com.moing.backend.domain.member.application.mapper.MemberMapper;
+import com.moing.backend.domain.member.domain.entity.Member;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service("appleSignIn")
+@AllArgsConstructor
+public class AppleSignInUseCase implements SignInProvider {
+
+ private final AppleTokenUseCase appleTokenUseCase;
+ private final MemberMapper memberMapper;
+
+
+ public Member getUserData(String identityToken) {
+ Jws oidcTokenJws = appleTokenUseCase.sigVerificationAndGetJws(identityToken);
+
+ String socialId = oidcTokenJws.getBody().getSubject();
+ String email = (String) oidcTokenJws.getBody().get("email");
+
+ return MemberMapper.createAppleMember(socialId, email);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleTokenUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleTokenUseCase.java
new file mode 100644
index 00000000..4a5db82e
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleTokenUseCase.java
@@ -0,0 +1,97 @@
+package com.moing.backend.domain.auth.application.service.apple;
+
+import com.moing.backend.domain.auth.application.service.apple.utils.AppleClient;
+import com.moing.backend.domain.auth.application.service.apple.utils.Keys;
+import com.moing.backend.domain.auth.exception.TokenInvalidException;
+import com.moing.backend.global.exception.InternalServerErrorException;
+import io.jsonwebtoken.*;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.math.BigInteger;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Base64;
+
+@Component
+@RequiredArgsConstructor
+public class AppleTokenUseCase {
+
+ @Value("${oauth2.apple.clientId}")
+ private String appId;
+
+ private final AppleClient appleClient;
+
+ public Jws sigVerificationAndGetJws(String unverifiedToken) {
+
+ try {
+ String kid = getKidFromUnsignedTokenHeader(
+ unverifiedToken,
+ "https://appleid.apple.com",
+ appId);
+
+ Keys keys = appleClient.getKeys();
+ Keys.PubKey pubKey = keys.getKeys().stream()
+ .filter((key) -> key.getKid().equals(kid))
+ .findAny()
+ .orElseThrow(TokenInvalidException::new);
+
+ return getOIDCTokenJws(unverifiedToken, pubKey.getN(), pubKey.getE());
+ } catch (Exception e) {
+ throw new TokenInvalidException();
+ }
+ }
+
+
+ private Jws getOIDCTokenJws(String token, String modulus, String exponent) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(getRSAPublicKey(modulus, exponent))
+ .build()
+ .parseClaimsJws(token);
+ } catch (Exception e) {
+ throw new TokenInvalidException();
+ }
+ }
+
+ private Key getRSAPublicKey(String modulus, String exponent) {
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
+ byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
+ BigInteger n = new BigInteger(1, decodeN);
+ BigInteger e = new BigInteger(1, decodeE);
+
+ RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
+ return keyFactory.generatePublic(keySpec);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new InternalServerErrorException("Key creation failed");
+ }
+ }
+
+ private String getKidFromUnsignedTokenHeader(String token, String iss, String aud) {
+ return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get("kid");
+ }
+
+ private Jwt getUnsignedTokenClaims(String token, String iss, String aud) {
+ try {
+ return Jwts.parserBuilder()
+ .requireAudience(aud)
+ .requireIssuer(iss)
+ .build()
+ .parseClaimsJwt(getUnsignedToken(token));
+ } catch (Exception e) {
+ throw new TokenInvalidException();
+ }
+ }
+
+ private String getUnsignedToken(String token) {
+ String[] splitToken = token.split("\\.");
+ if (splitToken.length != 3) throw new TokenInvalidException();
+ return splitToken[0] + "." + splitToken[1] + ".";
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleWithdrawUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleWithdrawUseCase.java
new file mode 100644
index 00000000..12ff5133
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/AppleWithdrawUseCase.java
@@ -0,0 +1,75 @@
+package com.moing.backend.domain.auth.application.service.apple;
+
+import com.moing.backend.domain.auth.application.service.WithdrawProvider;
+import com.moing.backend.domain.auth.application.service.apple.utils.AppleClient;
+import com.moing.backend.domain.auth.application.service.apple.utils.AppleToken;
+import com.moing.backend.global.config.sns.AppleConfig;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Service("appleWithdraw")
+@RequiredArgsConstructor
+public class AppleWithdrawUseCase implements WithdrawProvider {
+
+ @Value("${oauth2.apple.keyId}")
+ private String keyId;
+
+ @Value("${oauth2.apple.teamId}")
+ private String teamId;
+
+ @Value("${oauth2.apple.clientId}")
+ private String clientId;
+
+ private final AppleClient appleClient;
+ private final AppleConfig appleConfig;
+
+ public void withdraw(String token) throws IOException {
+ AppleToken.Response response = generateAuthToken(token);
+
+ if (response.getAccess_token() != null) {
+ appleClient.revoke(AppleToken.RevokeRequest.of(
+ clientId,
+ createClientSecret(),
+ response.getAccess_token()
+ )
+ );
+ }
+ }
+
+ public AppleToken.Response generateAuthToken(String authorizationCode) throws IOException {
+
+ return appleClient.getToken(AppleToken.Request.of(
+ authorizationCode,
+ clientId,
+ createClientSecret(),
+ "authorization_code"
+ ));
+ }
+
+ public String createClientSecret() throws IOException {
+ Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant());
+ Map jwtHeader = new HashMap<>();
+ jwtHeader.put("kid", keyId);
+ jwtHeader.put("alg", "ES256");
+
+ return Jwts.builder()
+ .setHeaderParams(jwtHeader)
+ .setIssuer(teamId)
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(expirationDate)
+ .setAudience("https://appleid.apple.com")
+ .setSubject(clientId)
+ .signWith(appleConfig.applePrivateKey(), SignatureAlgorithm.ES256)
+ .compact();
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleClient.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleClient.java
new file mode 100644
index 00000000..68686358
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleClient.java
@@ -0,0 +1,17 @@
+package com.moing.backend.domain.auth.application.service.apple.utils;
+
+import com.moing.backend.global.utils.FeignClientConfig;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@FeignClient(name = "appleClient", url = "https://appleid.apple.com/auth", configuration = FeignClientConfig.class)
+public interface AppleClient {
+ @GetMapping(value = "/keys")
+ Keys getKeys();
+ @PostMapping(value = "/token", consumes = "application/x-www-form-urlencoded")
+ AppleToken.Response getToken(AppleToken.Request request);
+
+ @PostMapping(value = "/revoke", consumes = "application/x-www-form-urlencoded")
+ void revoke(AppleToken.RevokeRequest request);
+}
\ No newline at end of file
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleToken.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleToken.java
new file mode 100644
index 00000000..2fabd74a
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/AppleToken.java
@@ -0,0 +1,47 @@
+package com.moing.backend.domain.auth.application.service.apple.utils;
+
+import lombok.Getter;
+
+public class AppleToken {
+
+ public static class Request {
+ private String code;
+ private String client_id;
+ private String client_secret;
+ private String grant_type;
+
+ public static Request of(String code, String clientId, String clientSecret, String grantType) {
+ Request request = new Request();
+ request.code = code;
+ request.client_id = clientId;
+ request.client_secret = clientSecret;
+ request.grant_type = grantType;
+ return request;
+ }
+ }
+
+ @Getter
+ public static class Response {
+ private String access_token;
+ private String expires_in;
+ private String id_token;
+ private String refresh_token;
+ private String token_type;
+ private String error;
+ }
+
+ @Getter
+ public static class RevokeRequest {
+ private String client_id;
+ private String client_secret;
+ private String token;
+
+ public static RevokeRequest of(String clientId, String clientSecret, String token) {
+ RevokeRequest request = new RevokeRequest();
+ request.client_id = clientId;
+ request.client_secret = clientSecret;
+ request.token = token;
+ return request;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/Keys.java b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/Keys.java
new file mode 100644
index 00000000..9261d011
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/apple/utils/Keys.java
@@ -0,0 +1,27 @@
+package com.moing.backend.domain.auth.application.service.apple.utils;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class Keys {
+
+ private List keys;
+
+ @Data
+ public static class PubKey{
+ private String alg;
+
+ private String e;
+
+ private String kid;
+
+ private String kty;
+
+ private String n;
+
+ private String use;
+ }
+}
+
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleSignInUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleSignInUseCase.java
new file mode 100644
index 00000000..69993fd4
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleSignInUseCase.java
@@ -0,0 +1,39 @@
+package com.moing.backend.domain.auth.application.service.google;
+
+import com.moing.backend.domain.auth.application.dto.response.GoogleUserResponse;
+import com.moing.backend.domain.auth.application.service.SignInProvider;
+import com.moing.backend.domain.auth.exception.TokenInvalidException;
+import com.moing.backend.domain.member.application.mapper.MemberMapper;
+import com.moing.backend.domain.member.domain.entity.Member;
+import com.moing.backend.global.exception.InternalServerErrorException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+@Service("googleSignIn")
+@RequiredArgsConstructor
+public class GoogleSignInUseCase implements SignInProvider {
+
+ private final WebClient webClient;
+ private final GoogleTokenUseCase googleTokenUseCase;
+
+ public Member getUserData(String accessToken) {
+ GoogleUserResponse googleUserResponse = webClient.get()
+ .uri("https://oauth2.googleapis.com/tokeninfo", builder -> builder.queryParam("id_token", accessToken).build())
+ .retrieve()
+ .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new TokenInvalidException()))
+ .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new InternalServerErrorException("Google Internal Server Error ")))
+ .bodyToMono(GoogleUserResponse.class)
+ .block();
+
+ if (googleUserResponse != null) {
+ googleTokenUseCase.verifyAccessToken(googleUserResponse.getAud());
+ googleUserResponse.adaptResponse();
+ return MemberMapper.createGoogleMember(googleUserResponse);
+ }
+ throw new TokenInvalidException();
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleTokenUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleTokenUseCase.java
new file mode 100644
index 00000000..39bfb01b
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleTokenUseCase.java
@@ -0,0 +1,25 @@
+package com.moing.backend.domain.auth.application.service.google;
+
+import com.moing.backend.domain.auth.exception.AppIdInvalidException;
+import com.moing.backend.domain.auth.exception.TokenInvalidException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+
+@Component
+@RequiredArgsConstructor
+public class GoogleTokenUseCase {
+
+ @Value("${oauth2.google.appId}")
+ private String appId;
+
+ public void verifyAccessToken(String aud) {
+ String extractedAppId = Arrays.stream(aud.split("-"))
+ .findFirst()
+ .orElseThrow(TokenInvalidException::new);
+
+ if (!appId.equals(extractedAppId)) throw new AppIdInvalidException();
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleWithdrawUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleWithdrawUseCase.java
new file mode 100644
index 00000000..c28d4bf9
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/google/GoogleWithdrawUseCase.java
@@ -0,0 +1,20 @@
+package com.moing.backend.domain.auth.application.service.google;
+
+import com.moing.backend.domain.auth.application.service.WithdrawProvider;
+import com.moing.backend.domain.auth.application.service.google.utils.GoogleClient;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+
+@Service("googleWithdraw")
+@RequiredArgsConstructor
+public class GoogleWithdrawUseCase implements WithdrawProvider {
+
+ private final GoogleClient googleClient;
+
+ public void withdraw(String token) throws IOException {
+ googleClient.revoke(token);
+ }
+
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/google/utils/GoogleClient.java b/src/main/java/com/moing/backend/domain/auth/application/service/google/utils/GoogleClient.java
new file mode 100644
index 00000000..af6653ee
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/google/utils/GoogleClient.java
@@ -0,0 +1,12 @@
+package com.moing.backend.domain.auth.application.service.google.utils;
+
+import com.moing.backend.global.utils.FeignClientConfig;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@FeignClient(name = "googleClient", url = "https://oauth2.googleapis.com", configuration = FeignClientConfig.class)
+public interface GoogleClient {
+ @PostMapping(value = "/revoke", consumes = "application/x-www-form-urlencoded")
+ void revoke(@RequestParam("token") String token);
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoSignInUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoSignInUseCase.java
new file mode 100644
index 00000000..7e955df4
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoSignInUseCase.java
@@ -0,0 +1,39 @@
+package com.moing.backend.domain.auth.application.service.kakao;
+
+import com.moing.backend.domain.auth.application.dto.response.KakaoUserResponse;
+import com.moing.backend.domain.auth.application.service.SignInProvider;
+import com.moing.backend.domain.member.application.mapper.MemberMapper;
+import com.moing.backend.domain.auth.exception.TokenInvalidException;
+import com.moing.backend.domain.member.domain.entity.Member;
+import com.moing.backend.global.exception.InternalServerErrorException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+@Service("kakaoSignIn")
+@RequiredArgsConstructor
+public class KakaoSignInUseCase implements SignInProvider {
+
+ private final WebClient webClient;
+ private final KakaoTokenUseCase kakaoTokenUseCase;
+
+ public Member getUserData(String accessToken) {
+
+ kakaoTokenUseCase.verifyAccessToken(accessToken);
+
+
+ KakaoUserResponse kakaoUserResponse = webClient.get()
+ .uri("https://kapi.kakao.com/v2/user/me")
+ .headers(h -> h.setBearerAuth(accessToken))
+ .retrieve()
+ .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new TokenInvalidException()))
+ .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new InternalServerErrorException("Kakao Internal Server Error")))
+ .bodyToMono(KakaoUserResponse.class)
+ .block();
+ kakaoUserResponse.adaptResponse();
+
+ return MemberMapper.createKakaoMember(kakaoUserResponse);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoTokenUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoTokenUseCase.java
new file mode 100644
index 00000000..802c0828
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoTokenUseCase.java
@@ -0,0 +1,37 @@
+package com.moing.backend.domain.auth.application.service.kakao;
+
+import com.moing.backend.domain.auth.application.dto.response.KakaoAccessTokenResponse;
+import com.moing.backend.domain.auth.exception.AppIdInvalidException;
+import com.moing.backend.domain.auth.exception.TokenInvalidException;
+import com.moing.backend.global.exception.InternalServerErrorException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+@Component
+@RequiredArgsConstructor
+public class KakaoTokenUseCase {
+
+ @Value("${oauth2.kakao.appId}")
+ private String appId;
+
+ private final WebClient webClient;
+
+ public void verifyAccessToken(String accessToken) {
+
+ KakaoAccessTokenResponse kakaoAccessTokenResponse = webClient.get()
+ .uri("https://kapi.kakao.com/v1/user/access_token_info")
+ .headers(h -> h.setBearerAuth(accessToken))
+ .retrieve()
+ .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new TokenInvalidException()))
+ .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new InternalServerErrorException("Kakao Internal Server Error ")))
+ .bodyToMono(KakaoAccessTokenResponse.class)
+ .block();
+
+ if (!kakaoAccessTokenResponse.getAppId().equals(appId)) throw new AppIdInvalidException();
+
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoWithdrawUseCase.java b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoWithdrawUseCase.java
new file mode 100644
index 00000000..f8e70689
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/KakaoWithdrawUseCase.java
@@ -0,0 +1,20 @@
+package com.moing.backend.domain.auth.application.service.kakao;
+
+import com.moing.backend.domain.auth.application.service.WithdrawProvider;
+import com.moing.backend.domain.auth.application.service.kakao.utils.KakaoClient;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+
+@Service("kakaoWithdraw")
+@RequiredArgsConstructor
+public class KakaoWithdrawUseCase implements WithdrawProvider {
+
+ private final KakaoClient kakaoClient;
+
+ public void withdraw(String token) throws IOException {
+
+ kakaoClient.unlinkUser("Bearer " + token);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoClient.java b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoClient.java
new file mode 100644
index 00000000..3de222fe
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoClient.java
@@ -0,0 +1,12 @@
+package com.moing.backend.domain.auth.application.service.kakao.utils;
+
+import com.moing.backend.global.utils.FeignClientConfig;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+@FeignClient(name = "kakaoClient", url = "https://kapi.kakao.com", configuration = FeignClientConfig.class)
+public interface KakaoClient {
+ @PostMapping("/v1/user/unlink")
+ KakaoUnlinkResponse unlinkUser(@RequestHeader("Authorization") String accessToken);
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoUnlinkResponse.java b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoUnlinkResponse.java
new file mode 100644
index 00000000..de0e8333
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/application/service/kakao/utils/KakaoUnlinkResponse.java
@@ -0,0 +1,8 @@
+package com.moing.backend.domain.auth.application.service.kakao.utils;
+
+import lombok.Getter;
+
+@Getter
+public class KakaoUnlinkResponse {
+ private String id;
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/AccountAlreadyExistedException.java b/src/main/java/com/moing/backend/domain/auth/exception/AccountAlreadyExistedException.java
new file mode 100644
index 00000000..d6c44e68
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/AccountAlreadyExistedException.java
@@ -0,0 +1,12 @@
+package com.moing.backend.domain.auth.exception;
+
+import com.moing.backend.global.response.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public class AccountAlreadyExistedException extends AuthException {
+ public AccountAlreadyExistedException() {
+ super(ErrorCode.ACCOUNT_ALREADY_EXIST,
+ HttpStatus.UNAUTHORIZED);
+ }
+}
+
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/AppIdInvalidException.java b/src/main/java/com/moing/backend/domain/auth/exception/AppIdInvalidException.java
new file mode 100644
index 00000000..6a48c7bb
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/AppIdInvalidException.java
@@ -0,0 +1,11 @@
+package com.moing.backend.domain.auth.exception;
+
+import com.moing.backend.global.response.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public class AppIdInvalidException extends AuthException{
+ public AppIdInvalidException() {
+ super(ErrorCode.APPID_INVALID_ERROR,
+ HttpStatus.CONFLICT);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/AuthException.java b/src/main/java/com/moing/backend/domain/auth/exception/AuthException.java
new file mode 100644
index 00000000..8dde5a90
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/AuthException.java
@@ -0,0 +1,11 @@
+package com.moing.backend.domain.auth.exception;
+
+import com.moing.backend.global.exception.ApplicationException;
+import com.moing.backend.global.response.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public abstract class AuthException extends ApplicationException {
+ protected AuthException(ErrorCode errorCode, HttpStatus httpStatus) {
+ super(errorCode, httpStatus);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/NicknameDuplicationException.java b/src/main/java/com/moing/backend/domain/auth/exception/NicknameDuplicationException.java
new file mode 100644
index 00000000..957d9de9
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/NicknameDuplicationException.java
@@ -0,0 +1,11 @@
+package com.moing.backend.domain.auth.exception;
+
+import com.moing.backend.global.response.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public class NicknameDuplicationException extends AuthException {
+ public NicknameDuplicationException(){
+ super(ErrorCode.NICKNAME_DUPLICATION_ERROR,
+ HttpStatus.BAD_REQUEST);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/TokenInvalidException.java b/src/main/java/com/moing/backend/domain/auth/exception/TokenInvalidException.java
new file mode 100644
index 00000000..65dc4e0b
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/TokenInvalidException.java
@@ -0,0 +1,11 @@
+package com.moing.backend.domain.auth.exception;
+
+import com.moing.backend.global.response.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public class TokenInvalidException extends AuthException {
+ public TokenInvalidException() {
+ super(ErrorCode.TOKEN_INVALID_ERROR,
+ HttpStatus.CONFLICT);
+ }
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/exception/UnknownProviderException.java b/src/main/java/com/moing/backend/domain/auth/exception/UnknownProviderException.java
new file mode 100644
index 00000000..baeec5ac
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/exception/UnknownProviderException.java
@@ -0,0 +1,4 @@
+package com.moing.backend.domain.auth.exception;
+
+public class UnknownProviderException {
+}
diff --git a/src/main/java/com/moing/backend/domain/auth/presentation/AuthController.java b/src/main/java/com/moing/backend/domain/auth/presentation/AuthController.java
new file mode 100644
index 00000000..4f459e26
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/presentation/AuthController.java
@@ -0,0 +1,85 @@
+package com.moing.backend.domain.auth.presentation;
+
+import com.moing.backend.domain.auth.application.dto.request.SignInRequest;
+import com.moing.backend.domain.auth.application.dto.request.SignUpRequest;
+import com.moing.backend.domain.auth.application.dto.request.TestRequest;
+import com.moing.backend.domain.auth.application.dto.response.CheckNicknameResponse;
+import com.moing.backend.domain.auth.application.dto.response.ReissueTokenResponse;
+import com.moing.backend.domain.auth.application.dto.response.SignInResponse;
+import com.moing.backend.domain.auth.application.service.CheckNicknameUseCase;
+import com.moing.backend.domain.auth.application.service.ReissueTokenUseCase;
+import com.moing.backend.domain.auth.application.service.SignInUseCase;
+import com.moing.backend.domain.auth.application.service.SignUpUseCase;
+import com.moing.backend.global.response.SuccessResponse;
+import lombok.AllArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+import static com.moing.backend.domain.auth.presentation.constant.AuthResponseMessage.*;
+
+@RestController
+@AllArgsConstructor
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ private final SignInUseCase authService;
+ private final SignUpUseCase signUpService;
+ private final ReissueTokenUseCase reissueTokenService;
+ private final CheckNicknameUseCase checkNicknameService;
+
+ /**
+ * 소셜 로그인 (애플/ 카카오/구글)
+ * [POST] api/auth/signIn/kakao||apple||google
+ * 작성자 : 김민수
+ */
+ @PostMapping("/signIn/{provider}")
+ public ResponseEntity> signIn(@PathVariable String provider,
+ @Valid @RequestBody SignInRequest signInRequest) {
+ return ResponseEntity.ok(SuccessResponse.create(SIGN_IN_SUCCESS.getMessage(), this.authService.signIn(signInRequest, provider)));
+ }
+
+ /**
+ * 회원가입 (초기 로그인한 사용자 닉네임 입력)
+ * [PUT] api/auth/signUp
+ * 작성자 : 김민수
+ */
+ @PutMapping("/signUp")
+ public ResponseEntity> signUp(@RequestHeader(value = "Authorization") String token,
+ @Valid @RequestBody SignUpRequest signUpRequest) {
+ token = (token != null && token.startsWith("Bearer ")) ? token.substring(7) : token;
+ return ResponseEntity.ok(SuccessResponse.create(SIGN_UP_SUCCESS.getMessage(), this.signUpService.signUp(token, signUpRequest)));
+ }
+ /**
+ * 토큰 재발급
+ * [GET] api/auth/reissue
+ * 작성자 : 김민수
+ */
+ @GetMapping("/reissue")
+ public ResponseEntity> reissue(@RequestHeader(value = "RefreshToken") String token) {
+ ReissueTokenResponse reissueToken = reissueTokenService.reissueToken(token);
+ return ResponseEntity.ok(SuccessResponse.create(REISSUE_TOKEN_SUCCESS.getMessage(), reissueToken));
+ }
+
+ /**
+ * 닉네임 중복검사
+ * [GET] api/auth/checkNickName?nickname={}
+ * 작성자 : 김민수
+ */
+ @GetMapping("/checkNickname")
+ public ResponseEntity> checkNickname(@RequestParam String nickname){
+ return ResponseEntity.ok(SuccessResponse.create(CHECK_NICKNAME_SUCCESS.getMessage(), checkNicknameService.checkNickname(nickname)));
+ }
+
+ /**
+ * 테스트 계정 로그인 (토큰 만드는 컨트롤러)
+ * [POST] api/auth/test
+ * 작성자: 김민수
+ */
+ @PostMapping("/test/{provider}")
+ public ResponseEntity> testLogin(@PathVariable String provider,
+ @RequestBody TestRequest testRequest){
+ return ResponseEntity.ok(SuccessResponse.create(SIGN_IN_SUCCESS.getMessage(), this.authService.testSignIn(testRequest.getFcmToken(), testRequest.getSocialId(),provider)));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/moing/backend/domain/auth/presentation/AuthRedirectController.java b/src/main/java/com/moing/backend/domain/auth/presentation/AuthRedirectController.java
new file mode 100644
index 00000000..36b1f66b
--- /dev/null
+++ b/src/main/java/com/moing/backend/domain/auth/presentation/AuthRedirectController.java
@@ -0,0 +1,42 @@
+package com.moing.backend.domain.auth.presentation;
+
+import lombok.AllArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+//@RestController
+//@AllArgsConstructor
+//public class AuthRedirectController {
+// @Value("${android.package}")
+// private String androidPackage;
+//
+// @Value("${android.scheme}")
+// private String androidScheme;
+//
+// /**
+// * 애플 로그인 리다이렉트
+// */
+// @CrossOrigin(origins = "https://appleid.apple.com")
+// @PostMapping(value = "/auth/apple/callback", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+// public ResponseEntity